@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 297 lines 7.33 KB
Raw
1 package caddyhttp
2
3 import (
4 "context"
5 "crypto/tls"
6 "net/http"
7 "net/url"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/caddyserver/caddy/v2"
14 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
15 )
16
17 func TestURLPatternMatcher(t *testing.T) {
18 for _, tc := range []struct {
19 name string
20 match MatchURLPattern
21 host string
22 tls bool
23 input string
24 expect bool
25 provisionErr bool
26 }{
27 {
28 name: "literal path matches",
29 match: MatchURLPattern{Pattern: "/foo"},
30 host: "example.com",
31 input: "/foo",
32 expect: true,
33 },
34 {
35 name: "literal path mismatch",
36 match: MatchURLPattern{Pattern: "/foo"},
37 host: "example.com",
38 input: "/bar",
39 expect: false,
40 },
41 {
42 name: "named group matches",
43 match: MatchURLPattern{Pattern: "/books/:id"},
44 host: "example.com",
45 input: "/books/123",
46 expect: true,
47 },
48 {
49 name: "named group requires segment",
50 match: MatchURLPattern{Pattern: "/books/:id"},
51 host: "example.com",
52 input: "/books",
53 expect: false,
54 },
55 {
56 name: "wildcard spans segments",
57 match: MatchURLPattern{Pattern: "/files/*"},
58 host: "example.com",
59 input: "/files/a/b/c",
60 expect: true,
61 },
62 {
63 name: "absolute pattern matches host and scheme",
64 match: MatchURLPattern{Pattern: "https://example.com/foo"},
65 host: "example.com",
66 tls: true,
67 input: "/foo",
68 expect: true,
69 },
70 {
71 name: "absolute pattern rejects scheme mismatch",
72 match: MatchURLPattern{Pattern: "https://example.com/foo"},
73 host: "example.com",
74 input: "/foo",
75 expect: false,
76 },
77 {
78 name: "absolute pattern rejects host mismatch",
79 match: MatchURLPattern{Pattern: "https://example.com/foo"},
80 host: "other.com",
81 tls: true,
82 input: "/foo",
83 expect: false,
84 },
85 {
86 name: "ignore_case matches mixed case",
87 match: MatchURLPattern{Pattern: "/foo", IgnoreCase: true},
88 host: "example.com",
89 input: "/FOO",
90 expect: true,
91 },
92 {
93 name: "case sensitive by default",
94 match: MatchURLPattern{Pattern: "/foo"},
95 host: "example.com",
96 input: "/FOO",
97 expect: false,
98 },
99 {
100 name: "base_url scopes to host",
101 match: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com"},
102 host: "example.com",
103 tls: true,
104 input: "/search?q=caddy",
105 expect: true,
106 },
107 {
108 name: "base_url rejects other host",
109 match: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com"},
110 host: "other.com",
111 tls: true,
112 input: "/search",
113 expect: false,
114 },
115 {
116 name: "encoded slash traversal is rejected",
117 match: MatchURLPattern{Pattern: "/public/*"},
118 host: "example.com",
119 input: "/public/..%2fadmin/secret.txt",
120 expect: false,
121 },
122 {
123 name: "uppercase encoded slash traversal is rejected",
124 match: MatchURLPattern{Pattern: "/public/*"},
125 host: "example.com",
126 input: "/public/..%2Fadmin/secret.txt",
127 expect: false,
128 },
129 {
130 name: "encoded dot traversal is rejected",
131 match: MatchURLPattern{Pattern: "/public/*"},
132 host: "example.com",
133 input: "/public/%2e%2e/admin/secret.txt",
134 expect: false,
135 },
136 {
137 name: "raw dot traversal is rejected",
138 match: MatchURLPattern{Pattern: "/public/*"},
139 host: "example.com",
140 input: "/public/../admin/secret.txt",
141 expect: false,
142 },
143 {
144 name: "cleaned traversal still matches its real target",
145 match: MatchURLPattern{Pattern: "/admin/*"},
146 host: "example.com",
147 input: "/public/..%2fadmin/secret.txt",
148 expect: true,
149 },
150 {
151 name: "doubled slashes are collapsed before matching",
152 match: MatchURLPattern{Pattern: "/public/*"},
153 host: "example.com",
154 input: "/public//nested//file",
155 expect: true,
156 },
157 {
158 name: "pattern with empty segment preserves request slashes",
159 match: MatchURLPattern{Pattern: "/a//b"},
160 host: "example.com",
161 input: "/a//b",
162 expect: true,
163 },
164 {
165 name: "encoded characters match their decoded form",
166 match: MatchURLPattern{Pattern: "/caf%C3%A9"},
167 host: "example.com",
168 input: "/caf%C3%A9",
169 expect: true,
170 },
171 {
172 name: "invalid pattern fails provisioning",
173 match: MatchURLPattern{Pattern: "https://[invalid"},
174 provisionErr: true,
175 },
176 } {
177 t.Run(tc.name, func(t *testing.T) {
178 err := tc.match.Provision(caddy.Context{})
179 if tc.provisionErr {
180 require.Error(t, err)
181 return
182 }
183 require.NoError(t, err)
184
185 u, err := url.ParseRequestURI(tc.input)
186 require.NoError(t, err)
187
188 req := &http.Request{URL: u, Host: tc.host}
189 if tc.tls {
190 req.TLS = &tls.ConnectionState{}
191 }
192
193 actual, err := tc.match.MatchWithError(req)
194 require.NoError(t, err)
195 assert.Equal(t, tc.expect, actual)
196 })
197 }
198 }
199
200 func TestURLPatternMatcherUnmarshalCaddyfile(t *testing.T) {
201 for _, tc := range []struct {
202 name string
203 input string
204 expect MatchURLPattern
205 expectErr bool
206 }{
207 {
208 name: "pattern only",
209 input: `url_pattern /books/:id`,
210 expect: MatchURLPattern{Pattern: "/books/:id"},
211 },
212 {
213 name: "base_url and ignore_case",
214 input: `url_pattern /search {
215 base_url https://example.com
216 ignore_case
217 }`,
218 expect: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com", IgnoreCase: true},
219 },
220 {
221 name: "missing pattern",
222 input: `url_pattern`,
223 expectErr: true,
224 },
225 {
226 name: "unknown option",
227 input: `url_pattern /foo {
228 nope
229 }`,
230 expectErr: true,
231 },
232 {
233 name: "base_url without value",
234 input: `url_pattern /foo {
235 base_url
236 }`,
237 expectErr: true,
238 },
239 {
240 name: "ignore_case with stray arg",
241 input: `url_pattern /foo {
242 ignore_case yes
243 }`,
244 expectErr: true,
245 },
246 } {
247 t.Run(tc.name, func(t *testing.T) {
248 var m MatchURLPattern
249 err := m.UnmarshalCaddyfile(caddyfile.NewTestDispenser(tc.input))
250 if tc.expectErr {
251 require.Error(t, err)
252 return
253 }
254 require.NoError(t, err)
255 assert.Equal(t, tc.expect, m)
256 })
257 }
258 }
259
260 // TestURLPatternMatcherGroups checks that captured groups are exposed as
261 // component-scoped placeholders, mirroring the URLPattern result object.
262 func TestURLPatternMatcherGroups(t *testing.T) {
263 m := MatchURLPattern{Pattern: "/books/:id/chapters/:chapter"}
264 require.NoError(t, m.Provision(caddy.Context{}))
265
266 u, err := url.ParseRequestURI("/books/42/chapters/7")
267 require.NoError(t, err)
268
269 repl := caddy.NewReplacer()
270 ctx := context.WithValue(context.Background(), caddy.ReplacerCtxKey, repl)
271 req := (&http.Request{URL: u, Host: "example.com"}).WithContext(ctx)
272
273 ok, err := m.MatchWithError(req)
274 require.NoError(t, err)
275 require.True(t, ok)
276
277 id, _ := repl.GetString("http.url_pattern.pathname.id")
278 assert.Equal(t, "42", id)
279 chapter, _ := repl.GetString("http.url_pattern.pathname.chapter")
280 assert.Equal(t, "7", chapter)
281 }
282
283 // TestURLPatternMatcherRelative checks that a relative pattern matches the
284 // request path regardless of the request's host.
285 func TestURLPatternMatcherRelative(t *testing.T) {
286 m := MatchURLPattern{Pattern: "/books/:id"}
287 require.NoError(t, m.Provision(caddy.Context{}))
288
289 for _, host := range []string{"example.com", "other.org", "192.0.2.1:8080"} {
290 u, err := url.ParseRequestURI("/books/42")
291 require.NoError(t, err)
292
293 ok, err := m.MatchWithError(&http.Request{URL: u, Host: host})
294 require.NoError(t, err)
295 assert.Truef(t, ok, "expected match on host %q", host)
296 }
297 }