1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
package serve
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func setup(t *testing.T, token string) (http.Handler, string) {
t.Helper()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "feed.xml"), []byte("<rss/>"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "01 - One.mp3"), []byte("audio"), 0o644); err != nil {
t.Fatal(err)
}
return Handler(Options{Dir: dir, Token: token}), dir
}
func get(h http.Handler, target string, bearer string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, target, nil)
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestServesFeedAndAudioWithoutToken(t *testing.T) {
h, _ := setup(t, "")
if rec := get(h, "/feed.xml", ""); rec.Code != http.StatusOK {
t.Errorf("/feed.xml = %d, want 200", rec.Code)
}
if rec := get(h, "/audio/01%20-%20One.mp3", ""); rec.Code != http.StatusOK {
t.Errorf("audio = %d, want 200", rec.Code)
}
}
// The escaped URL the feed generator emits must actually resolve against the
// server that serves those files — the two halves have to agree.
func TestEscapedFilenameResolves(t *testing.T) {
h, dir := setup(t, "")
if err := os.WriteFile(filepath.Join(dir, "10 - Probability & Uncertainty.mp3"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
rec := get(h, "/audio/10%20-%20Probability%20&%20Uncertainty.mp3", "")
if rec.Code != http.StatusOK {
t.Errorf("escaped filename = %d, want 200", rec.Code)
}
}
func TestTokenRequiredWhenSet(t *testing.T) {
h, _ := setup(t, "s3cret")
for _, target := range []string{"/feed.xml", "/audio/01%20-%20One.mp3", "/"} {
if rec := get(h, target, ""); rec.Code != http.StatusNotFound {
t.Errorf("%s without a token = %d, want 404", target, rec.Code)
}
}
}
func TestTokenAcceptedInQuery(t *testing.T) {
h, _ := setup(t, "s3cret")
if rec := get(h, "/feed.xml?k=s3cret", ""); rec.Code != http.StatusOK {
t.Errorf("query token = %d, want 200", rec.Code)
}
}
// Podcast clients cannot do interactive auth, so the query form is the one
// that matters; the bearer header is for curl and for testing.
func TestTokenAcceptedAsBearer(t *testing.T) {
h, _ := setup(t, "s3cret")
if rec := get(h, "/feed.xml", "s3cret"); rec.Code != http.StatusOK {
t.Errorf("bearer token = %d, want 200", rec.Code)
}
}
func TestWrongTokenRejected(t *testing.T) {
h, _ := setup(t, "s3cret")
if rec := get(h, "/feed.xml?k=wrong", ""); rec.Code != http.StatusNotFound {
t.Errorf("wrong token = %d, want 404", rec.Code)
}
if rec := get(h, "/feed.xml", "wrong"); rec.Code != http.StatusNotFound {
t.Errorf("wrong bearer = %d, want 404", rec.Code)
}
}
// A gated server must not confirm that anything is there.
func TestRejectionLooksLikeAbsence(t *testing.T) {
h, _ := setup(t, "s3cret")
rec := get(h, "/feed.xml", "")
if rec.Code == http.StatusUnauthorized || rec.Code == http.StatusForbidden {
t.Errorf("status %d advertises that something exists here", rec.Code)
}
if strings.Contains(strings.ToLower(rec.Body.String()), "token") {
t.Error("the rejection body mentions a token, which tells a prober what to look for")
}
}
// http.FileServer resolves .. before dispatch, but this is the kind of thing
// worth a standing assertion rather than a memory of having checked once.
func TestNoPathTraversal(t *testing.T) {
h, dir := setup(t, "")
secret := filepath.Join(filepath.Dir(dir), "secret.txt")
if err := os.WriteFile(secret, []byte("do not serve"), 0o644); err != nil {
t.Fatal(err)
}
defer func() { _ = os.Remove(secret) }()
for _, target := range []string{
"/audio/../secret.txt",
"/audio/..%2fsecret.txt",
"/audio/%2e%2e/secret.txt",
} {
rec := get(h, target, "")
if strings.Contains(rec.Body.String(), "do not serve") {
t.Errorf("%s escaped the served directory", target)
}
}
}
func TestNewTokenIsRandomAndLongEnough(t *testing.T) {
a, err := NewToken()
if err != nil {
t.Fatalf("NewToken: %v", err)
}
b, _ := NewToken()
if a == b {
t.Error("two generated tokens were identical")
}
if len(a) < 32 {
t.Errorf("token is only %d chars; it is the only thing protecting the feed", len(a))
}
}
func TestUnknownPathIs404(t *testing.T) {
h, _ := setup(t, "")
if rec := get(h, "/nope", ""); rec.Code != http.StatusNotFound {
t.Errorf("unknown path = %d, want 404", rec.Code)
}
}
|