diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-11-11 16:30:54 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-11-11 16:30:54 +0000 |
| commit | 8e70e36563e1b32280b1391bbd390e164bd13b52 (patch) | |
| tree | f8bd645e3e7900ebae0dcd614447c69409507e15 /server/internal | |
| parent | 58d87473d4da30069abbf72c352d08c40b6c56eb (diff) | |
The audio URL stops carrying the token
Apache logs the request line, so a token in the query string was a copy of the
secret in every line of a file logrotate keeps fourteen of. That is why the
vhosts log /audio/ without query strings at all; this removes the reason they
had to.
Safe now because both halves of the phone send the header: the downloader
always did, and the player does since it got a ResolvingDataSource. Verified
against this server before changing it -- the player reaches READY on
listen.terst.org audio with a header and no token in the URL.
Query tokens are still *accepted*. Withdrawing that as well would lock out an
older build with no way back in, and accepting one costs nothing now that
nothing emits it.
The transcript URL made this move already, for the same reason. This was the
last one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'server/internal')
| -rw-r--r-- | server/internal/api/api.go | 10 | ||||
| -rw-r--r-- | server/internal/api/api_test.go | 31 |
2 files changed, 37 insertions, 4 deletions
diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 5a11c92..c974ecb 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -254,8 +254,14 @@ func (s *Server) episode(w http.ResponseWriter, r *http.Request) { }) } writeJSON(w, http.StatusOK, episodeResponse{ - Status: "ready", - AudioURL: "/audio/" + found.ID + "?token=" + url.QueryEscape(s.opts.Token), + Status: "ready", + // No token. Apache logs the request line, so one in the query put a copy of the secret + // in every line of a file logrotate keeps fourteen of. The phone sends + // Authorization: Bearer for this host -- the downloader always did, and the player does + // too since it got a ResolvingDataSource -- and tokenOK takes either, so the query + // parameter cost a leak and bought nothing. Query tokens are still accepted, because + // withdrawing that as well would lock out an older build with no way back in. + AudioURL: "/audio/" + found.ID, DurationMs: found.DurationMs, IntroMs: found.IntroMs, OutroMs: found.OutroMs, diff --git a/server/internal/api/api_test.go b/server/internal/api/api_test.go index 5ae4435..79ee904 100644 --- a/server/internal/api/api_test.go +++ b/server/internal/api/api_test.go @@ -257,8 +257,27 @@ func TestEpisodeResponseCarriesTheKindOfEachBreak(t *testing.T) { } } -// The audio URL has to carry the token, or the phone gets a link it cannot use. -func TestTheAudioUrlIsUsableAsGiven(t *testing.T) { +// The audio URL carries no token. Apache logs the request line, so a token in the query was a +// copy of the secret in every line of a file logrotate keeps fourteen of. The phone sends +// Authorization: Bearer for this host now -- the downloader always did, and the player does +// since the ResolvingDataSource went in -- so the query parameter bought nothing. +func TestTheAudioUrlCarriesNoToken(t *testing.T) { + s, st, _, audioDir := newServer(t) + seedMirrored(t, st, audioDir, "audio-bytes") + + rec := get(t, s, "/api/episode?feed=https://f&guid=g1") + var got episodeResponse + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + + if strings.Contains(got.AudioURL, "token=") { + t.Errorf("url = %q, want no token in it", got.AudioURL) + } +} + +// Usable as given, given the header the phone already sends. +func TestTheAudioUrlIsUsableWithTheHeader(t *testing.T) { s, st, _, audioDir := newServer(t) seedMirrored(t, st, audioDir, "audio-bytes") @@ -269,6 +288,7 @@ func TestTheAudioUrlIsUsableAsGiven(t *testing.T) { } req := httptest.NewRequest(http.MethodGet, got.AudioURL, nil) + req.Header.Set("Authorization", "Bearer "+token) rec2 := httptest.NewRecorder() s.Handler().ServeHTTP(rec2, req) if rec2.Code != http.StatusOK { @@ -277,6 +297,13 @@ func TestTheAudioUrlIsUsableAsGiven(t *testing.T) { if rec2.Body.String() != "audio-bytes" { t.Errorf("body %q", rec2.Body.String()) } + + bare := httptest.NewRequest(http.MethodGet, got.AudioURL, nil) + rec3 := httptest.NewRecorder() + s.Handler().ServeHTTP(rec3, bare) + if rec3.Code != http.StatusUnauthorized { + t.Errorf("unauthenticated fetch returned %d, want 401", rec3.Code) + } } // Seeking is the normal case for a podcast, so byte ranges have to work. |
