/music/rand and the other song endpoints return a different song from the same URL, but the responses had a Last-Modified header and no Cache-Control, so browsers cached them. The same song played again and the server never picked a new one, so /music/addQue added nothing. Send Cache-Control: no-store again, as the server did before the rewrite. Also allow the Cache-Control request header in CORS, which the frontend sends to bypass responses that browsers have already cached.
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"music-server/cmd/web"
|
|
"music-server/internal/logging"
|
|
"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())
|
|
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.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)
|
|
}
|
|
}
|