@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 502 lines 16.6 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyauth
16
17 import (
18 "bufio"
19 "bytes"
20 "context"
21 "errors"
22 "io"
23 "net"
24 "net/http"
25 "net/http/httptest"
26 "testing"
27
28 "go.uber.org/zap"
29
30 "github.com/caddyserver/caddy/v2"
31 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
32 )
33
34 func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) {
35 auth := Authentication{
36 Providers: map[string]Authenticator{
37 "test": staticAuthenticator{
38 user: User{
39 ID: "alice",
40 Metadata: map[string]string{
41 "role": "admin",
42 },
43 },
44 },
45 },
46 logger: zap.NewNop(),
47 }
48 req, repl := newRequestWithReplacer()
49 nextCalled := false
50
51 err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
52 nextCalled = true
53 return nil
54 }))
55 if err == nil {
56 t.Fatal("expected authentication error")
57 }
58 var handlerErr caddyhttp.HandlerError
59 if !errors.As(err, &handlerErr) {
60 t.Fatalf("expected HandlerError, got %T", err)
61 }
62 if handlerErr.StatusCode != http.StatusUnauthorized {
63 t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode)
64 }
65 if nextCalled {
66 t.Fatal("next handler was called for rejected authentication")
67 }
68
69 assertPlaceholder(t, repl, "http.auth.candidate.id", "alice")
70 assertPlaceholder(t, repl, "http.auth.candidate.role", "admin")
71 assertPlaceholderAbsent(t, repl, "http.auth.user.id")
72 assertPlaceholderAbsent(t, repl, "http.auth.user.role")
73 }
74
75 func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) {
76 auth := Authentication{
77 Providers: map[string]Authenticator{
78 "test": staticAuthenticator{
79 user: User{
80 ID: "alice",
81 Metadata: map[string]string{
82 "role": "admin",
83 },
84 },
85 authed: true,
86 },
87 },
88 logger: zap.NewNop(),
89 }
90 req, repl := newRequestWithReplacer()
91 nextCalled := false
92
93 err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
94 nextCalled = true
95 return nil
96 }))
97 if err != nil {
98 t.Fatalf("expected no authentication error, got %v", err)
99 }
100 if !nextCalled {
101 t.Fatal("next handler was not called for successful authentication")
102 }
103
104 assertPlaceholder(t, repl, "http.auth.user.id", "alice")
105 assertPlaceholder(t, repl, "http.auth.user.role", "admin")
106 assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
107 assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
108 }
109
110 func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) {
111 auth := Authentication{
112 Providers: map[string]Authenticator{
113 "first": staticAuthenticator{
114 user: User{
115 ID: "rejected",
116 Metadata: map[string]string{
117 "role": "guest",
118 },
119 },
120 },
121 "second": staticAuthenticator{
122 user: User{
123 ID: "accepted",
124 Metadata: map[string]string{
125 "role": "admin",
126 },
127 },
128 authed: true,
129 },
130 },
131 logger: zap.NewNop(),
132 }
133 req, repl := newRequestWithReplacer()
134
135 err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
136 return nil
137 }))
138 if err != nil {
139 t.Fatalf("expected no authentication error, got %v", err)
140 }
141
142 assertPlaceholder(t, repl, "http.auth.user.id", "accepted")
143 assertPlaceholder(t, repl, "http.auth.user.role", "admin")
144 assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
145 assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
146 }
147
148 func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) {
149 auth := Authentication{
150 Providers: map[string]Authenticator{
151 "test": staticAuthenticator{},
152 },
153 logger: zap.NewNop(),
154 }
155 req, repl := newRequestWithReplacer()
156
157 err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
158 t.Fatal("next handler was called for rejected authentication")
159 return nil
160 }))
161 if err == nil {
162 t.Fatal("expected authentication error")
163 }
164
165 assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
166 }
167
168 func newRequestWithReplacer() (*http.Request, *caddy.Replacer) {
169 req := httptest.NewRequest(http.MethodGet, "/", nil)
170 repl := caddy.NewReplacer()
171 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
172 return req.WithContext(ctx), repl
173 }
174
175 func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) {
176 t.Helper()
177 actual, ok := repl.GetString(key)
178 if !ok {
179 t.Fatalf("expected placeholder %q to be set", key)
180 }
181 if actual != expected {
182 t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual)
183 }
184 }
185
186 func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) {
187 t.Helper()
188 if actual, ok := repl.GetString(key); ok {
189 t.Fatalf("expected placeholder %q to be absent, got %q", key, actual)
190 }
191 }
192
193 type staticAuthenticator struct {
194 user User
195 authed bool
196 err error
197 }
198
199 func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
200 return a.user, a.authed, a.err
201 }
202
203 // writingAuthenticator writes to the response during Authenticate (as a real
204 // provider might: a challenge/redirect on failure, a Set-Cookie on success).
205 type writingAuthenticator struct {
206 authed bool
207 write func(w http.ResponseWriter)
208 }
209
210 func (a writingAuthenticator) Authenticate(w http.ResponseWriter, _ *http.Request) (User, bool, error) {
211 if a.write != nil {
212 a.write(w)
213 }
214 if a.authed {
215 return User{ID: "u"}, true, nil
216 }
217 return User{}, false, nil
218 }
219
220 func serveAuth(providers map[string]Authenticator, next caddyhttp.Handler) *httptest.ResponseRecorder {
221 a := Authentication{Providers: providers, logger: zap.NewNop()}
222 req, _ := newRequestWithReplacer()
223 rr := httptest.NewRecorder()
224 _ = a.ServeHTTP(rr, req, next)
225 return rr
226 }
227
228 // A failing provider that writes a redirect must not clobber the response
229 // when another provider authenticates the request. Provider map iteration
230 // order is randomized, so exercise both orderings. #5190
231 func TestFailingProviderDoesNotClobberSuccess(t *testing.T) {
232 redirecter := writingAuthenticator{write: func(w http.ResponseWriter) {
233 w.Header().Set("Location", "/login")
234 w.WriteHeader(http.StatusFound)
235 }}
236 succeeder := writingAuthenticator{authed: true}
237
238 for i := 0; i < 20; i++ {
239 reached := false
240 rr := serveAuth(map[string]Authenticator{
241 "redirect": redirecter, "succeed": succeeder,
242 }, caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
243 reached = true
244 w.WriteHeader(http.StatusOK)
245 return nil
246 }))
247 if !reached {
248 t.Fatalf("run %d: handler chain not reached despite successful auth", i)
249 }
250 if rr.Code != http.StatusOK {
251 t.Fatalf("run %d: got status %d, want 200 (failing provider leaked its redirect)", i, rr.Code)
252 }
253 if loc := rr.Header().Get("Location"); loc != "" {
254 t.Fatalf("run %d: failing provider's Location header leaked: %q", i, loc)
255 }
256 }
257 }
258
259 // The successful provider's headers (e.g. a Set-Cookie for a new session)
260 // must reach the client even though its response is otherwise buffered. #5190
261 func TestSuccessfulProviderHeadersPreserved(t *testing.T) {
262 succeeder := writingAuthenticator{authed: true, write: func(w http.ResponseWriter) {
263 w.Header().Set("Set-Cookie", "session=abc; Path=/")
264 }}
265 rr := serveAuth(map[string]Authenticator{"succeed": succeeder},
266 caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
267 w.WriteHeader(http.StatusOK)
268 return nil
269 }))
270 if got := rr.Header().Get("Set-Cookie"); got != "session=abc; Path=/" {
271 t.Fatalf("successful provider's Set-Cookie not preserved: got %q", got)
272 }
273 if rr.Code != http.StatusOK {
274 t.Fatalf("got status %d, want 200", rr.Code)
275 }
276 }
277
278 // When every provider fails, a challenge still reaches the client and a
279 // redirect takes precedence over a plain failure. #5190
280 func TestAllFailReplaysRedirect(t *testing.T) {
281 plain := writingAuthenticator{write: func(w http.ResponseWriter) {
282 w.WriteHeader(http.StatusUnauthorized)
283 }}
284 redirecter := writingAuthenticator{write: func(w http.ResponseWriter) {
285 w.Header().Set("Location", "/login")
286 w.WriteHeader(http.StatusFound)
287 }}
288 rr := serveAuth(map[string]Authenticator{
289 "plain": plain, "redirect": redirecter,
290 }, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
291 t.Fatal("handler chain must not run when auth fails")
292 return nil
293 }))
294 if rr.Code != http.StatusFound {
295 t.Fatalf("got status %d, want 302 (redirect should win the replay)", rr.Code)
296 }
297 if loc := rr.Header().Get("Location"); loc != "/login" {
298 t.Fatalf("redirect Location not replayed: %q", loc)
299 }
300 }
301
302 // A provider that fails by setting only a challenge header (no status/body) —
303 // which is exactly what basic auth does (WWW-Authenticate) — must still result
304 // in a 401 with that header, not a 200. #5190
305 func TestHeaderOnlyChallengeStillReturns401(t *testing.T) {
306 basicish := writingAuthenticator{write: func(w http.ResponseWriter) {
307 w.Header().Set("WWW-Authenticate", `Basic realm="test"`)
308 }}
309 err := (func() error {
310 a := Authentication{Providers: map[string]Authenticator{"basic": basicish}, logger: zap.NewNop()}
311 req, _ := newRequestWithReplacer()
312 return a.ServeHTTP(&recordingStatusWriter{ResponseWriter: httptest.NewRecorder()}, req,
313 caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
314 t.Fatal("handler chain must not run when auth fails")
315 return nil
316 }))
317 })()
318 var handlerErr caddyhttp.HandlerError
319 if !errors.As(err, &handlerErr) {
320 t.Fatalf("expected a 401 HandlerError, got %v (a header-only challenge must not return nil/200)", err)
321 }
322 if handlerErr.StatusCode != http.StatusUnauthorized {
323 t.Fatalf("expected status 401, got %d", handlerErr.StatusCode)
324 }
325 }
326
327 // recordingStatusWriter records whether WriteHeader was called and with what
328 // status, to prove a header-only challenge does not emit a 200.
329 type recordingStatusWriter struct {
330 http.ResponseWriter
331 wroteHeader bool
332 status int
333 }
334
335 func (rw *recordingStatusWriter) WriteHeader(status int) {
336 rw.wroteHeader = true
337 rw.status = status
338 rw.ResponseWriter.WriteHeader(status)
339 }
340
341 // capabilityWriter implements the writer capabilities providers may rely on,
342 // recording whether each was actually invoked.
343 type capabilityWriter struct {
344 http.ResponseWriter
345 flushed bool
346 hijacked bool
347 }
348
349 func (cw *capabilityWriter) Flush() { cw.flushed = true }
350 func (cw *capabilityWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
351 cw.hijacked = true
352 return nil, nil, nil
353 }
354
355 // With a SINGLE provider there is nothing to isolate, so the provider must
356 // receive the real writer untouched — all capabilities preserved and writes
357 // streamed directly. This is the no-regression guarantee for the common case.
358 func TestSingleProviderGetsRealWriter(t *testing.T) {
359 var gotReal bool
360 prov := writingAuthenticator{authed: true, write: func(w http.ResponseWriter) {
361 _, gotReal = w.(*capabilityWriter) // the real writer, not a buffered wrapper
362 if f, ok := w.(http.Flusher); ok {
363 f.Flush()
364 }
365 }}
366 cw := &capabilityWriter{ResponseWriter: httptest.NewRecorder()}
367 a := Authentication{Providers: map[string]Authenticator{"only": prov}, logger: zap.NewNop()}
368 req, _ := newRequestWithReplacer()
369 if err := a.ServeHTTP(cw, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
370 return nil
371 })); err != nil {
372 t.Fatalf("unexpected error: %v", err)
373 }
374 if !gotReal {
375 t.Error("single provider did not receive the real ResponseWriter")
376 }
377 if !cw.flushed {
378 t.Error("single provider's Flush did not reach the real writer")
379 }
380 }
381
382 // With MULTIPLE providers the buffered writer must still satisfy every
383 // capability interface the real writer satisfies, so external providers that
384 // check for Flusher/Hijacker/Pusher/ReaderFrom do not change behavior. Both
385 // access paths are checked: a plain type assertion, which is what most
386 // existing providers do, and http.ResponseController.
387 func TestBufferedWriterPreservesCapabilities(t *testing.T) {
388 var (
389 ran bool
390 assertFlusher, assertHijacker, assertPusher, assertReaderFrom bool
391 ctrlFlush, ctrlHijack bool
392 )
393 probe := writingAuthenticator{write: func(w http.ResponseWriter) {
394 ran = true
395 flusher, ok := w.(http.Flusher)
396 assertFlusher = ok
397 if ok {
398 flusher.Flush() // must be suppressed, not forwarded
399 }
400 _, assertHijacker = w.(http.Hijacker)
401 _, assertPusher = w.(http.Pusher)
402 _, assertReaderFrom = w.(io.ReaderFrom)
403
404 ctrl := http.NewResponseController(w)
405 ctrlFlush = ctrl.Flush() == nil // FlushError suppressed → nil while buffering
406 _, _, herr := ctrl.Hijack()
407 ctrlHijack = herr == nil
408 }}
409 // Neither provider authenticates: providers are iterated from a map, so a
410 // succeeding one could break out of the loop before the probe ever runs.
411 // Two non-authenticating providers still enable isolation (len > 1) and
412 // guarantee the probe is exercised on every run.
413 other := writingAuthenticator{}
414 cw := &capabilityWriter{ResponseWriter: httptest.NewRecorder()}
415 a := Authentication{Providers: map[string]Authenticator{"probe": probe, "other": other}, logger: zap.NewNop()}
416 req, _ := newRequestWithReplacer()
417 _ = a.ServeHTTP(cw, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil }))
418
419 if !ran {
420 t.Fatal("probe provider never ran; the test asserts nothing")
421 }
422 for _, tc := range []struct {
423 name string
424 got bool
425 }{
426 {"w.(http.Flusher)", assertFlusher},
427 {"w.(http.Hijacker)", assertHijacker},
428 {"w.(http.Pusher)", assertPusher},
429 {"w.(io.ReaderFrom)", assertReaderFrom},
430 {"http.ResponseController.Flush", ctrlFlush},
431 {"http.ResponseController.Hijack", ctrlHijack},
432 } {
433 if !tc.got {
434 t.Errorf("%s failed on the buffered writer but succeeds on the real writer", tc.name)
435 }
436 }
437 // Flush during buffering must be suppressed — it must NOT reach the real writer.
438 if cw.flushed {
439 t.Error("a provider's Flush leaked to the real writer during buffering")
440 }
441 }
442
443 // A provider that writes an enormous body must not be buffered without bound.
444 func TestBufferedResponseIsSizeCapped(t *testing.T) {
445 huge := 4 * maxBufferedAuthResponse
446 flood := writingAuthenticator{write: func(w http.ResponseWriter) {
447 n, err := w.Write(make([]byte, huge))
448 if err != nil || n != huge {
449 t.Errorf("provider write short/errored: n=%d err=%v", n, err)
450 }
451 }}
452 succeeder := writingAuthenticator{authed: true}
453 bw := newBufferedResponseWriter(httptest.NewRecorder())
454 // drive the flood provider through a buffered writer directly to inspect the cap
455 flood.Authenticate(bw, httptest.NewRequest(http.MethodGet, "/", nil))
456 if bw.buf.Len() > maxBufferedAuthResponse {
457 t.Fatalf("buffered %d bytes, exceeds cap %d", bw.buf.Len(), maxBufferedAuthResponse)
458 }
459 if !bw.overflowed {
460 t.Error("expected overflow to be flagged when a provider exceeds the buffer cap")
461 }
462 _ = succeeder
463 }
464
465 // ReadFrom must honor the io.ReaderFrom contract even when the size cap is
466 // exceeded: report the total bytes consumed from the source (retained and
467 // drained alike) and propagate any error encountered while draining.
468 func TestBufferedWriterReadFromReportsTotalAndDrainError(t *testing.T) {
469 extra := int64(1234)
470 total := int64(maxBufferedAuthResponse) + extra
471 bw := newBufferedResponseWriter(httptest.NewRecorder())
472 n, err := bw.ReadFrom(bytes.NewReader(make([]byte, total)))
473 if err != nil {
474 t.Fatalf("ReadFrom() error = %v", err)
475 }
476 if n != total {
477 t.Errorf("ReadFrom() = %d, want total consumed %d", n, total)
478 }
479 if bw.buf.Len() != maxBufferedAuthResponse {
480 t.Errorf("retained %d bytes, want cap %d", bw.buf.Len(), maxBufferedAuthResponse)
481 }
482 if !bw.overflowed {
483 t.Error("expected overflow to be flagged when the source exceeds the buffer cap")
484 }
485
486 drainErr := errors.New("drain failure")
487 bw = newBufferedResponseWriter(httptest.NewRecorder())
488 n, err = bw.ReadFrom(io.MultiReader(
489 bytes.NewReader(make([]byte, maxBufferedAuthResponse)),
490 errorReader{err: drainErr},
491 ))
492 if !errors.Is(err, drainErr) {
493 t.Errorf("ReadFrom() error = %v, want the drain error %v", err, drainErr)
494 }
495 if n != int64(maxBufferedAuthResponse) {
496 t.Errorf("ReadFrom() = %d, want %d bytes consumed before the error", n, maxBufferedAuthResponse)
497 }
498 }
499
500 type errorReader struct{ err error }
501
502 func (er errorReader) Read([]byte) (int, error) { return 0, er.err }