package setup import ( "net/http" "time" "github.com/dariuszjaniak/warp/server/internal/buildinfo" "github.com/dariuszjaniak/warp/server/internal/httputil" ) // SpecVersions is what GET /_matrix/client/versions reports. // // Keep it honest: a version belongs here once the endpoints a client // actually uses from it work. Accounts, sessions, devices, profiles, // account data, rooms and — with Phase 4 — the event stream all do, which // is enough for a client to be used. What is still missing is end-to-end // encryption (Phase 7), media (Phase 6), push (Phase 8) and federation // (Phase 5); a client discovers each of those from the endpoint it needs // rather than from this list, which is why their absence does not make a // version listed here a lie. var SpecVersions = []string{"v1.13", "v1.14"} // UnstableFeatures advertises MSC support. Simplified Sliding Sync // (MSC4186) is Phase 10. var UnstableFeatures = map[string]bool{} type versionsResponse struct { Versions []string `json:"versions"` UnstableFeatures map[string]bool `json:"unstable_features,omitempty"` } // wellKnownClient is the /.well-known/matrix/client document. type wellKnownClient struct { Homeserver wellKnownBaseURL `json:"m.homeserver"` IdentityServer *wellKnownBaseURL `json:"m.identity_server,omitempty"` } type wellKnownBaseURL struct { BaseURL string `json:"base_url"` } type wellKnownServer struct { Server string `json:"m.server"` } type healthResponse struct { Status string `json:"status"` Version string `json:"version"` Database string `json:"database"` Error string `json:"error,omitempty"` } // routes registers every endpoint this server serves. func (s *Server) routes() *httputil.Router { r := httputil.NewRouter(s.metrics.Instrument) r.HandleFunc("GET /_matrix/client/versions", s.handleVersions) r.HandleFunc("GET /.well-known/matrix/client", s.handleWellKnownClient) // The server-side delegation document is part of the federation // surface, so it goes through the gate and disappears with it. r.Handle("GET /.well-known/matrix/server", s.federation.ProtectFunc(s.handleWellKnownServer)) s.clientAPI.RegisterRoutes(r) s.adminAPI.RegisterRoutes(r) r.HandleFunc("GET /healthz", s.handleHealth) if s.cfg.Metrics.Enabled { r.Handle("GET "+s.cfg.Metrics.Path, s.metrics.Handler()) } return r } // handleVersions implements GET /_matrix/client/versions. func (s *Server) handleVersions(w http.ResponseWriter, _ *http.Request) error { httputil.CacheFor(w, time.Hour) httputil.WriteJSON(w, http.StatusOK, versionsResponse{ Versions: SpecVersions, UnstableFeatures: UnstableFeatures, }) return nil } // handleWellKnownClient implements GET /.well-known/matrix/client, the // document a client fetches to turn "@me:warp.example" into an API URL. func (s *Server) handleWellKnownClient(w http.ResponseWriter, _ *http.Request) error { doc := wellKnownClient{ Homeserver: wellKnownBaseURL{BaseURL: s.cfg.ClientBaseURL()}, } if url := s.cfg.Global.WellKnown.IdentityServerBaseURL; url != "" { doc.IdentityServer = &wellKnownBaseURL{BaseURL: url} } httputil.CacheFor(w, time.Hour) httputil.WriteJSON(w, http.StatusOK, doc) return nil } // handleWellKnownServer implements GET /.well-known/matrix/server. With no // address configured there is nothing to delegate and the right answer is // 404: other servers then fall back to SRV and A record resolution. func (s *Server) handleWellKnownServer(w http.ResponseWriter, _ *http.Request) error { address := s.cfg.Global.WellKnown.ServerAddress if address == "" { return httputil.NotFound("No server delegation is configured") } httputil.CacheFor(w, time.Hour) httputil.WriteJSON(w, http.StatusOK, wellKnownServer{Server: address}) return nil } // handleHealth implements GET /healthz: a liveness and readiness check in // one. It reports 503 when the database is unreachable, which is what a // load balancer needs to stop sending traffic here. func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) error { resp := healthResponse{ Status: "ok", Version: buildinfo.Version, Database: s.db.Driver(), } status := http.StatusOK ctx, cancel := contextWithTimeout(r.Context(), 2*time.Second) defer cancel() if err := s.db.Ping(ctx); err != nil { resp.Status = "unavailable" resp.Error = "database unreachable" status = http.StatusServiceUnavailable httputil.LoggerFrom(r.Context()).Error("health check failed", "error", err) } httputil.NoCache(w) httputil.WriteJSON(w, status, resp) return nil }