253 lines
8.4 KiB
Go
253 lines
8.4 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
|
"allaboutapps.dev/aw/go-starter/internal/api/handlers"
|
|
"allaboutapps.dev/aw/go-starter/internal/api/handlers/constants"
|
|
"allaboutapps.dev/aw/go-starter/internal/api/middleware"
|
|
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
|
|
"github.com/labstack/echo-contrib/echoprometheus"
|
|
"github.com/labstack/echo/v4"
|
|
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
// #nosec G108 - pprof handlers (conditionally made available via http.DefaultServeMux)
|
|
"net/http/pprof"
|
|
)
|
|
|
|
func Init(s *api.Server) error {
|
|
s.Echo = echo.New()
|
|
|
|
viewsRenderer := &echoRenderer{
|
|
templates: map[templates.ViewTemplate]*template.Template{},
|
|
}
|
|
|
|
files, err := os.ReadDir(s.Config.Echo.WebTemplatesViewsBaseDirAbs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read views templates dir: %w", err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
continue
|
|
}
|
|
|
|
templateName := file.Name()
|
|
t, err := template.New(templateName).ParseGlob(filepath.Join(s.Config.Echo.WebTemplatesViewsBaseDirAbs, templateName))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse template file: %w", err)
|
|
}
|
|
|
|
viewsRenderer.templates[templates.ViewTemplate(templateName)] = t
|
|
}
|
|
|
|
s.Echo.Renderer = viewsRenderer
|
|
|
|
s.Echo.Debug = s.Config.Echo.Debug
|
|
s.Echo.HideBanner = true
|
|
s.Echo.Logger.SetOutput(&echoLogger{level: s.Config.Logger.RequestLevel, log: log.With().Str("component", "echo").Logger()})
|
|
echo.NotFoundHandler = NotFoundHandler(s.Config)
|
|
|
|
s.Echo.HTTPErrorHandler = HTTPErrorHandlerWithConfig(HTTPErrorHandlerConfig{
|
|
HideInternalServerErrorDetails: s.Config.Echo.HideInternalServerErrorDetails,
|
|
})
|
|
|
|
// ---
|
|
// General middleware
|
|
if s.Config.Management.EnableMetrics {
|
|
s.Echo.Use(echoprometheus.NewMiddleware(""))
|
|
err := s.Metrics.RegisterMetrics(context.Background())
|
|
if err != nil {
|
|
log.Err(err).Msg("Failed to register metrics")
|
|
return err
|
|
}
|
|
} else {
|
|
log.Warn().Msg("Disabling metrics middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableTrailingSlashMiddleware {
|
|
s.Echo.Pre(echoMiddleware.RemoveTrailingSlash())
|
|
} else {
|
|
log.Warn().Msg("Disabling trailing slash middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableRecoverMiddleware {
|
|
s.Echo.Use(echoMiddleware.RecoverWithConfig(echoMiddleware.RecoverConfig{
|
|
LogErrorFunc: middleware.LogErrorFuncWithRequestInfo,
|
|
}))
|
|
} else {
|
|
log.Warn().Msg("Disabling recover middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableSecureMiddleware {
|
|
s.Echo.Use(echoMiddleware.SecureWithConfig(echoMiddleware.SecureConfig{
|
|
Skipper: echoMiddleware.DefaultSecureConfig.Skipper,
|
|
XSSProtection: s.Config.Echo.SecureMiddleware.XSSProtection,
|
|
ContentTypeNosniff: s.Config.Echo.SecureMiddleware.ContentTypeNosniff,
|
|
XFrameOptions: s.Config.Echo.SecureMiddleware.XFrameOptions,
|
|
HSTSMaxAge: s.Config.Echo.SecureMiddleware.HSTSMaxAge,
|
|
HSTSExcludeSubdomains: s.Config.Echo.SecureMiddleware.HSTSExcludeSubdomains,
|
|
ContentSecurityPolicy: s.Config.Echo.SecureMiddleware.ContentSecurityPolicy,
|
|
CSPReportOnly: s.Config.Echo.SecureMiddleware.CSPReportOnly,
|
|
HSTSPreloadEnabled: s.Config.Echo.SecureMiddleware.HSTSPreloadEnabled,
|
|
ReferrerPolicy: s.Config.Echo.SecureMiddleware.ReferrerPolicy,
|
|
}))
|
|
} else {
|
|
log.Warn().Msg("Disabling secure middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableRequestIDMiddleware {
|
|
s.Echo.Use(echoMiddleware.RequestID())
|
|
} else {
|
|
log.Warn().Msg("Disabling request ID middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableLoggerMiddleware {
|
|
s.Echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
Level: s.Config.Logger.RequestLevel,
|
|
LogRequestBody: s.Config.Logger.LogRequestBody,
|
|
LogRequestHeader: s.Config.Logger.LogRequestHeader,
|
|
LogRequestQuery: s.Config.Logger.LogRequestQuery,
|
|
LogResponseBody: s.Config.Logger.LogResponseBody,
|
|
LogResponseHeader: s.Config.Logger.LogResponseHeader,
|
|
LogCaller: s.Config.Logger.LogCaller,
|
|
RequestBodyLogSkipper: func(req *http.Request) bool {
|
|
// We skip all body logging for auth endpoints as these might contain credentials
|
|
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
|
|
return true
|
|
}
|
|
|
|
return middleware.DefaultRequestBodyLogSkipper(req)
|
|
},
|
|
ResponseBodyLogSkipper: func(req *http.Request, res *echo.Response) bool {
|
|
// We skip all body logging for auth endpoints as these might contain credentials
|
|
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
|
|
return true
|
|
}
|
|
|
|
return middleware.DefaultResponseBodyLogSkipper(req, res)
|
|
},
|
|
Skipper: func(c echo.Context) bool {
|
|
// We skip logging of readiness and liveness endpoints
|
|
switch c.Path() {
|
|
case "/-/ready", "/-/healthy":
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}))
|
|
} else {
|
|
log.Warn().Msg("Disabling logger middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableCORSMiddleware {
|
|
s.Echo.Use(echoMiddleware.CORS())
|
|
} else {
|
|
log.Warn().Msg("Disabling CORS middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Echo.EnableCacheControlMiddleware {
|
|
s.Echo.Use(middleware.CacheControl())
|
|
} else {
|
|
log.Warn().Msg("Disabling cache control middleware due to environment config")
|
|
}
|
|
|
|
if s.Config.Pprof.Enable {
|
|
pprofAuthMiddleware := middleware.Noop()
|
|
|
|
if s.Config.Pprof.EnableManagementKeyAuth {
|
|
pprofAuthMiddleware = echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
|
|
KeyLookup: "query:mgmt-secret",
|
|
Validator: func(key string, _ echo.Context) (bool, error) {
|
|
return key == s.Config.Management.Secret, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
s.Echo.GET("/debug/pprof", echo.WrapHandler(http.HandlerFunc(pprof.Index)), pprofAuthMiddleware)
|
|
s.Echo.Any("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux), pprofAuthMiddleware)
|
|
|
|
log.Warn().Bool("EnableManagementKeyAuth", s.Config.Pprof.EnableManagementKeyAuth).Msg("Pprof http handlers are available at /debug/pprof")
|
|
|
|
if s.Config.Pprof.RuntimeBlockProfileRate != 0 {
|
|
runtime.SetBlockProfileRate(s.Config.Pprof.RuntimeBlockProfileRate)
|
|
log.Warn().Int("RuntimeBlockProfileRate", s.Config.Pprof.RuntimeBlockProfileRate).Msg("Pprof runtime.SetBlockProfileRate")
|
|
}
|
|
|
|
if s.Config.Pprof.RuntimeMutexProfileFraction != 0 {
|
|
runtime.SetMutexProfileFraction(s.Config.Pprof.RuntimeMutexProfileFraction)
|
|
log.Warn().Int("RuntimeMutexProfileFraction", s.Config.Pprof.RuntimeMutexProfileFraction).Msg("Pprof runtime.SetMutexProfileFraction")
|
|
}
|
|
}
|
|
|
|
// Add your custom / additional middlewares here.
|
|
// see https://echo.labstack.com/middleware
|
|
|
|
// ---
|
|
// Initialize our general groups and set middleware to use above them
|
|
s.Router = &api.Router{
|
|
Routes: nil, // will be populated by handlers.AttachAllRoutes(s)
|
|
|
|
// Unsecured base group available at /**
|
|
Root: s.Echo.Group(""),
|
|
|
|
// Management endpoints, uncacheable, secured by key auth (query param), available at /-/**
|
|
Management: s.Echo.Group("/-", echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
|
|
KeyLookup: "query:mgmt-secret",
|
|
Validator: func(key string, _ echo.Context) (bool, error) {
|
|
return key == s.Config.Management.Secret, nil
|
|
},
|
|
Skipper: func(c echo.Context) bool {
|
|
//nolint:gocritic
|
|
switch c.Path() {
|
|
case "/-/ready":
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}), middleware.NoCache()),
|
|
|
|
// OAuth2, unsecured or secured by bearer auth, available at /api/v1/auth/**
|
|
APIV1Auth: s.Echo.Group("/api/v1/auth", middleware.AuthWithConfig(middleware.AuthConfig{
|
|
S: s,
|
|
Mode: middleware.AuthModeRequired,
|
|
Skipper: func(c echo.Context) bool {
|
|
switch c.Path() {
|
|
case "/api/v1/auth/forgot-password",
|
|
"/api/v1/auth/forgot-password/complete",
|
|
"/api/v1/auth/login",
|
|
"/api/v1/auth/refresh",
|
|
"/api/v1/auth/register",
|
|
fmt.Sprintf("/api/v1/auth/register/:%s", constants.RegistrationTokenParam):
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
})),
|
|
WellKnown: s.Echo.Group("/.well-known"),
|
|
|
|
// Your other endpoints, typically secured by bearer auth, available at /api/v1/**
|
|
APIV1Push: s.Echo.Group("/api/v1/push", middleware.Auth(s)),
|
|
}
|
|
|
|
// ---
|
|
// Finally attach our handlers
|
|
handlers.AttachAllRoutes(s)
|
|
|
|
if s.Config.Management.EnableMetrics {
|
|
log.Info().Msg("Metrics enabled and available under /metrics")
|
|
s.Echo.GET("/metrics", echoprometheus.NewHandler())
|
|
}
|
|
|
|
return nil
|
|
}
|