package httptransport import ( "net/http" "os" "path" mr "git.sendico.io/tech/sxchnge/product/app/metrics" "git.sendico.io/tech/sxchnge/product/auth/config" "git.sendico.io/tech/sxchnge/product/auth/flow" httprouter "git.sendico.io/tech/sxchnge/product/auth/http" public "git.sendico.io/tech/sxchnge/product/auth/http/public" "git.sendico.io/tech/sxchnge/product/auth/response" authstorage "git.sendico.io/tech/sxchnge/product/auth/storage" accountservice "git.sendico.io/tech/sxchnge/product/customers" "git.sendico.io/tech/sxchnge/product/notifications" "git.sendico.io/tech/sxchnge/product/sessions/browser" "git.sendico.io/tech/sxchnge/product/twofactor" delivery "git.sendico.io/tech/sxchnge/product/twofactor/delivery" factorhttp "git.sendico.io/tech/sxchnge/product/twofactor/http" factorstore "git.sendico.io/tech/sxchnge/product/twofactor/store" "git.sendico.io/tech/sxchnge/shared/database" "git.sendico.io/tech/sxchnge/shared/errors" api "git.sendico.io/tech/sxchnge/shared/http" "git.sendico.io/tech/sxchnge/shared/http/response" "git.sendico.io/tech/sxchnge/shared/logging" "git.sendico.io/tech/sxchnge/shared/service" "github.com/go-chi/chi/v5" cm "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/metrics" "go.uber.org/zap" "moul.io/chizap" ) type Middleware struct { notifications *notifications.Client logger mlogger.Logger router *chi.Mux apiEndpoint string metrics mr.Metrics epdispatcher *httprouter.Dispatcher } func (mw *Middleware) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) { mw.epdispatcher.Handler(service, endpoint, method, handler) } func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) { mw.epdispatcher.AccountHandler(service, endpoint, method, handler) } func (mw *Middleware) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) { mw.epdispatcher.PendingAccountHandler(service, endpoint, method, handler) } func (mw *Middleware) Finish() { mw.metrics.Finish() if mw.notifications != nil { _ = mw.notifications.Close() } } func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) { mw.logger.Debug("Installing middleware stack...") // Collect metrics for application traffic only. Scrapes and health probes are // operational traffic and would otherwise distort request and in-flight SLIs. mw.router.Use(metrics.Collector(metrics.CollectorOpts{ Host: false, // avoid high-cardinality "host" label Proto: true, // include HTTP protocol label Skip: func(r *http.Request) bool { return isObservabilityRequest(mw.apiEndpoint, r.URL.Path) }, })) mw.router.Use(cm.RequestID) if debug { mw.router.Use(chizap.New(mw.logger.Named("http_trace"), &chizap.Opts{ WithReferer: true, WithUserAgent: true, })) } mw.router.Use(cors.Handler(cors.Options{ AllowedOrigins: config.CORS.AllowedOrigins, AllowedMethods: config.CORS.AllowedMethods, AllowedHeaders: config.CORS.AllowedHeaders, ExposedHeaders: config.CORS.ExposedHeaders, AllowCredentials: config.CORS.AllowCredentials, MaxAge: config.CORS.MaxAge, OptionsPassthrough: false, Debug: debug, })) mw.router.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := browser.ValidateCSRF(r, config.CORS.AllowedOrigins); err != nil { mw.logger.Info("Rejected browser request with invalid CSRF context", zap.String("method", r.Method), zap.String("path", r.URL.Path)) response.Forbidden(mw.logger, mservice.Accounts, "csrf_rejected", "browser request failed CSRF validation").ServeHTTP(w, r) return } next.ServeHTTP(w, r) }) }) mw.router.Use(cm.Recoverer) mw.router.Handle("/metrics", metrics.Handler()) mw.logger.Info("Middleware stack installation complete") } func isObservabilityRequest(apiEndpoint, requestPath string) bool { healthPath := path.Join("/", apiEndpoint, "health") metricsPath := path.Join("/", apiEndpoint, "metrics") return requestPath == healthPath || requestPath == metricsPath || requestPath == "/metrics" } func CreateMiddleware( logger mlogger.Logger, db db.Factory, apiRouter *chi.Mux, config *middleware.Config, authStorage *middleware.AuthStorageConfig, debug bool, notificationConfig ...*notifications.Config, ) (*Middleware, error) { if config == nil { return nil, merrors.InvalidArgument("middleware configuration is required", "middleware") } if err := config.Token.Validate(); err != nil { return nil, err } if err := config.CORS.Validate(); err != nil { return nil, err } loginRateLimits, err := config.ResolveLoginRateLimits() if err != nil { logger.Error("Invalid login rate limit configuration", zap.Error(err)) return nil, err } p := &Middleware{ logger: logger.Named("middleware"), router: apiRouter, apiEndpoint: os.Getenv(config.EndPointEnv), } p.logger.Info("Set endpoint", zap.String("endpoint", p.apiEndpoint)) p.installMiddleware(config, debug) if p.metrics, err = mr.NewMetricsRouter(p.logger, p.router, p.apiEndpoint); err != nil { p.logger.Error("Failed to create metrics router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint)) return nil, err } stores, err := authstorage.OpenStores(p.logger, db, authStorage) if err != nil { return nil, err } var authFlow *flow.Service var factors *twofactor.Service if len(notificationConfig) > 0 && notificationConfig[0] != nil { c := notificationConfig[0] customers, err := accountservice.NewAccountService(p.logger, db, &config.Password, authStorage) if err != nil { return nil, err } authFlow = &flow.Service{Accounts: stores.Accounts, Customers: customers, Tokens: stores.Verification, Transactions: db.TransactionFactory(), Attempts: stores.LoginAttempts, Secret: middleware.SignatureConf(config).PrivateKey, ActivationURL: c.ActivationURL} if err = authFlow.Validate(); err != nil { return nil, err } p.notifications, err = notifications.Open(*c) if err != nil { return nil, err } authFlow.Sender = p.notifications records, err := factorstore.Open(p.logger, db) if err != nil { _ = p.notifications.Close() return nil, err } factors, err = twofactor.New(stores.Accounts, stores.Verification, records, db.TransactionFactory(), middleware.SignatureConf(config).PrivateKey, p.logger, delivery.Email{Sender: p.notifications}) if err != nil { _ = p.notifications.Close() return nil, err } } flows := public.Flows{Onboarding: authFlow} if factors != nil { flows.Factors = factors } p.epdispatcher = httprouter.NewDispatcher( p.logger, p.router, stores.Accounts, stores.RefreshTokens, stores.LoginAttempts, config, loginRateLimits, flows, ) if factors != nil { factorhttp.Install(p.epdispatcher, factors, p.logger) } return p, nil }