265 lines
7.2 KiB
Go
265 lines
7.2 KiB
Go
|
package web
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"embed"
|
||
|
"encoding/gob"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"github.com/a-h/templ"
|
||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||
|
"github.com/goccy/go-json"
|
||
|
"github.com/gofiber/fiber/v2"
|
||
|
"github.com/gofiber/fiber/v2/log"
|
||
|
"github.com/gofiber/fiber/v2/middleware/earlydata"
|
||
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||
|
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||
|
"github.com/gofiber/fiber/v2/middleware/healthcheck"
|
||
|
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||
|
"github.com/gofiber/fiber/v2/middleware/session"
|
||
|
"github.com/gofiber/storage/postgres/v3"
|
||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
"github.com/spf13/viper"
|
||
|
"github.com/uptrace/bun"
|
||
|
"go.uber.org/zap"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"omnibill.net/omnibill/web/utils"
|
||
|
"omnibill.net/omnibill/web/views/layouts"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
//go:embed assets/**/*
|
||
|
var assetDir embed.FS
|
||
|
|
||
|
func Start(logger *zap.Logger, db *bun.DB, dbPool *pgxpool.Pool) {
|
||
|
|
||
|
panelURL, err := url.Parse(viper.GetString("omnibill.domain"))
|
||
|
if err != nil {
|
||
|
logger.Fatal("error parsing panel URL", zap.Error(err))
|
||
|
}
|
||
|
|
||
|
gob.Register(&webauthn.SessionData{})
|
||
|
webAuthnConfig := &webauthn.Config{
|
||
|
RPDisplayName: viper.GetString("omnibill.display_name"),
|
||
|
RPID: panelURL.Host,
|
||
|
RPOrigins: []string{panelURL.String()},
|
||
|
}
|
||
|
|
||
|
webAuthn, err := webauthn.New(webAuthnConfig)
|
||
|
if err != nil {
|
||
|
logger.Fatal("error creating webauthn", zap.Error(err))
|
||
|
}
|
||
|
|
||
|
appConfig := fiber.Config{
|
||
|
AppName: viper.GetString("omnibill.display_name"),
|
||
|
JSONEncoder: json.Marshal,
|
||
|
JSONDecoder: json.Unmarshal,
|
||
|
}
|
||
|
|
||
|
if len(viper.GetString("omnibill.webserver.proxy")) != 0 {
|
||
|
switch strings.ToLower(viper.GetString("omnibill.webserver.proxy")) {
|
||
|
case "cloudflare", "cf":
|
||
|
logger.Info("grabbing trusted proxy list")
|
||
|
|
||
|
var trustedProxies []string
|
||
|
|
||
|
v4Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v4/#", nil)
|
||
|
if err != nil {
|
||
|
logger.Fatal("error creating request", zap.Error(err))
|
||
|
}
|
||
|
|
||
|
v6Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v6/#", nil)
|
||
|
if err != nil {
|
||
|
logger.Fatal("error creating request", zap.Error(err))
|
||
|
}
|
||
|
|
||
|
client := &http.Client{}
|
||
|
|
||
|
v4Resp, err := client.Do(v4Req)
|
||
|
if err != nil {
|
||
|
logger.Fatal("error doing request", zap.Error(err))
|
||
|
}
|
||
|
defer v4Resp.Body.Close()
|
||
|
|
||
|
v4Scanner := bufio.NewScanner(v4Resp.Body)
|
||
|
v4Scanner.Split(bufio.ScanLines)
|
||
|
|
||
|
for v4Scanner.Scan() {
|
||
|
trustedProxies = append(trustedProxies, v4Scanner.Text())
|
||
|
}
|
||
|
|
||
|
v6Resp, err := client.Do(v6Req)
|
||
|
if err != nil {
|
||
|
logger.Fatal("error doing request", zap.Error(err))
|
||
|
}
|
||
|
defer v6Resp.Body.Close()
|
||
|
|
||
|
v6Scanner := bufio.NewScanner(v6Resp.Body)
|
||
|
v6Scanner.Split(bufio.ScanLines)
|
||
|
|
||
|
for v6Scanner.Scan() {
|
||
|
trustedProxies = append(trustedProxies, v6Scanner.Text())
|
||
|
}
|
||
|
|
||
|
appConfig.ProxyHeader = "X-Forwarded-For"
|
||
|
appConfig.TrustedProxies = trustedProxies
|
||
|
case "none":
|
||
|
default:
|
||
|
log.Warnf("Proxy '%s' is not supported", viper.GetString("omnibill.webserver.proxy"))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
app := fiber.New(appConfig)
|
||
|
app.Use(recover.New())
|
||
|
app.Use(earlydata.New())
|
||
|
app.Use(healthcheck.New())
|
||
|
app.Use(helmet.New())
|
||
|
app.Use(etag.New())
|
||
|
app.Use(limiter.New(limiter.Config{
|
||
|
Max: 250,
|
||
|
Expiration: 3 * time.Second,
|
||
|
LimiterMiddleware: limiter.SlidingWindow{},
|
||
|
}))
|
||
|
app.Use("/assets", filesystem.New(filesystem.Config{
|
||
|
Root: http.FS(assetDir),
|
||
|
PathPrefix: "assets",
|
||
|
Browse: false,
|
||
|
}))
|
||
|
|
||
|
storage := postgres.New(postgres.Config{
|
||
|
DB: dbPool,
|
||
|
Table: "sessions",
|
||
|
})
|
||
|
authSessionStore := session.New(session.Config{
|
||
|
Storage: storage,
|
||
|
})
|
||
|
sessionStore := session.New(session.Config{
|
||
|
KeyLookup: "cookie:osession",
|
||
|
})
|
||
|
|
||
|
for _, handler := range utils.Handlers {
|
||
|
handlerType := reflect.TypeOf(handler).Elem()
|
||
|
handlerValue := reflect.ValueOf(handler).Elem()
|
||
|
|
||
|
pathField, ok := handlerType.FieldByName("Path")
|
||
|
if !ok {
|
||
|
fmt.Println("invalid handler")
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
var requireAuth bool
|
||
|
|
||
|
omnibillTag := pathField.Tag.Get("omnibill")
|
||
|
for _, option := range strings.Split(omnibillTag, ",") {
|
||
|
switch option {
|
||
|
case "requireAuth":
|
||
|
requireAuth = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var pathHandlers []fiber.Handler
|
||
|
if requireAuth {
|
||
|
pathHandlers = append(pathHandlers, nil)
|
||
|
}
|
||
|
|
||
|
handlerValue.FieldByName("Db").Set(reflect.ValueOf(db))
|
||
|
handlerValue.FieldByName("AuthSessionStore").Set(reflect.ValueOf(authSessionStore))
|
||
|
handlerValue.FieldByName("SessionStore").Set(reflect.ValueOf(sessionStore))
|
||
|
handlerValue.FieldByName("Logger").Set(reflect.ValueOf(logger))
|
||
|
handlerValue.FieldByName("WebAuthn").Set(reflect.ValueOf(webAuthn))
|
||
|
|
||
|
path := handlerValue.FieldByName("Path").String()
|
||
|
if path == "index" {
|
||
|
path = ""
|
||
|
}
|
||
|
path = "/" + path
|
||
|
|
||
|
if iHandler, ok := handler.(utils.GET); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Get)
|
||
|
app.Get(path, func(ctx *fiber.Ctx) error {
|
||
|
sess, err := sessionStore.Get(ctx)
|
||
|
if err != nil {
|
||
|
return fiber.ErrInternalServerError
|
||
|
}
|
||
|
|
||
|
handlerValue.FieldByName("Session").Set(reflect.ValueOf(sess))
|
||
|
for _, pathHandler := range pathHandlers {
|
||
|
err := pathHandler(ctx)
|
||
|
if err != nil {
|
||
|
var e *fiber.Error
|
||
|
if errors.As(err, &e) {
|
||
|
return utils.Render(ctx, layouts.Error(*e), templ.WithStatus(e.Code))
|
||
|
} else {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.POST); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Post)
|
||
|
app.Post(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.PUT); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Put)
|
||
|
app.Put(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.DELETE); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Delete)
|
||
|
app.Delete(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.PATCH); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Patch)
|
||
|
app.Patch(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.OPTIONS); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Options)
|
||
|
app.Options(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
if iHandler, ok := handler.(utils.HEAD); ok {
|
||
|
pathHandlers = append(pathHandlers, iHandler.Head)
|
||
|
app.Head(path, func(ctx *fiber.Ctx) error {
|
||
|
return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func genericPathHandler(ctx *fiber.Ctx, handler reflect.Value, sessionStore *session.Store, handlers []fiber.Handler) error {
|
||
|
sess, err := sessionStore.Get(ctx)
|
||
|
if err != nil {
|
||
|
return fiber.ErrInternalServerError
|
||
|
}
|
||
|
|
||
|
handler.FieldByName("Session").Set(reflect.ValueOf(sess))
|
||
|
for _, pathHandler := range handlers {
|
||
|
err := pathHandler(ctx)
|
||
|
if err != nil {
|
||
|
var e *fiber.Error
|
||
|
if errors.As(err, &e) {
|
||
|
return err
|
||
|
} else {
|
||
|
return fiber.ErrInternalServerError
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|