tavern/web/command.go

317 lines
7.8 KiB
Go

package web
import (
"context"
"fmt"
"html/template"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/foolin/goview"
"github.com/foolin/goview/supports/ginview"
"github.com/getsentry/sentry-go"
sentrygin "github.com/getsentry/sentry-go/gin"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/static"
ginzap "github.com/gin-contrib/zap"
"github.com/gin-gonic/gin"
ut "github.com/go-playground/universal-translator"
"github.com/oklog/run"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"github.com/ngerakines/tavern/config"
"github.com/ngerakines/tavern/g"
"github.com/ngerakines/tavern/job"
"github.com/ngerakines/tavern/storage"
)
var Command = cli.Command{
Name: "server",
Usage: "Run the web server.",
Flags: []cli.Flag{
&config.EnvironmentFlag,
&config.ListenFlag,
&config.DomainFlag,
&config.DatabaseFlag,
&config.TranslationsFlag,
&config.SecretFlag,
&cli.StringFlag{
Name: "admin-name",
Usage: "The name of the admin user",
EnvVars: []string{"ADMIN_NAME"},
Value: "nick",
},
&config.EnableSVGerFlag,
&config.SVGerEndpointFlag,
},
Action: serverCommandAction,
}
func serverCommandAction(cliCtx *cli.Context) error {
logger, err := config.Logger(cliCtx)
if err != nil {
return err
}
domain := cliCtx.String("domain")
siteBase := fmt.Sprintf("https://%s", domain)
logger.Info("Starting",
zap.String("command", cliCtx.Command.Name),
zap.String("GOOS", runtime.GOOS),
zap.String("site", siteBase),
zap.String("env", cliCtx.String("environment")))
ohioLocal, err = time.LoadLocation("America/New_York")
if err != nil {
return err
}
sentryConfig, err := config.NewSentryConfig(cliCtx)
if err != nil {
return err
}
svgerConfig := config.NewSVGerConfig(cliCtx)
if sentryConfig.Enabled {
err = sentry.Init(sentry.ClientOptions{
Dsn: sentryConfig.Key,
Environment: cliCtx.String("environment"),
Release: fmt.Sprintf("%s-%s", g.ReleaseCode, g.GitCommit),
})
if err != nil {
return err
}
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetTags(map[string]string{"container": "server"})
})
defer sentry.Recover()
}
db, dbClose, err := config.DB(cliCtx, logger)
if err != nil {
return err
}
defer dbClose()
s := storage.DefaultStorage(db)
utrans, err := config.Trans(cliCtx)
if err != nil {
panic(err)
return err
}
r := gin.New()
if sentryConfig.Enabled {
r.Use(sentrygin.New(sentrygin.Options{
Repanic: true,
}))
}
r.HTMLRender = ginview.New(goview.Config{
Root: "templates",
Extension: ".html",
Master: filepath.Join("layouts", "master"),
Partials: []string{
"partials/flashes",
"partials/dashboard_menu",
"partials/activity_feed",
"partials/activity_create_note",
"partials/activity_announce_note",
},
Funcs: template.FuncMap{
"date": tmplDate,
"odate": tmplOptionalDate,
"time": tmplTime,
"otime": tmplOptionalTime,
"datetime": tmplDateTime,
"odatetime": tmplOptionalDateTime,
"shortUUID": tmplShortUUID,
"short": tmplShort,
"title": strings.Title,
"a": tmplAnchor,
"toHTML": tmplToHTML,
"strong": tmplStrong,
"wrapMaybe": tmplWrapMaybe,
"url": tmplUrlGen(siteBase),
"dict": tmplDict,
"lookupStr": tmplLookupStr,
"lookupBool": tmplLookupBool,
"urlEncode": tmplUrlEncode,
},
DisableCache: true,
})
r.Use(func(i *gin.Context) {
nonce := i.GetHeader("X-Request-ID")
if nonce != "" {
i.Set("nonce", nonce)
}
i.Next()
})
r.Use(ginzap.Ginzap(logger, time.RFC3339, true))
r.GET("/health", func(i *gin.Context) {
i.Data(200, "text/plain", []byte("OK"))
})
r.Use(static.Serve("/public", static.LocalFile("./public", false)))
secret, err := config.Secret(cliCtx)
if err != nil {
return err
}
webFingerQueue := storage.NewStringQueue()
crawlQueue := storage.NewStringQueue()
var svgConv SVGConverter
if svgerConfig.Enabled {
svgConv = DefaultSVGerClient(svgerConfig.Location)
}
h := handler{
storage: s,
logger: logger,
domain: domain,
sentryConfig: sentryConfig,
webFingerQueue: webFingerQueue,
crawlQueue: crawlQueue,
adminUser: cliCtx.String("admin-name"),
url: tmplUrlGen(siteBase),
svgConverter: svgConv,
}
configI18nMiddleware(sentryConfig, logger, utrans, domain, r)
root := r.Group("/")
{
root.GET("/.well-known/webfinger", h.webFinger)
root.GET("/.well-known/nodeinfo", h.nodeInfo)
root.GET("/nodeinfo/2.0", h.nodeInfoDetails)
}
apiV1 := r.Group("/api/v1")
{
apiV1.GET("/instance", h.apiV1Instance)
apiV1.GET("/instance/peers", h.apiV1InstancePeers)
}
root.GET("/activity/:activity", h.getActivity)
root.GET("/object/:object", h.getObject)
root.GET("/tags/:tag", h.getTaggedObjects)
root.GET("/avatar/svg/:domain/:name", h.avatarSVG)
if svgerConfig.Enabled {
root.GET("/avatar/png/:domain/:name", h.avatarPNG)
}
authenticated := r.Group("/")
{
configSessionMiddleware(secret, domain, authenticated)
authenticated.GET("/", h.home)
authenticated.POST("/signin", h.signin)
authenticated.GET("/signout", h.signout)
authenticated.GET("/feed", h.viewFeed)
authenticated.GET("/feed/mine", h.viewMyFeed)
authenticated.GET("/feed/local", h.viewFeed)
authenticated.GET("/network", h.dashboardNetwork)
authenticated.POST("/network/follow", h.networkFollow)
authenticated.POST("/network/unfollow", h.networkUnfollow)
authenticated.POST("/network/accept", h.networkAccept)
authenticated.POST("/network/reject", h.networkReject)
authenticated.GET("/compose", h.compose)
authenticated.POST("/compose/create/note", h.createNote)
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
authenticated.GET("/configure", h.configure)
authenticated.GET("/utilities", h.utilities)
authenticated.POST("/utilities/webfinger", h.utilitiesWebfinger)
}
actor := r.Group("/users")
{
actor.GET("/:name", h.actorInfo)
actor.POST("/:name/inbox", h.actorInbox)
actor.GET("/:name/outbox", h.actorOutbox)
actor.GET("/:name/following", h.actorFollowing)
actor.GET("/:name/followers", h.actorFollowers)
}
var group run.Group
srv := &http.Server{
Addr: config.ListenAddress(cliCtx),
Handler: r,
}
parentCtx, parentCtxCancel := context.WithCancel(context.Background())
defer parentCtxCancel()
group.Add(func() error {
logger.Info("starting http service", zap.String("address", srv.Addr))
return srv.ListenAndServe()
}, func(error) {
httpCancelCtx, httpCancelCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer httpCancelCtxCancel()
logger.Info("stopping http service")
if err := srv.Shutdown(httpCancelCtx); err != nil {
logger.Info("error stopping http service:", zap.Error(err))
}
})
webFingerJob := job.NewWebFingerWorker(logger, webFingerQueue, s)
job.RunWorker(&group, logger, webFingerJob, parentCtx)
crawlJob := job.NewCrawlWorker(logger, crawlQueue, s)
job.RunWorker(&group, logger, crawlJob, parentCtx)
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
group.Add(func() error {
logger.Info("starting signal listener")
<-quit
return nil
}, func(error) {
logger.Info("stopping signal listener")
close(quit)
})
return group.Run()
}
func configI18nMiddleware(sentryCfg config.SentryConfig, logger *zap.Logger, utrans *ut.UniversalTranslator, domain string, r gin.IRouter) {
im := i18nMiddleware{
utrans: utrans,
logger: logger,
domain: domain,
enableSentry: sentryCfg.Enabled,
}
r.Use(im.findLocale)
}
func configSessionMiddleware(secret []byte, domain string, r gin.IRouter) {
store := cookie.NewStore(secret)
store.Options(sessions.Options{
Path: "/",
Domain: domain,
MaxAge: 86400 * 30,
Secure: true,
HttpOnly: true,
})
r.Use(sessions.Sessions("sess", store))
}