@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 464 lines 13.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 caddyhttp
16
17 import (
18 "context"
19 "crypto/tls"
20 "crypto/x509"
21 "encoding/pem"
22 "errors"
23 "net"
24 "net/http"
25 "net/http/httptest"
26 "testing"
27
28 "github.com/caddyserver/caddy/v2"
29 )
30
31 func TestMissingCookiePlaceholder(t *testing.T) {
32 req := httptest.NewRequest(http.MethodGet, "/", nil)
33 req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
34 repl := NewTestReplacer(req)
35 const input = "before-{http.request.cookie.session}-after"
36 if got := repl.ReplaceKnown(input, ""); got != "before--after" {
37 t.Fatalf("missing cookie = %q, want %q", got, "before--after")
38 }
39 req.AddCookie(&http.Cookie{Name: "session", Value: "present"})
40 if got := repl.ReplaceKnown(input, ""); got != "before-present-after" {
41 t.Fatalf("present cookie = %q, want %q", got, "before-present-after")
42 }
43 }
44
45 func TestTLSPlaceholdersWithoutTLS(t *testing.T) {
46 req := httptest.NewRequest(http.MethodGet, "/", nil)
47 req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
48 repl := NewTestReplacer(req)
49 for _, field := range []string{
50 "version", "cipher_suite", "resumed", "proto", "proto_mutual", "server_name", "ech",
51 "client.fingerprint", "client.public_key", "client.public_key_sha256",
52 "client.issuer", "client.serial", "client.subject", "client.certificate_pem", "client.certificate_der_base64",
53 "client.san.dns_names", "client.san.emails", "client.san.ips", "client.san.uris",
54 "client.san.dns_names.0", "client.san.emails.0", "client.san.ips.0", "client.san.uris.0",
55 } {
56 t.Run(field, func(t *testing.T) {
57 key := "http.request.tls." + field
58 value, known := repl.Get(key)
59 if !known || caddy.ToString(value) != "" {
60 t.Fatalf("Get(%q) = %v, %v; want empty, known", key, value, known)
61 }
62 if got := repl.ReplaceKnown("before-{"+key+"}-after", ""); got != "before--after" {
63 t.Fatalf("replacement = %q, want %q", got, "before--after")
64 }
65 })
66 }
67 }
68
69 func TestUnknownTLSPlaceholdersRemainLiteral(t *testing.T) {
70 for _, state := range []*tls.ConnectionState{nil, {ServerName: "example.com"}} {
71 req := httptest.NewRequest(http.MethodGet, "/", nil)
72 req.TLS = state
73 req = req.WithContext(context.WithValue(req.Context(), VarsCtxKey, map[string]any{}))
74 repl := NewTestReplacer(req)
75 for _, field := range []string{
76 "unknown", "client.unknown", "client.san.unknown",
77 "client.san.dns_names_extra", "client.san.dns_names.-1", "client.san.dns_names.nope",
78 } {
79 input := "{http.request.tls." + field + "}"
80 if got := repl.ReplaceKnown(input, ""); got != input {
81 t.Errorf("replacement = %q, want literal %q", got, input)
82 }
83 }
84 }
85 }
86
87 func TestHTTPVarReplacement(t *testing.T) {
88 req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil)
89 repl := caddy.NewReplacer()
90 localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.159.1:80")
91 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
92 ctx = context.WithValue(ctx, http.LocalAddrContextKey, localAddr)
93 req = req.WithContext(ctx)
94 req.Host = "example.com:80"
95 req.RemoteAddr = "192.168.159.32:1234"
96
97 clientCert := []byte(`-----BEGIN CERTIFICATE-----
98 MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
99 eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
100 A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
101 iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
102 z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
103 fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
104 BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
105 AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
106 eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
107 3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
108 9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
109 -----END CERTIFICATE-----`)
110
111 block, _ := pem.Decode(clientCert)
112 if block == nil {
113 t.Fatalf("failed to decode PEM certificate")
114 }
115
116 cert, err := x509.ParseCertificate(block.Bytes)
117 if err != nil {
118 t.Fatalf("failed to decode PEM certificate: %v", err)
119 }
120
121 req.TLS = &tls.ConnectionState{
122 Version: tls.VersionTLS13,
123 HandshakeComplete: true,
124 ServerName: "example.com",
125 CipherSuite: tls.TLS_AES_256_GCM_SHA384,
126 PeerCertificates: []*x509.Certificate{cert},
127 NegotiatedProtocol: "h2",
128 NegotiatedProtocolIsMutual: true,
129 }
130
131 res := httptest.NewRecorder()
132 addHTTPVarsToReplacer(repl, req, res)
133
134 for i, tc := range []struct {
135 get string
136 expect string
137 }{
138 {
139 get: "http.request.scheme",
140 expect: "https",
141 },
142 {
143 get: "http.request.method",
144 expect: http.MethodGet,
145 },
146 {
147 get: "http.request.host",
148 expect: "example.com",
149 },
150 {
151 get: "http.request.port",
152 expect: "80",
153 },
154 {
155 get: "http.request.hostport",
156 expect: "example.com:80",
157 },
158 {
159 get: "http.request.local.host",
160 expect: "192.168.159.1",
161 },
162 {
163 get: "http.request.local.port",
164 expect: "80",
165 },
166 {
167 get: "http.request.local",
168 expect: "192.168.159.1:80",
169 },
170 {
171 get: "http.request.remote.host",
172 expect: "192.168.159.32",
173 },
174 {
175 get: "http.request.remote.host/24",
176 expect: "192.168.159.0/24",
177 },
178 {
179 get: "http.request.remote.host/24,32",
180 expect: "192.168.159.0/24",
181 },
182 {
183 get: "http.request.remote.host/999",
184 expect: "",
185 },
186 {
187 get: "http.request.remote.port",
188 expect: "1234",
189 },
190 {
191 get: "http.request.host.labels.0",
192 expect: "com",
193 },
194 {
195 get: "http.request.host.labels.1",
196 expect: "example",
197 },
198 {
199 get: "http.request.host.labels.2",
200 expect: "",
201 },
202 {
203 get: "http.request.uri",
204 expect: "/foo/bar.tar.gz?a=1&b=2",
205 },
206 {
207 get: "http.request.uri_escaped",
208 expect: "%2Ffoo%2Fbar.tar.gz%3Fa%3D1%26b%3D2",
209 },
210 {
211 get: "http.request.uri.path",
212 expect: "/foo/bar.tar.gz",
213 },
214 {
215 get: "http.request.uri.path_escaped",
216 expect: "%2Ffoo%2Fbar.tar.gz",
217 },
218 {
219 get: "http.request.uri.path.file",
220 expect: "bar.tar.gz",
221 },
222 {
223 get: "http.request.uri.path.file.base",
224 expect: "bar.tar",
225 },
226 {
227 // not ideal, but also most correct, given that files can have dots (example: index.<SHA>.html) TODO: maybe this isn't right..
228 get: "http.request.uri.path.file.ext",
229 expect: ".gz",
230 },
231 {
232 get: "http.request.uri.query",
233 expect: "a=1&b=2",
234 },
235 {
236 get: "http.request.uri.query_escaped",
237 expect: "a%3D1%26b%3D2",
238 },
239 {
240 get: "http.request.uri.query.a",
241 expect: "1",
242 },
243 {
244 get: "http.request.uri.query.b",
245 expect: "2",
246 },
247 {
248 get: "http.request.uri.prefixed_query",
249 expect: "?a=1&b=2",
250 },
251 {
252 get: "http.request.tls.cipher_suite",
253 expect: "TLS_AES_256_GCM_SHA384",
254 },
255 {
256 get: "http.request.tls.proto",
257 expect: "h2",
258 },
259 {
260 get: "http.request.tls.proto_mutual",
261 expect: "true",
262 },
263 {
264 get: "http.request.tls.resumed",
265 expect: "false",
266 },
267 {
268 get: "http.request.tls.server_name",
269 expect: "example.com",
270 },
271 {
272 get: "http.request.tls.version",
273 expect: "tls1.3",
274 },
275 {
276 get: "http.request.tls.client.fingerprint",
277 expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702",
278 },
279 {
280 get: "http.request.tls.client.issuer",
281 expect: "CN=Caddy Test CA",
282 },
283 {
284 get: "http.request.tls.client.serial",
285 expect: "2",
286 },
287 {
288 get: "http.request.tls.client.subject",
289 expect: "CN=client.localdomain",
290 },
291 {
292 get: "http.request.tls.client.san.dns_names",
293 expect: "[localhost]",
294 },
295 {
296 get: "http.request.tls.client.san.dns_names.0",
297 expect: "localhost",
298 },
299 {
300 get: "http.request.tls.client.san.dns_names.1",
301 expect: "",
302 },
303 {
304 get: "http.request.tls.client.san.ips",
305 expect: "[127.0.0.1]",
306 },
307 {
308 get: "http.request.tls.client.san.ips.0",
309 expect: "127.0.0.1",
310 },
311 {
312 get: "http.request.tls.client.certificate_pem",
313 expect: string(clientCert) + "\n", // returned value comes with a newline appended to it
314 },
315 } {
316 actual, got := repl.GetString(tc.get)
317 if !got {
318 t.Errorf("Test %d: Expected to recognize the placeholder name, but didn't", i)
319 }
320 if actual != tc.expect {
321 t.Errorf("Test %d: Expected %s to be '%s' but got '%s'",
322 i, tc.get, tc.expect, actual)
323 }
324 }
325 }
326
327 func TestHTTPProtoNameNormalization(t *testing.T) {
328 for _, tc := range []struct {
329 proto string
330 major int
331 expectRaw string
332 expectName string
333 }{
334 {proto: "HTTP/1.0", major: 1, expectRaw: "HTTP/1.0", expectName: "HTTP/1.0"},
335 {proto: "HTTP/1.1", major: 1, expectRaw: "HTTP/1.1", expectName: "HTTP/1.1"},
336 {proto: "HTTP/2.0", major: 2, expectRaw: "HTTP/2.0", expectName: "HTTP/2"},
337 {proto: "HTTP/3.0", major: 3, expectRaw: "HTTP/3.0", expectName: "HTTP/3"},
338 } {
339 req := httptest.NewRequest(http.MethodGet, "/", nil)
340 req.Proto = tc.proto
341 req.ProtoMajor = tc.major
342 repl := caddy.NewReplacer()
343 addHTTPVarsToReplacer(repl, req, nil)
344
345 gotRaw, okRaw := repl.GetString("http.request.proto")
346 if !okRaw || gotRaw != tc.expectRaw {
347 t.Errorf("proto=%s: expected http.request.proto to be %q, got %q (ok=%t)", tc.proto, tc.expectRaw, gotRaw, okRaw)
348 }
349
350 gotName, okName := repl.GetString("http.request.proto_name")
351 if !okName || gotName != tc.expectName {
352 t.Errorf("proto=%s: expected http.request.proto_name to be %q, got %q (ok=%t)", tc.proto, tc.expectName, gotName, okName)
353 }
354 }
355 }
356
357 // BenchmarkAddHTTPVarsToReplacer measures the per-request replacer setup, which
358 // is the common path where the request UUID is never referenced.
359 func BenchmarkAddHTTPVarsToReplacer(b *testing.B) {
360 req := httptest.NewRequest(http.MethodGet, "http://example.com/foo?a=b", nil)
361 req.Header.Set("User-Agent", "test-agent")
362 ctx := context.WithValue(req.Context(), VarsCtxKey, make(map[string]any))
363 ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
364 req = req.WithContext(ctx)
365
366 b.ReportAllocs()
367 for b.Loop() {
368 repl := caddy.NewReplacer()
369 addHTTPVarsToReplacer(repl, req, nil)
370 }
371 }
372
373 // TestHTTPVarReplacementUUID verifies the lazily-allocated request UUID is
374 // generated on first access and stays stable across references.
375 func TestHTTPVarReplacementUUID(t *testing.T) {
376 req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
377 repl := caddy.NewReplacer()
378 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
379 ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
380 ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
381 req = req.WithContext(ctx)
382 addHTTPVarsToReplacer(repl, req, nil)
383
384 first, ok := repl.GetString("http.request.uuid")
385 if !ok || first == "" {
386 t.Fatalf("expected a non-empty uuid, got %q (ok=%t)", first, ok)
387 }
388
389 second, _ := repl.GetString("http.request.uuid")
390 if first != second {
391 t.Errorf("expected stable uuid across references: %q != %q", first, second)
392 }
393 }
394
395 // failingBodyReader serves some bytes and then returns a fixed error, to
396 // exercise the read failure path of the request body placeholders.
397 type failingBodyReader struct {
398 data []byte
399 pos int
400 err error
401 }
402
403 func (b *failingBodyReader) Read(p []byte) (int, error) {
404 if b.pos < len(b.data) {
405 n := copy(p, b.data[b.pos:])
406 b.pos += n
407 return n, nil
408 }
409 return 0, b.err
410 }
411
412 func (b *failingBodyReader) Close() error { return nil }
413
414 // TestRequestBodyPlaceholderErrorScoping verifies that only a max_size-driven
415 // handler error from the request body read becomes a RequestBodyLimitError,
416 // while every other error keeps the placeholder's old ignore-and-truncate
417 // behavior.
418 func TestRequestBodyPlaceholderErrorScoping(t *testing.T) {
419 maxBytes := &http.MaxBytesError{Limit: 10}
420 for _, tc := range []struct {
421 name string
422 readErr error
423 wantMarker bool
424 }{
425 {name: "max_size handler error becomes the marker", readErr: HandlerError{Err: maxBytes, StatusCode: http.StatusRequestEntityTooLarge}, wantMarker: true},
426 {name: "unrelated handler error keeps old behavior", readErr: HandlerError{Err: errors.New("boom"), StatusCode: http.StatusInternalServerError}},
427 {name: "raw max bytes error keeps old behavior", readErr: maxBytes},
428 } {
429 t.Run(tc.name, func(t *testing.T) {
430 req := httptest.NewRequest(http.MethodPost, "/", nil)
431 req.Body = &failingBodyReader{data: []byte("abc"), err: tc.readErr}
432
433 body, err := readRequestBodyForPlaceholder(req)
434
435 if !tc.wantMarker {
436 if err != nil {
437 t.Fatalf("unrelated errors must be ignored like before but got %v", err)
438 }
439 if string(body) != "abc" {
440 t.Errorf("expected the truncated prefix %q but got %q", "abc", body)
441 }
442 return
443 }
444 if err == nil {
445 t.Fatal("expected the body limit marker but got none")
446 }
447 var marker RequestBodyLimitError
448 if !errors.As(err, &marker) {
449 t.Fatalf("expected RequestBodyLimitError but got %T", err)
450 }
451 var handlerErr HandlerError
452 if errors.As(err, &handlerErr) {
453 t.Error("the marker must not be detectable as a HandlerError")
454 }
455 if marker.StatusCode() != http.StatusRequestEntityTooLarge {
456 t.Errorf("expected status 413 but got %d", marker.StatusCode())
457 }
458 var maxErr *http.MaxBytesError
459 if !errors.As(err, &maxErr) {
460 t.Error("the marker must unwrap to the MaxBytesError")
461 }
462 })
463 }
464 }