Allow CORS origins from CORS_ALLOWED_ORIGINS
CI / test (push) Successful in 23s
CI / publish (push) Skipped
CI / docker (push) Successful in 46s
CI / integration (push) Successful in 19s

Echo compares allowed origins literally, so "https://*" and "http://*" never
matched a real origin and no CORS headers were sent. The allowed origins now
come from CORS_ALLOWED_ORIGINS (comma-separated, checked at startup). When it
is empty, only same-origin requests work, which is enough for the bundled
frontend.

Credentials are no longer allowed since the API uses Bearer tokens. Browsers
may now send Range and X-Queue-Id and read X-Queue-Id and Content-Range.
This commit is contained in:
Sansan committed 2026-10-07 20:03:13 +02:00
1 parent a705d7b476
commit d31bf3551c
4 files changed
+85 -8

No files matched your search

+31
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"net/url"
"os"
"strconv"
"strings"
@@ -38,6 +39,10 @@ type Config struct {
// TrustedProxies are CIDR ranges whose X-Forwarded-For header is trusted when resolving client IPs.
TrustedProxies []*net.IPNet
// CORSAllowedOrigins are the browser origins, like https://music.example.com, that may call the API
// from another site. Empty means only same-origin requests, which is enough for the bundled frontend.
CORSAllowedOrigins []string
ReleasesURL string
}
@@ -91,6 +96,11 @@ func load(getenv func(string) string) (Config, error) {
errs = append(errs, fmt.Errorf("TRUSTED_PROXIES: %w", err))
}
cfg.CORSAllowedOrigins, err = parseOrigins(getenv("CORS_ALLOWED_ORIGINS"))
if err != nil {
errs = append(errs, fmt.Errorf("CORS_ALLOWED_ORIGINS: %w", err))
}
return cfg, errors.Join(errs...)
}
@@ -110,6 +120,27 @@ func parseCIDRs(value string) ([]*net.IPNet, error) {
return nets, nil
}
// parseOrigins parses a comma-separated list of origins (scheme://host[:port], nothing else).
func parseOrigins(value string) ([]string, error) {
var origins []string
for part := range strings.SplitSeq(value, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
u, err := url.Parse(part)
if err != nil {
return nil, err
}
if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.Path != "" || u.RawQuery != "" ||
u.Fragment != "" || u.User != nil || strings.Contains(u.Host, "*") {
return nil, fmt.Errorf("%q is not an origin like https://music.example.com", part)
}
origins = append(origins, u.Scheme+"://"+u.Host)
}
return origins, nil
}
func withDefault(value, fallback string) string {
if value == "" {
return fallback
+16
View File
@@ -31,6 +31,7 @@ func TestLoadDefaults(t *testing.T) {
assert.Equal(t, "songs", cfg.SoundCheckPath)
assert.Equal(t, DefaultReleasesURL, cfg.ReleasesURL)
assert.Empty(t, cfg.TrustedProxies)
assert.Empty(t, cfg.CORSAllowedOrigins)
}
func TestLoadMissingRequired(t *testing.T) {
@@ -56,6 +57,21 @@ func TestLoadTrustedProxies(t *testing.T) {
assert.ErrorContains(t, err, "TRUSTED_PROXIES")
}
func TestLoadCORSAllowedOrigins(t *testing.T) {
values := validEnv()
values["CORS_ALLOWED_ORIGINS"] = "https://music.example.com, http://localhost:5173,"
cfg, err := load(env(values))
require.NoError(t, err)
assert.Equal(t, []string{"https://music.example.com", "http://localhost:5173"}, cfg.CORSAllowedOrigins)
for _, invalid := range []string{"music.example.com", "ftp://example.com", "https://example.com/app",
"https://*.example.com", "https://*", "*", "https://example.com?x=1", "https://user@example.com"} {
values["CORS_ALLOWED_ORIGINS"] = invalid
_, err = load(env(values))
assert.ErrorContains(t, err, "CORS_ALLOWED_ORIGINS", invalid)
}
}
func TestLoadInvalidPort(t *testing.T) {
values := validEnv()
values["PORT"] = "abc"