audon/server.go

309 wiersze
8.3 KiB
Go

2022-12-03 03:20:49 +00:00
package main
import (
"context"
"crypto/rand"
"encoding/base32"
2022-12-03 03:20:49 +00:00
"encoding/gob"
"html/template"
"io"
"log"
"net/http"
"net/url"
"os"
2022-12-10 13:16:34 +00:00
"os/signal"
"strings"
2022-12-03 03:20:49 +00:00
"time"
"github.com/go-playground/validator/v10"
2022-12-05 07:45:51 +00:00
"github.com/go-redis/redis/v9"
2022-12-03 03:20:49 +00:00
"github.com/gorilla/sessions"
2023-01-23 12:10:21 +00:00
"github.com/jellydator/ttlcache/v3"
2022-12-03 03:20:49 +00:00
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
2022-12-05 02:38:34 +00:00
lksdk "github.com/livekit/server-sdk-go"
2022-12-03 03:20:49 +00:00
"github.com/mattn/go-mastodon"
2023-01-14 23:02:15 +00:00
"github.com/nicksnyder/go-i18n/v2/i18n"
2022-12-05 07:45:51 +00:00
"github.com/rbcervilla/redisstore/v9"
2022-12-03 03:20:49 +00:00
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type (
Template struct {
templates *template.Template
}
2023-01-04 10:48:24 +00:00
TemplateData struct {
Config *AppConfigBase
Room *Room
}
2022-12-03 03:20:49 +00:00
CustomValidator struct {
validator *validator.Validate
}
M map[string]interface{}
)
var (
2022-12-10 16:31:32 +00:00
// mastAppConfigBase *mastodon.AppConfig = nil
mainDB *mongo.Database = nil
mainValidator = validator.New()
2022-12-05 02:38:34 +00:00
mainConfig *AppConfig
lkRoomServiceClient *lksdk.RoomServiceClient
2023-01-14 23:02:15 +00:00
localeBundle *i18n.Bundle
2023-01-26 22:31:53 +00:00
userSessionCache *ttlcache.Cache[string, *SessionData]
2023-01-23 12:10:21 +00:00
webhookTimerCache *ttlcache.Cache[string, *time.Timer]
2023-01-26 22:31:53 +00:00
orphanRooms *ttlcache.Cache[string, bool]
2022-12-03 03:20:49 +00:00
)
func init() {
gob.Register(&SessionData{})
gob.Register(&M{})
}
func main() {
var err error
2022-12-10 16:31:32 +00:00
log.Println("Audon server started.")
// Load config from environment variables and .env
2022-12-10 17:44:18 +00:00
log.Println("Loading Audon config values")
mainConfig, err = loadConfig(os.Getenv("AUDON_ENV"))
if err != nil {
2022-12-10 17:44:18 +00:00
log.Fatalf("Failed loading config values: %s\n", err.Error())
2022-12-03 03:20:49 +00:00
}
2023-01-14 23:02:15 +00:00
// Load locales
localeBundle = initLocaleBundle()
2022-12-05 02:38:34 +00:00
// Setup Livekit RoomService Client
2022-12-05 07:45:51 +00:00
lkURL := &url.URL{
Scheme: "https",
Host: mainConfig.Livekit.Host,
}
if mainConfig.Environment == "development" {
lkURL.Scheme = "http"
}
2022-12-05 07:45:51 +00:00
lkRoomServiceClient = lksdk.NewRoomServiceClient(lkURL.String(), mainConfig.Livekit.APIKey, mainConfig.Livekit.APISecret)
2022-12-05 02:38:34 +00:00
2022-12-10 16:31:32 +00:00
backContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
2022-12-03 03:20:49 +00:00
defer cancel()
2022-12-04 17:52:44 +00:00
// Setup database client
2022-12-10 16:31:32 +00:00
log.Println("Connecting to DB")
2022-12-04 17:52:44 +00:00
dbClient, err := mongo.Connect(backContext, options.Client().ApplyURI(mainConfig.MongoURL.String()))
2022-12-13 18:02:25 +00:00
defer dbClient.Disconnect(backContext)
2022-12-03 03:20:49 +00:00
if err != nil {
2022-12-10 16:31:32 +00:00
log.Fatalf("Failed connecting to DB: %s\n", err.Error())
2022-12-03 03:20:49 +00:00
}
2022-12-04 05:19:41 +00:00
mainDB = dbClient.Database(mainConfig.Database.Name)
2022-12-04 17:52:44 +00:00
err = createIndexes(backContext)
2022-12-03 03:20:49 +00:00
if err != nil {
2022-12-10 16:31:32 +00:00
log.Fatalf("Failed creating indexes: %s\n", err.Error())
2022-12-03 03:20:49 +00:00
}
2022-12-05 07:45:51 +00:00
// Setup redis client
redisClient := redis.NewClient(&redis.Options{
Addr: mainConfig.Redis.Host,
Username: mainConfig.Redis.User,
Password: mainConfig.Redis.Password,
DB: 0,
})
2022-12-05 01:11:44 +00:00
// Setup echo server
2022-12-03 03:20:49 +00:00
e := echo.New()
defer e.Close()
e.Validator = &CustomValidator{validator: mainValidator}
2023-01-04 10:48:24 +00:00
e.Renderer = &Template{
templates: template.Must(template.New("tmpl").Delims("{%", "%}").ParseGlob("audon-fe/dist/index.html")),
}
2022-12-05 01:11:44 +00:00
// Setup session middleware (currently Audon stores all client data in cookie)
2022-12-10 16:31:32 +00:00
log.Println("Connecting to Redis")
2022-12-05 07:45:51 +00:00
redisStore, err := redisstore.NewRedisStore(backContext, redisClient)
if err != nil {
2022-12-10 16:31:32 +00:00
log.Fatalf("Failed connecting to Redis: %s\n", err.Error())
2022-12-05 07:45:51 +00:00
}
2022-12-13 18:02:25 +00:00
defer redisStore.Close()
redisStore.KeyGen(func() (string, error) {
k := make([]byte, 64)
if _, err := rand.Read(k); err != nil {
return "", err
}
return strings.TrimRight(base32.StdEncoding.EncodeToString(k), "="), nil
})
2022-12-05 07:45:51 +00:00
redisStore.KeyPrefix("session_")
sessionOptions := sessions.Options{
2022-12-04 17:52:44 +00:00
Path: "/",
Domain: mainConfig.LocalDomain,
MaxAge: 86400 * 30,
HttpOnly: true,
2022-12-10 19:45:37 +00:00
SameSite: http.SameSiteDefaultMode,
2022-12-04 17:52:44 +00:00
Secure: true,
}
if mainConfig.Environment == "development" {
2022-12-05 07:45:51 +00:00
sessionOptions.Domain = ""
2022-12-06 13:20:36 +00:00
sessionOptions.SameSite = http.SameSiteDefaultMode
2022-12-05 07:45:51 +00:00
sessionOptions.Secure = false
sessionOptions.MaxAge = 3600 * 24
sessionOptions.HttpOnly = false
2022-12-04 17:52:44 +00:00
}
2022-12-05 07:45:51 +00:00
redisStore.Options(sessionOptions)
e.Use(session.Middleware(redisStore))
2022-12-03 03:20:49 +00:00
2023-01-23 12:10:21 +00:00
// Setup caches
2023-01-30 02:30:10 +00:00
userSessionCache = ttlcache.New(ttlcache.WithTTL[string, *SessionData](168 * time.Hour))
webhookTimerCache = ttlcache.New(ttlcache.WithTTL[string, *time.Timer](5 * time.Minute))
2023-01-26 22:31:53 +00:00
orphanRooms = ttlcache.New(ttlcache.WithTTL[string, bool](24 * time.Hour))
go userSessionCache.Start()
2023-01-23 12:10:21 +00:00
go webhookTimerCache.Start()
2023-01-26 22:31:53 +00:00
go orphanRooms.Start()
2023-01-23 12:10:21 +00:00
2022-12-04 19:50:55 +00:00
e.POST("/app/login", loginHandler)
e.GET("/app/oauth", oauthHandler)
e.GET("/app/verify", verifyHandler)
e.POST("/app/logout", logoutHandler)
2022-12-29 14:53:42 +00:00
e.GET("/app/preview/:id", previewRoomHandler)
2023-01-24 06:03:15 +00:00
e.GET("/app/user/:id", getUserHandler)
2022-12-03 03:20:49 +00:00
2022-12-06 02:28:14 +00:00
e.POST("/app/webhook", livekitWebhookHandler)
2022-12-04 19:50:55 +00:00
api := e.Group("/api", authMiddleware)
2023-01-25 06:37:31 +00:00
api.GET("/token", getUserTokenHandler)
2023-01-25 20:58:15 +00:00
api.GET("/room", getStatusHandler)
2022-12-04 19:50:55 +00:00
api.POST("/room", createRoomHandler)
2023-01-23 12:10:21 +00:00
api.DELETE("/room", leaveRoomHandler)
api.POST("/room/:id", joinRoomHandler)
api.PATCH("/room/:id", updateRoomHandler)
2022-12-05 01:11:44 +00:00
api.DELETE("/room/:id", closeRoomHandler)
2023-01-29 03:16:21 +00:00
api.PUT("/room/:id", updateRoleHandler)
2022-12-05 01:11:44 +00:00
2022-12-09 14:59:37 +00:00
e.Static("/assets", "audon-fe/dist/assets")
2023-01-04 10:48:24 +00:00
e.Static("/static", "audon-fe/dist/static")
2023-01-23 12:10:21 +00:00
e.Static("/storage", mainConfig.StorageDir)
2023-01-04 10:48:24 +00:00
e.GET("/r/:id", renderRoomHandler)
2023-01-26 18:28:25 +00:00
e.GET("/u/:webfinger", redirectUserHandler)
2023-01-04 10:48:24 +00:00
e.GET("/*", func(c echo.Context) error {
return c.Render(http.StatusOK, "tmpl", &TemplateData{Config: &mainConfig.AppConfigBase})
})
// e.File("/*", "audon-fe/dist/index.html")
2022-12-05 07:45:51 +00:00
2022-12-10 13:16:34 +00:00
// use anonymous func to support graceful shutdown
go func() {
2022-12-10 16:31:32 +00:00
if err := e.Start(":8100"); err != nil && err != http.ErrServerClosed {
e.Logger.Fatalf("Shutting down the server: %s\n", err.Error())
2022-12-10 13:16:34 +00:00
}
}()
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
// Use a buffered channel to avoid missing signals as recommended for signal.Notify
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
e.Logger.Print("Attempting graceful shutdown")
defer shutdownCancel()
2023-01-26 22:31:53 +00:00
userSessionCache.DeleteAll()
2023-01-23 12:10:21 +00:00
webhookTimerCache.DeleteAll()
2023-01-26 22:31:53 +00:00
orphanRooms.DeleteAll()
2022-12-10 13:16:34 +00:00
if err := e.Shutdown(shutdownCtx); err != nil {
2022-12-10 16:31:32 +00:00
e.Logger.Fatalf("Failed shutting down gracefully: %s\n", err.Error())
2022-12-10 13:16:34 +00:00
}
2022-12-03 03:20:49 +00:00
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
return wrapValidationError(err)
}
return nil
}
2023-03-22 14:14:00 +00:00
func getAppConfig(server string) (*mastodon.AppConfig, error) {
2022-12-03 03:20:49 +00:00
redirectURI := "urn:ietf:wg:oauth:2.0:oob"
u := &url.URL{
Host: mainConfig.LocalDomain,
2022-12-04 05:19:41 +00:00
Scheme: "https",
Path: "/",
2022-12-03 03:20:49 +00:00
}
2022-12-04 19:50:55 +00:00
u = u.JoinPath("app", "oauth")
redirectURI = u.String()
2022-12-03 03:20:49 +00:00
conf := &mastodon.AppConfig{
2023-04-28 13:31:40 +00:00
ClientName: "Audon",
// Scopes: "read:accounts read:follows write:accounts",
Scopes: "read:accounts read:follows",
Website: "https://codeberg.org/nmkj/audon",
2022-12-03 03:20:49 +00:00
RedirectURIs: redirectURI,
}
2022-12-10 03:16:43 +00:00
// mastAppConfigBase = conf
2022-12-03 03:20:49 +00:00
return &mastodon.AppConfig{
Server: server,
ClientName: conf.ClientName,
Scopes: conf.Scopes,
Website: conf.Website,
RedirectURIs: conf.RedirectURIs,
}, nil
}
2022-12-04 19:50:55 +00:00
func getSession(c echo.Context, sessionID string) (sess *sessions.Session, err error) {
sess, err = session.Get(sessionID, c)
2022-12-03 03:20:49 +00:00
if err != nil {
return nil, err
}
return sess, nil
}
// retrieve user's session, returns invalid cookie error if failed
2022-12-04 19:50:55 +00:00
func getSessionData(c echo.Context) (data *SessionData, err error) {
sess, err := getSession(c, SESSION_NAME)
if err != nil {
c.Logger().Error(err)
return nil, ErrSessionNotAvailable
}
2022-12-03 03:20:49 +00:00
val := sess.Values[SESSION_DATASTORE_NAME]
data, ok := val.(*SessionData)
if !ok {
2022-12-04 19:50:55 +00:00
return nil, ErrInvalidSession
2022-12-03 03:20:49 +00:00
}
return data, nil
}
// write user's session, returns error if failed
func writeSessionData(c echo.Context, data *SessionData) error {
2022-12-04 19:50:55 +00:00
sess, err := getSession(c, SESSION_NAME)
2022-12-03 03:20:49 +00:00
if err != nil {
return err
}
2023-01-19 02:22:20 +00:00
if data == nil {
sess.Values[SESSION_DATASTORE_NAME] = ""
} else {
sess.Values[SESSION_DATASTORE_NAME] = data
}
2022-12-03 03:20:49 +00:00
2022-12-04 05:19:41 +00:00
return sess.Save(c.Request(), c.Response())
2022-12-03 03:20:49 +00:00
}
2022-12-04 19:50:55 +00:00
// handler for GET to /app/verify
func verifyHandler(c echo.Context) (err error) {
2022-12-05 07:45:51 +00:00
valid, acc, _ := verifyTokenInSession(c)
2022-12-04 19:50:55 +00:00
if !valid {
return c.NoContent(http.StatusUnauthorized)
}
2022-12-05 07:45:51 +00:00
return c.JSON(http.StatusOK, acc)
2022-12-04 19:50:55 +00:00
}