summaryrefslogtreecommitdiff
path: root/server/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-11-11 16:30:54 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-11-11 16:30:54 +0000
commit8e70e36563e1b32280b1391bbd390e164bd13b52 (patch)
treef8bd645e3e7900ebae0dcd614447c69409507e15 /server/internal
parent58d87473d4da30069abbf72c352d08c40b6c56eb (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.go10
-rw-r--r--server/internal/api/api_test.go31
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.