package server import ( "encoding/json" "net/http" "music-server/cmd/web" "music-server/internal/logging" "music-server/internal/metrics" "music-server/internal/server/middleware" "github.com/a-h/templ" "github.com/danielgtaylor/huma/v2" "github.com/labstack/echo/v5" echoMiddleware "github.com/labstack/echo/v5/middleware" "go.uber.org/zap" ) func (s *Server) RegisterRoutes() http.Handler { e, _ := s.routes() return e } // OpenAPISpec returns the API description. It needs no database or configuration: // the handlers are registered but never called. func OpenAPISpec() *huma.OpenAPI { _, a := (&Server{}).routes() return a.OpenAPI() } func (s *Server) routes() (*echo.Echo, huma.API) { e := echo.New() e.HTTPErrorHandler = httpErrorHandler if len(s.cfg.TrustedProxies) > 0 { var trust []echo.TrustOption for _, ipNet := range s.cfg.TrustedProxies { trust = append(trust, echo.TrustIPRange(ipNet)) } e.IPExtractor = echo.ExtractIPFromXFFHeader(trust...) } e.Use(logging.RequestLogger()) // Outside Recover, so requests that panic are counted with status 500. e.Use(metrics.Middleware()) e.Use(echoMiddleware.Recover()) // Without allowed origins no CORS headers are sent, so only same-origin browser requests work. // The API uses Bearer tokens, not cookies, so credentials are not allowed. if len(s.cfg.CORSAllowedOrigins) > 0 { e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ AllowOrigins: s.cfg.CORSAllowedOrigins, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Cache-Control", "Content-Type", "Range", middleware.QueueHeader}, ExposeHeaders: []string{middleware.QueueHeader, "Content-Range", "Accept-Ranges"}, MaxAge: 300, })) } // The API: /api/v1 is documented at /docs and /openapi.json. a := newAPI(e) a.UseMiddleware(middleware.ClientInfo, middleware.TokenAuth(a, s.q, s.cfg.DisableSessionAuth)) s.registerToken(a) s.registerMisc(a) s.registerSync(a) s.registerMusic(a) s.registerLibrary(a) s.registerCovers(a) s.registerUploads(a) s.registerStatistics(a) // Older links to the spec and docs. e.GET("/openapi", redirect("/openapi.json")) e.GET("/swagger/*", redirect("/docs")) // Search page and frontend. e.GET("/assets/*", echo.WrapHandler(http.FileServer(http.FS(web.Assets)))) e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm()))) e.POST("/find", echo.WrapHandler(web.NewSearchHandler(s.search))) e.GET("/config.js", s.frontendConfig) e.Static("/", s.cfg.FrontendPath) for _, r := range e.Router().Routes() { logging.GetLogger().Debug("Registered route", zap.String("method", r.Method), zap.String("path", r.Path)) } return e, a } // frontendConfig serves the runtime config of the bundled frontend. The frontend uses // the API password to get a token, so anyone who can open the frontend can read it. func (s *Server) frontendConfig(c *echo.Context) error { cfg, err := json.Marshal(map[string]string{ // Empty: the frontend calls this server with relative paths. "API_HOSTNAME": "", "API_PASSWORD": s.cfg.APIPassword, }) if err != nil { return err } c.Response().Header().Set("Cache-Control", "no-store") return c.Blob(http.StatusOK, "text/javascript; charset=utf-8", []byte("window.__RUNTIME_CONFIG__ = "+string(cfg)+";\n")) } func redirect(to string) echo.HandlerFunc { return func(c *echo.Context) error { return c.Redirect(http.StatusMovedPermanently, to) } }