package internal import ( "context" "io" "net/http" "net/http/httptest" "net/http/httputil" "os" "strings" "sync" "time" "github.com/gorilla/mux" "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" federationsenderAPI "github.com/matrix-org/dendrite/federationsender/api" "github.com/matrix-org/dendrite/internal/config" "github.com/matrix-org/dendrite/internal/httpapis" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" ) // BasicAuth is used for authorization on /metrics handlers type BasicAuth struct { Username string `yaml:"username"` Password string `yaml:"password"` } // MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request. func MakeAuthAPI( metricsName string, data auth.Data, f func(*http.Request, *authtypes.Device) util.JSONResponse, ) http.Handler { h := func(req *http.Request) util.JSONResponse { device, err := auth.VerifyUserFromRequest(req, data) if err != nil { return *err } // add the user ID to the logger logger := util.GetLogger((req.Context())) logger = logger.WithField("user_id", device.UserID) req = req.WithContext(util.ContextWithLogger(req.Context(), logger)) return f(req, device) } return MakeExternalAPI(metricsName, h) } // MakeExternalAPI turns a util.JSONRequestHandler function into an http.Handler. // This is used for APIs that are called from the internet. func MakeExternalAPI(metricsName string, f func(*http.Request) util.JSONResponse) http.Handler { // TODO: We shouldn't be directly reading env vars here, inject it in instead. // Refactor this when we split out config structs. verbose := false if os.Getenv("DENDRITE_TRACE_HTTP") == "1" { verbose = true } h := util.MakeJSONAPI(util.NewJSONRequestHandler(f)) withSpan := func(w http.ResponseWriter, req *http.Request) { nextWriter := w if verbose { logger := logrus.NewEntry(logrus.StandardLogger()) // Log outgoing response rec := httptest.NewRecorder() nextWriter = rec defer func() { resp := rec.Result() dump, err := httputil.DumpResponse(resp, true) if err != nil { logger.Debugf("Failed to dump outgoing response: %s", err) } else { strSlice := strings.Split(string(dump), "\n") for _, s := range strSlice { logger.Debug(s) } } // copy the response to the client for hdr, vals := range resp.Header { for _, val := range vals { w.Header().Add(hdr, val) } } w.WriteHeader(resp.StatusCode) // discard errors as this is for debugging _, _ = io.Copy(w, resp.Body) _ = resp.Body.Close() }() // Log incoming request dump, err := httputil.DumpRequest(req, true) if err != nil { logger.Debugf("Failed to dump incoming request: %s", err) } else { strSlice := strings.Split(string(dump), "\n") for _, s := range strSlice { logger.Debug(s) } } } span := opentracing.StartSpan(metricsName) defer span.Finish() req = req.WithContext(opentracing.ContextWithSpan(req.Context(), span)) h.ServeHTTP(nextWriter, req) } return http.HandlerFunc(withSpan) } // MakeHTMLAPI adds Span metrics to the HTML Handler function // This is used to serve HTML alongside JSON error messages func MakeHTMLAPI(metricsName string, f func(http.ResponseWriter, *http.Request) *util.JSONResponse) http.Handler { withSpan := func(w http.ResponseWriter, req *http.Request) { span := opentracing.StartSpan(metricsName) defer span.Finish() req = req.WithContext(opentracing.ContextWithSpan(req.Context(), span)) if err := f(w, req); err != nil { h := util.MakeJSONAPI(util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { return *err })) h.ServeHTTP(w, req) } } return promhttp.InstrumentHandlerCounter( promauto.NewCounterVec( prometheus.CounterOpts{ Name: metricsName, Help: "Total number of http requests for HTML resources", }, []string{"code"}, ), http.HandlerFunc(withSpan), ) } // MakeInternalAPI turns a util.JSONRequestHandler function into an http.Handler. // This is used for APIs that are internal to dendrite. // If we are passed a tracing context in the request headers then we use that // as the parent of any tracing spans we create. func MakeInternalAPI(metricsName string, f func(*http.Request) util.JSONResponse) http.Handler { h := util.MakeJSONAPI(util.NewJSONRequestHandler(f)) withSpan := func(w http.ResponseWriter, req *http.Request) { carrier := opentracing.HTTPHeadersCarrier(req.Header) tracer := opentracing.GlobalTracer() clientContext, err := tracer.Extract(opentracing.HTTPHeaders, carrier) var span opentracing.Span if err == nil { // Default to a span without RPC context. span = tracer.StartSpan(metricsName) } else { // Set the RPC context. span = tracer.StartSpan(metricsName, ext.RPCServerOption(clientContext)) } defer span.Finish() req = req.WithContext(opentracing.ContextWithSpan(req.Context(), span)) h.ServeHTTP(w, req) } return http.HandlerFunc(withSpan) } // MakeFedAPI makes an http.Handler that checks matrix federation authentication. func MakeFedAPI( metricsName string, serverName gomatrixserverlib.ServerName, keyRing gomatrixserverlib.KeyRing, wakeup *FederationWakeups, f func(*http.Request, *gomatrixserverlib.FederationRequest, map[string]string) util.JSONResponse, ) http.Handler { h := func(req *http.Request) util.JSONResponse { fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest( req, time.Now(), serverName, keyRing, ) if fedReq == nil { return errResp } go wakeup.Wakeup(req.Context(), fedReq.Origin()) vars, err := URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) } return f(req, fedReq, vars) } return MakeExternalAPI(metricsName, h) } type FederationWakeups struct { FsAPI federationsenderAPI.FederationSenderInternalAPI origins sync.Map } func (f *FederationWakeups) Wakeup(ctx context.Context, origin gomatrixserverlib.ServerName) { key, keyok := f.origins.Load(origin) if keyok { lastTime, ok := key.(time.Time) if ok && time.Since(lastTime) < time.Minute { return } } aliveReq := federationsenderAPI.PerformServersAliveRequest{ Servers: []gomatrixserverlib.ServerName{origin}, } aliveRes := federationsenderAPI.PerformServersAliveResponse{} if err := f.FsAPI.PerformServersAlive(ctx, &aliveReq, &aliveRes); err != nil { util.GetLogger(ctx).WithError(err).WithFields(logrus.Fields{ "origin": origin, }).Warn("incoming federation request failed to notify server alive") } else { f.origins.Store(origin, time.Now()) } } // SetupHTTPAPI registers an HTTP API mux under /api and sets up a metrics // listener. func SetupHTTPAPI(servMux *http.ServeMux, publicApiMux *mux.Router, internalApiMux *mux.Router, cfg *config.Dendrite, enableHTTPAPIs bool) { if cfg.Metrics.Enabled { servMux.Handle("/metrics", WrapHandlerInBasicAuth(promhttp.Handler(), cfg.Metrics.BasicAuth)) } if enableHTTPAPIs { servMux.Handle(httpapis.InternalPathPrefix, internalApiMux) } servMux.Handle(httpapis.PublicPathPrefix, WrapHandlerInCORS(publicApiMux)) } // WrapHandlerInBasicAuth adds basic auth to a handler. Only used for /metrics func WrapHandlerInBasicAuth(h http.Handler, b BasicAuth) http.HandlerFunc { if b.Username == "" || b.Password == "" { logrus.Warn("Metrics are exposed without protection. Make sure you set up protection at proxy level.") } return func(w http.ResponseWriter, r *http.Request) { // Serve without authorization if either Username or Password is unset if b.Username == "" || b.Password == "" { h.ServeHTTP(w, r) return } user, pass, ok := r.BasicAuth() if !ok || user != b.Username || pass != b.Password { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } h.ServeHTTP(w, r) } } // WrapHandlerInCORS adds CORS headers to all responses, including all error // responses. // Handles OPTIONS requests directly. func WrapHandlerInCORS(h http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { // Its easiest just to always return a 200 OK for everything. Whether // this is technically correct or not is a question, but in the end this // is what a lot of other people do (including synapse) and the clients // are perfectly happy with it. w.WriteHeader(http.StatusOK) } else { h.ServeHTTP(w, r) } }) }