1package publictls
2
3import (
4 "context"
5 "fmt"
6 "sync"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11)
12
13func TestHTTP01Challenges(t *testing.T) {
14 ctx := context.Background()
15
16 t.Run("full lifecycle", func(t *testing.T) {
17 ch := NewHTTP01Challenges()
18 require.NotNil(t, ch)
19
20 // Present a valid token
21 ch.Present("token", "key-auth")
22 got, ok := ch.Get(ctx, "token")
23 assert.True(t, ok)
24 assert.Equal(t, "key-auth", got)
25
26 // Clean up
27 ch.CleanUp("token")
28 _, ok = ch.Get(ctx, "token")
29 assert.False(t, ok)
30 })
31
32 t.Run("concurrent access is correct", func(t *testing.T) {
33 ch := NewHTTP01Challenges()
34
35 // Pre-populate some tokens.
36 ch.Present("existing-token", "existing-keyauth")
37
38 const numGoroutines = 20
39 var wg sync.WaitGroup
40 errCh := make(chan error, numGoroutines)
41
42 // Spawn concurrent readers and writers.
43 for i := range numGoroutines {
44 wg.Add(1)
45 go func(id int) {
46 defer wg.Done()
47 token := fmt.Sprintf("token-%d", id)
48 keyAuth := fmt.Sprintf("ka-%d", id)
49
50 // Every goroutine presents its own token.
51 ch.Present(token, keyAuth)
52
53 // Every goroutine reads back its own token.
54 got, ok := ch.Get(ctx, token)
55 if !ok {
56 errCh <- fmt.Errorf("token %q not found after Present", token)
57 return
58 }
59 if got != keyAuth {
60 errCh <- fmt.Errorf("token %q: got %q, want %q", token, got, keyAuth)
61 return
62 }
63
64 // Every goroutine reads the pre-populated existing token.
65 existing, ok := ch.Get(ctx, "existing-token")
66 if !ok {
67 errCh <- fmt.Errorf("existing-token not found by goroutine %d", id)
68 return
69 }
70 if existing != "existing-keyauth" {
71 errCh <- fmt.Errorf("existing-token: got %q, want existing-keyauth", existing)
72 return
73 }
74
75 // Every goroutine cleans up its own token.
76 ch.CleanUp(token)
77 _, ok = ch.Get(ctx, token)
78 if ok {
79 errCh <- fmt.Errorf("token %q should be gone after CleanUp", token)
80 return
81 }
82 }(i)
83 }
84
85 wg.Wait()
86 close(errCh)
87
88 // Collect any errors from goroutines.
89 for err := range errCh {
90 t.Error(err)
91 }
92
93 // Verify that pre-populated token is still present after all concurrent access.
94 existing, ok := ch.Get(ctx, "existing-token")
95 require.True(t, ok, "existing-token should survive concurrent access")
96 assert.Equal(t, "existing-keyauth", existing)
97 })
98}
99
100func TestHTTP01ChallengesRejectUnsafeToken(t *testing.T) {
101 ctx := context.Background()
102
103 t.Run("path traversal token ../secret is rejected", func(t *testing.T) {
104 ch := NewHTTP01Challenges()
105
106 ch.Present("../secret", "key-auth")
107 _, ok := ch.Get(ctx, "../secret")
108 assert.False(t, ok, "Present should silently ignore ../secret")
109 })
110
111 t.Run("token with slash is rejected", func(t *testing.T) {
112 ch := NewHTTP01Challenges()
113
114 ch.Present("a/b", "key-auth")
115 _, ok := ch.Get(ctx, "a/b")
116 assert.False(t, ok, "token containing / should be rejected")
117
118 // Get also rejects the unsafe token
119 _, ok = ch.Get(ctx, "safe/token")
120 assert.False(t, ok)
121 })
122
123 t.Run("token with backslash is rejected", func(t *testing.T) {
124 ch := NewHTTP01Challenges()
125
126 ch.Present("a\\b", "key-auth")
127 _, ok := ch.Get(ctx, "a\\b")
128 assert.False(t, ok, "token containing backslash should be rejected")
129
130 _, ok = ch.Get(ctx, "safe\\token")
131 assert.False(t, ok)
132 })
133
134 t.Run("empty token is rejected", func(t *testing.T) {
135 ch := NewHTTP01Challenges()
136
137 ch.Present("", "key-auth")
138 _, ok := ch.Get(ctx, "")
139 assert.False(t, ok, "empty token should be rejected")
140 })
141
142 t.Run("'..' token is rejected", func(t *testing.T) {
143 ch := NewHTTP01Challenges()
144
145 ch.Present("..", "key-auth")
146 _, ok := ch.Get(ctx, "..")
147 assert.False(t, ok, "token '..' should be rejected")
148 })
149
150 t.Run("empty keyAuth is silently ignored", func(t *testing.T) {
151 ch := NewHTTP01Challenges()
152 ch.Present("valid-token", "")
153 _, ok := ch.Get(ctx, "valid-token")
154 assert.False(t, ok, "Present with empty keyAuth should not store anything")
155 })
156
157 t.Run("valid tokens are accepted and retrievable", func(t *testing.T) {
158 ch := NewHTTP01Challenges()
159 ch.Present("valid-token", "key-auth-value")
160 got, ok := ch.Get(ctx, "valid-token")
161 assert.True(t, ok)
162 assert.Equal(t, "key-auth-value", got)
163 })
164}