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
| 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 | "encoding/json" |
| 19 | "errors" |
| 20 | "fmt" |
| 21 | "net" |
| 22 | "net/http" |
| 23 | "net/textproto" |
| 24 | "net/url" |
| 25 | "path" |
| 26 | "regexp" |
| 27 | "runtime" |
| 28 | "slices" |
| 29 | "sort" |
| 30 | "strconv" |
| 31 | "strings" |
| 32 | "unicode/utf8" |
| 33 | |
| 34 | "cel.dev/cel-go/cel" |
| 35 | "cel.dev/cel-go/common" |
| 36 | "cel.dev/cel-go/common/ast" |
| 37 | "cel.dev/cel-go/common/types" |
| 38 | "cel.dev/cel-go/common/types/ref" |
| 39 | "cel.dev/cel-go/parser" |
| 40 | "golang.org/x/net/idna" |
| 41 | |
| 42 | "github.com/caddyserver/caddy/v2" |
| 43 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
| 44 | ) |
| 45 | |
| 46 | type ( |
| 47 | // MatchHost matches requests by the Host value (case-insensitive). |
| 48 | // |
| 49 | // When used in a top-level HTTP route, |
| 50 | // [qualifying domain names](/docs/automatic-https#hostname-requirements) |
| 51 | // may trigger [automatic HTTPS](/docs/automatic-https), which automatically |
| 52 | // provisions and renews certificates for you. Before doing this, you |
| 53 | // should ensure that DNS records for these domains are properly configured, |
| 54 | // especially A/AAAA pointed at your server. |
| 55 | // |
| 56 | // Automatic HTTPS can be |
| 57 | // [customized or disabled](/docs/modules/http#servers/automatic_https). |
| 58 | // |
| 59 | // Wildcards (`*`) may be used to represent exactly one label of the |
| 60 | // hostname, in accordance with RFC 1034 (because host matchers are also |
| 61 | // used for automatic HTTPS which influences TLS certificates). Thus, |
| 62 | // a host of `*` matches hosts like `localhost` or `internal` but not |
| 63 | // `example.com`. To catch all hosts, omit the host matcher entirely. |
| 64 | // |
| 65 | // The wildcard can be useful for matching all subdomains, for example: |
| 66 | // `*.example.com` matches `foo.example.com` but not `foo.bar.example.com`. |
| 67 | // |
| 68 | // Duplicate entries will return an error. |
| 69 | MatchHost []string |
| 70 | |
| 71 | // MatchPath case-insensitively matches requests by the URI's path. Path |
| 72 | // matching is exact, not prefix-based, giving you more control and clarity |
| 73 | // over matching. Wildcards (`*`) may be used: |
| 74 | // |
| 75 | // - At the end only, for a prefix match (`/prefix/*`) |
| 76 | // - At the beginning only, for a suffix match (`*.suffix`) |
| 77 | // - On both sides only, for a substring match (`*/contains/*`) |
| 78 | // - In the middle, for a globular match (`/accounts/*/info`) |
| 79 | // |
| 80 | // Slashes are significant; i.e. `/foo*` matches `/foo`, `/foo/`, `/foo/bar`, |
| 81 | // and `/foobar`; but `/foo/*` does not match `/foo` or `/foobar`. Valid |
| 82 | // paths start with a slash `/`. |
| 83 | // |
| 84 | // Because there are, in general, multiple possible escaped forms of any |
| 85 | // path, path matchers operate in unescaped space; that is, path matchers |
| 86 | // should be written in their unescaped form to prevent ambiguities and |
| 87 | // possible security issues, as all request paths will be normalized to |
| 88 | // their unescaped forms before matcher evaluation. |
| 89 | // |
| 90 | // However, escape sequences in a match pattern are supported; they are |
| 91 | // compared with the request's raw/escaped path for those bytes only. |
| 92 | // In other words, a matcher of `/foo%2Fbar` will match a request path |
| 93 | // of precisely `/foo%2Fbar`, but not `/foo/bar`. It follows that matching |
| 94 | // the literal percent sign (%) in normalized space can be done using the |
| 95 | // escaped form, `%25`. |
| 96 | // |
| 97 | // Even though wildcards (`*`) operate in the normalized space, the special |
| 98 | // escaped wildcard (`%*`), which is not a valid escape sequence, may be |
| 99 | // used in place of a span that should NOT be decoded; that is, `/bands/%*/` |
| 100 | // will match `/bands/AC%2fDC/` whereas `/bands/*/` will not. Note that a |
| 101 | // pattern ending in a bare trailing wildcard, such as `/bands/*`, is a |
| 102 | // prefix match in normalized space, so it does match `/bands/AC%2fDC`. |
| 103 | // |
| 104 | // This matcher is fast, so it does not support regular expressions or |
| 105 | // capture groups. For slower but more powerful matching, use the |
| 106 | // path_regexp matcher. (Note that due to the special treatment of |
| 107 | // escape sequences in matcher patterns, they may perform slightly slower |
| 108 | // in high-traffic environments.) |
| 109 | MatchPath []string |
| 110 | |
| 111 | // MatchPathRE matches requests by a regular expression on the URI's path. |
| 112 | // Path matching is performed in the unescaped (decoded) form of the path. |
| 113 | // |
| 114 | // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` |
| 115 | // where `name` is the regular expression's name, and `capture_group` is either |
| 116 | // the named or positional capture group from the expression itself. If no name |
| 117 | // is given, then the placeholder omits the name: `{http.regexp.capture_group}` |
| 118 | // (potentially leading to collisions). |
| 119 | MatchPathRE struct{ MatchRegexp } |
| 120 | |
| 121 | // MatchMethod matches requests by the method. |
| 122 | MatchMethod []string |
| 123 | |
| 124 | // MatchQuery matches requests by the URI's query string. It takes a JSON object |
| 125 | // keyed by the query keys, with an array of string values to match for that key. |
| 126 | // Query key matches are exact, but wildcards may be used for value matches. Both |
| 127 | // keys and values may be placeholders. |
| 128 | // |
| 129 | // An example of the structure to match `?key=value&topic=api&query=something` is: |
| 130 | // |
| 131 | // ```json |
| 132 | // { |
| 133 | // "key": ["value"], |
| 134 | // "topic": ["api"], |
| 135 | // "query": ["*"] |
| 136 | // } |
| 137 | // ``` |
| 138 | // |
| 139 | // Invalid query strings, including those with bad escapings or illegal characters |
| 140 | // like semicolons, will fail to parse and thus fail to match. |
| 141 | // |
| 142 | // **NOTE:** Notice that query string values are arrays, not singular values. This is |
| 143 | // because repeated keys are valid in query strings, and each one may have a |
| 144 | // different value. This matcher will match for a key if any one of its configured |
| 145 | // values is assigned in the query string. Backend applications relying on query |
| 146 | // strings MUST take into consideration that query string values are arrays and can |
| 147 | // have multiple values. |
| 148 | MatchQuery url.Values |
| 149 | |
| 150 | // MatchHeader matches requests by header fields. The key is the field |
| 151 | // name and the array is the list of field values. It performs fast, |
| 152 | // exact string comparisons of the field values. Fast prefix, suffix, |
| 153 | // and substring matches can also be done by suffixing, prefixing, or |
| 154 | // surrounding the value with the wildcard `*` character, respectively. |
| 155 | // If a list is null, the header must not exist. If the list is empty, |
| 156 | // the field must simply exist, regardless of its value. |
| 157 | // |
| 158 | // **NOTE:** Notice that header values are arrays, not singular values. This is |
| 159 | // because repeated fields are valid in headers, and each one may have a |
| 160 | // different value. This matcher will match for a field if any one of its configured |
| 161 | // values matches in the header. Backend applications relying on headers MUST take |
| 162 | // into consideration that header field values are arrays and can have multiple |
| 163 | // values. |
| 164 | MatchHeader http.Header |
| 165 | |
| 166 | // MatchHeaderRE matches requests by a regular expression on header fields. |
| 167 | // |
| 168 | // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` |
| 169 | // where `name` is the regular expression's name, and `capture_group` is either |
| 170 | // the named or positional capture group from the expression itself. If no name |
| 171 | // is given, then the placeholder omits the name: `{http.regexp.capture_group}` |
| 172 | // (potentially leading to collisions). |
| 173 | MatchHeaderRE map[string]*MatchRegexp |
| 174 | |
| 175 | // MatchProtocol matches requests by protocol. Recognized values are |
| 176 | // "http", "https", and "grpc" for broad protocol matches, or specific |
| 177 | // HTTP versions can be specified like so: "http/1", "http/1.1", |
| 178 | // "http/2", "http/3", or minimum versions: "http/2+", etc. |
| 179 | MatchProtocol string |
| 180 | |
| 181 | // MatchTLS matches HTTP requests based on the underlying |
| 182 | // TLS connection state. If this matcher is specified but |
| 183 | // the request did not come over TLS, it will never match. |
| 184 | // If this matcher is specified but is empty and the request |
| 185 | // did come in over TLS, it will always match. |
| 186 | MatchTLS struct { |
| 187 | // Matches if the TLS handshake has completed. QUIC 0-RTT early |
| 188 | // data may arrive before the handshake completes. Generally, it |
| 189 | // is unsafe to replay these requests if they are not idempotent; |
| 190 | // additionally, the remote IP of early data packets can more |
| 191 | // easily be spoofed. It is conventional to respond with HTTP 425 |
| 192 | // Too Early if the request cannot risk being processed in this |
| 193 | // state. |
| 194 | HandshakeComplete *bool `json:"handshake_complete,omitempty"` |
| 195 | } |
| 196 | |
| 197 | // MatchNot matches requests by negating the results of its matcher |
| 198 | // sets. A single "not" matcher takes one or more matcher sets. Each |
| 199 | // matcher set is OR'ed; in other words, if any matcher set returns |
| 200 | // true, the final result of the "not" matcher is false. Individual |
| 201 | // matchers within a set work the same (i.e. different matchers in |
| 202 | // the same set are AND'ed). |
| 203 | // |
| 204 | // NOTE: The generated docs which describe the structure of this |
| 205 | // module are wrong because of how this type unmarshals JSON in a |
| 206 | // custom way. The correct structure is: |
| 207 | // |
| 208 | // ```json |
| 209 | // [ |
| 210 | // {}, |
| 211 | // {} |
| 212 | // ] |
| 213 | // ``` |
| 214 | // |
| 215 | // where each of the array elements is a matcher set, i.e. an |
| 216 | // object keyed by matcher name. |
| 217 | MatchNot struct { |
| 218 | MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` |
| 219 | MatcherSets []MatcherSet `json:"-"` |
| 220 | } |
| 221 | ) |
| 222 | |
| 223 | func init() { |
| 224 | caddy.RegisterModule(MatchHost{}) |
| 225 | caddy.RegisterModule(MatchPath{}) |
| 226 | caddy.RegisterModule(MatchPathRE{}) |
| 227 | caddy.RegisterModule(MatchMethod{}) |
| 228 | caddy.RegisterModule(MatchQuery{}) |
| 229 | caddy.RegisterModule(MatchHeader{}) |
| 230 | caddy.RegisterModule(MatchHeaderRE{}) |
| 231 | caddy.RegisterModule(new(MatchProtocol)) |
| 232 | caddy.RegisterModule(MatchTLS{}) |
| 233 | caddy.RegisterModule(MatchNot{}) |
| 234 | } |
| 235 | |
| 236 | // CaddyModule returns the Caddy module information. |
| 237 | func (MatchHost) CaddyModule() caddy.ModuleInfo { |
| 238 | return caddy.ModuleInfo{ |
| 239 | ID: "http.matchers.host", |
| 240 | New: func() caddy.Module { return new(MatchHost) }, |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 245 | func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 246 | // iterate to merge multiple matchers into one |
| 247 | for d.Next() { |
| 248 | *m = append(*m, d.RemainingArgs()...) |
| 249 | if d.NextBlock(0) { |
| 250 | return d.Err("malformed host matcher: blocks are not supported") |
| 251 | } |
| 252 | } |
| 253 | return nil |
| 254 | } |
| 255 | |
| 256 | // Provision sets up and validates m, including making it more efficient for large lists. |
| 257 | func (m MatchHost) Provision(_ caddy.Context) error { |
| 258 | // check for duplicates; they are nonsensical and reduce efficiency |
| 259 | // (we could just remove them, but the user should know their config is erroneous) |
| 260 | seen := make(map[string]int, len(m)) |
| 261 | for i, host := range m { |
| 262 | asciiHost, err := idna.ToASCII(host) |
| 263 | if err != nil { |
| 264 | return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err) |
| 265 | } |
| 266 | normalizedHost := strings.ToLower(asciiHost) |
| 267 | if firstI, ok := seen[normalizedHost]; ok { |
| 268 | return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host) |
| 269 | } |
| 270 | // Normalize exact hosts for standardized comparison in large-list fastpath later on. |
| 271 | // Keep wildcards/placeholders untouched. |
| 272 | if m.fuzzy(asciiHost) { |
| 273 | m[i] = asciiHost |
| 274 | } else { |
| 275 | m[i] = normalizedHost |
| 276 | } |
| 277 | seen[normalizedHost] = i |
| 278 | } |
| 279 | |
| 280 | if m.large() { |
| 281 | // sort the slice lexicographically, grouping "fuzzy" entries (wildcards and placeholders) |
| 282 | // at the front of the list; this allows us to use binary search for exact matches, which |
| 283 | // we have seen from experience is the most common kind of value in large lists; and any |
| 284 | // other kinds of values (wildcards and placeholders) are grouped in front so the linear |
| 285 | // search should find a match fairly quickly |
| 286 | sort.Slice(m, func(i, j int) bool { |
| 287 | iInexact, jInexact := m.fuzzy(m[i]), m.fuzzy(m[j]) |
| 288 | if iInexact && !jInexact { |
| 289 | return true |
| 290 | } |
| 291 | if !iInexact && jInexact { |
| 292 | return false |
| 293 | } |
| 294 | return m[i] < m[j] |
| 295 | }) |
| 296 | } |
| 297 | |
| 298 | return nil |
| 299 | } |
| 300 | |
| 301 | // Match returns true if r matches m. |
| 302 | func (m MatchHost) Match(r *http.Request) bool { |
| 303 | match, _ := m.MatchWithError(r) |
| 304 | return match |
| 305 | } |
| 306 | |
| 307 | // MatchWithError returns true if r matches m. |
| 308 | func (m MatchHost) MatchWithError(r *http.Request) (bool, error) { |
| 309 | reqHost, _, err := net.SplitHostPort(r.Host) |
| 310 | if err != nil { |
| 311 | // OK; probably didn't have a port |
| 312 | reqHost = r.Host |
| 313 | |
| 314 | // make sure we strip the brackets from IPv6 addresses |
| 315 | reqHost = strings.TrimPrefix(reqHost, "[") |
| 316 | reqHost = strings.TrimSuffix(reqHost, "]") |
| 317 | } |
| 318 | |
| 319 | if m.large() { |
| 320 | reqHostLower := strings.ToLower(reqHost) |
| 321 | // fast path: locate exact match using binary search (about 100-1000x faster for large lists) |
| 322 | pos := sort.Search(len(m), func(i int) bool { |
| 323 | if m.fuzzy(m[i]) { |
| 324 | return false |
| 325 | } |
| 326 | return m[i] >= reqHostLower |
| 327 | }) |
| 328 | if pos < len(m) && m[pos] == reqHostLower { |
| 329 | return true, nil |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 334 | |
| 335 | outer: |
| 336 | for _, host := range m { |
| 337 | // fast path: if matcher is large, we already know we don't have an exact |
| 338 | // match, so we're only looking for fuzzy match now, which should be at the |
| 339 | // front of the list; if we have reached a value that is not fuzzy, there |
| 340 | // will be no match and we can short-circuit for efficiency |
| 341 | if m.large() && !m.fuzzy(host) { |
| 342 | break |
| 343 | } |
| 344 | |
| 345 | host = repl.ReplaceAll(host, "") |
| 346 | if strings.Contains(host, "*") { |
| 347 | patternParts := strings.Split(host, ".") |
| 348 | incomingParts := strings.Split(reqHost, ".") |
| 349 | if len(patternParts) != len(incomingParts) { |
| 350 | continue |
| 351 | } |
| 352 | for i := range patternParts { |
| 353 | if patternParts[i] == "*" { |
| 354 | continue |
| 355 | } |
| 356 | if !strings.EqualFold(patternParts[i], incomingParts[i]) { |
| 357 | continue outer |
| 358 | } |
| 359 | } |
| 360 | return true, nil |
| 361 | } else if strings.EqualFold(reqHost, host) { |
| 362 | return true, nil |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | return false, nil |
| 367 | } |
| 368 | |
| 369 | // CELLibrary produces options that expose this matcher for use in CEL |
| 370 | // expression matchers. |
| 371 | // |
| 372 | // Example: |
| 373 | // |
| 374 | // expression host('localhost') |
| 375 | func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { |
| 376 | return CELMatcherImpl( |
| 377 | "host", |
| 378 | "host_match_request_list", |
| 379 | []*cel.Type{cel.ListType(cel.StringType)}, |
| 380 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 381 | refStringList := stringSliceType |
| 382 | strList, err := data.ConvertToNative(refStringList) |
| 383 | if err != nil { |
| 384 | return nil, err |
| 385 | } |
| 386 | matcher := MatchHost(strList.([]string)) |
| 387 | err = matcher.Provision(ctx) |
| 388 | return matcher, err |
| 389 | }, |
| 390 | ) |
| 391 | } |
| 392 | |
| 393 | // fuzzy returns true if the given hostname h is not a specific |
| 394 | // hostname, e.g. has placeholders or wildcards. |
| 395 | func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") } |
| 396 | |
| 397 | // large returns true if m is considered to be large. Optimizing |
| 398 | // the matcher for smaller lists has diminishing returns. |
| 399 | // See related benchmark function in test file to conduct experiments. |
| 400 | func (m MatchHost) large() bool { return len(m) > 100 } |
| 401 | |
| 402 | // CaddyModule returns the Caddy module information. |
| 403 | func (MatchPath) CaddyModule() caddy.ModuleInfo { |
| 404 | return caddy.ModuleInfo{ |
| 405 | ID: "http.matchers.path", |
| 406 | New: func() caddy.Module { return new(MatchPath) }, |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | // Provision lower-cases the static paths in m to ensure case-insensitive |
| 411 | // matching. Paths with placeholders are left untouched, since placeholder |
| 412 | // names can be case-sensitive (e.g. environment variables); they are |
| 413 | // lower-cased after replacement, at request time. |
| 414 | func (m MatchPath) Provision(_ caddy.Context) error { |
| 415 | for i := range m { |
| 416 | if m[i] == "*" && i > 0 { |
| 417 | // will always match, so just put it first |
| 418 | m[0] = m[i] |
| 419 | break |
| 420 | } |
| 421 | if !strings.Contains(m[i], "{") { |
| 422 | m[i] = strings.ToLower(m[i]) |
| 423 | } |
| 424 | } |
| 425 | return nil |
| 426 | } |
| 427 | |
| 428 | // Match returns true if r matches m. |
| 429 | func (m MatchPath) Match(r *http.Request) bool { |
| 430 | match, _ := m.MatchWithError(r) |
| 431 | return match |
| 432 | } |
| 433 | |
| 434 | // MatchWithError returns true if r matches m. |
| 435 | func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { |
| 436 | // Even though RFC 9110 says that path matching is case-sensitive |
| 437 | // (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3), |
| 438 | // we do case-insensitive matching to mitigate security issues |
| 439 | // related to differences between operating systems, applications, |
| 440 | // etc; if case-sensitive matching is needed, the regex matcher |
| 441 | // can be used instead. |
| 442 | reqPath := normalizeWindowsPath(strings.ToLower(r.URL.Path)) |
| 443 | |
| 444 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 445 | |
| 446 | for _, matchPattern := range m { |
| 447 | if strings.Contains(matchPattern, "{") { |
| 448 | matchPattern = strings.ToLower(repl.ReplaceAll(matchPattern, "")) |
| 449 | } |
| 450 | |
| 451 | // special case: whole path is wildcard; this is unnecessary |
| 452 | // as it matches all requests, which is the same as no matcher |
| 453 | if matchPattern == "*" { |
| 454 | return true, nil |
| 455 | } |
| 456 | |
| 457 | // Clean the path, merge doubled slashes, etc. |
| 458 | // This ensures maliciously crafted requests can't bypass |
| 459 | // the path matcher. See #4407. Good security posture |
| 460 | // requires that we should do all we can to reduce any |
| 461 | // funny-looking paths into "normalized" forms such that |
| 462 | // weird variants can't sneak by. |
| 463 | // |
| 464 | // How we clean the path depends on the kind of pattern: |
| 465 | // we either merge slashes or we don't. If the pattern |
| 466 | // has double slashes, we preserve them in the path. |
| 467 | // |
| 468 | // TODO: Despite the fact that the *vast* majority of path |
| 469 | // matchers have only 1 pattern, a possible optimization is |
| 470 | // to remember the cleaned form of the path for future |
| 471 | // iterations; it's just that the way we clean depends on |
| 472 | // the kind of pattern. |
| 473 | |
| 474 | mergeSlashes := !strings.Contains(matchPattern, "//") |
| 475 | |
| 476 | // if '%' appears in the match pattern, we interpret that to mean |
| 477 | // the intent is to compare that part of the path in raw/escaped |
| 478 | // space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/" |
| 479 | if strings.Contains(matchPattern, "%") { |
| 480 | escapedPath := r.URL.EscapedPath() |
| 481 | if runtime.GOOS == "windows" { |
| 482 | escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath) |
| 483 | matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern) |
| 484 | escapedPath = normalizeWindowsEscapedPath(escapedPath) |
| 485 | } |
| 486 | reqPathForPattern := CleanPath(escapedPath, mergeSlashes) |
| 487 | if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) { |
| 488 | return true, nil |
| 489 | } |
| 490 | |
| 491 | // doing prefix/suffix/substring matches doesn't make sense |
| 492 | continue |
| 493 | } |
| 494 | |
| 495 | reqPathForPattern := CleanPath(reqPath, mergeSlashes) |
| 496 | |
| 497 | // for substring, prefix, and suffix matching, only perform those |
| 498 | // special, fast matches if they are the only wildcards in the pattern; |
| 499 | // otherwise we assume a globular match if any * appears in the middle |
| 500 | |
| 501 | // special case: first and last characters are wildcard, |
| 502 | // treat it as a fast substring match |
| 503 | if strings.Count(matchPattern, "*") == 2 && |
| 504 | strings.HasPrefix(matchPattern, "*") && |
| 505 | strings.HasSuffix(matchPattern, "*") { |
| 506 | if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) { |
| 507 | return true, nil |
| 508 | } |
| 509 | continue |
| 510 | } |
| 511 | |
| 512 | // only perform prefix/suffix match if it is the only wildcard... |
| 513 | // I think that is more correct most of the time |
| 514 | if strings.Count(matchPattern, "*") == 1 { |
| 515 | // special case: first character is a wildcard, |
| 516 | // treat it as a fast suffix match |
| 517 | if strings.HasPrefix(matchPattern, "*") { |
| 518 | if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) { |
| 519 | return true, nil |
| 520 | } |
| 521 | continue |
| 522 | } |
| 523 | |
| 524 | // special case: last character is a wildcard, |
| 525 | // treat it as a fast prefix match |
| 526 | if strings.HasSuffix(matchPattern, "*") { |
| 527 | if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) { |
| 528 | return true, nil |
| 529 | } |
| 530 | continue |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | // at last, use globular matching, which also is exact matching |
| 535 | // if there are no glob/wildcard chars; we ignore the error here |
| 536 | // because we can't handle it anyway |
| 537 | matches, _ := path.Match(matchPattern, reqPathForPattern) |
| 538 | if matches { |
| 539 | return true, nil |
| 540 | } |
| 541 | } |
| 542 | return false, nil |
| 543 | } |
| 544 | |
| 545 | func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool { |
| 546 | escapedPath = strings.ToLower(escapedPath) |
| 547 | // We would just compare the pattern against r.URL.Path, |
| 548 | // but the pattern contains %, indicating that we should |
| 549 | // compare at least some part of the path in raw/escaped |
| 550 | // space, not normalized space; so we build the string we |
| 551 | // will compare against by adding the normalized parts |
| 552 | // of the path, then switching to the escaped parts where |
| 553 | // the pattern hints to us wherever % is present. |
| 554 | var sb strings.Builder |
| 555 | sb.Grow(len(escapedPath)) |
| 556 | |
| 557 | // iterate the pattern and escaped path in lock-step; |
| 558 | // increment iPattern every time we consume a char from the pattern, |
| 559 | // increment iPath every time we consume a char from the path; |
| 560 | // iPattern and iPath are our cursors/iterator positions for each string |
| 561 | var iPattern, iPath int |
| 562 | for { |
| 563 | if iPattern >= len(matchPath) || iPath >= len(escapedPath) { |
| 564 | break |
| 565 | } |
| 566 | // get the next character from the request path |
| 567 | |
| 568 | pathCh := string(escapedPath[iPath]) |
| 569 | var escapedPathCh string |
| 570 | |
| 571 | // normalize (decode) escape sequences |
| 572 | if pathCh == "%" && len(escapedPath) >= iPath+3 { |
| 573 | // hold onto this in case we find out the intent is to match in escaped space here; |
| 574 | // we lowercase it even though technically the spec says: "For consistency, URI |
| 575 | // producers and normalizers should use uppercase hexadecimal digits for all percent- |
| 576 | // encodings" (RFC 3986 section 2.1) - we lowercased the matcher pattern earlier in |
| 577 | // provisioning so we do the same here to gain case-insensitivity in equivalence; |
| 578 | // besides, this string is never shown visibly |
| 579 | escapedPathCh = strings.ToLower(escapedPath[iPath : iPath+3]) |
| 580 | |
| 581 | var err error |
| 582 | pathCh, err = url.PathUnescape(escapedPathCh) |
| 583 | if err != nil { |
| 584 | // should be impossible unless EscapedPath() is giving us an invalid sequence! |
| 585 | return false |
| 586 | } |
| 587 | iPath += 2 // escape sequence is 2 bytes longer than normal char |
| 588 | } |
| 589 | |
| 590 | // now get the next character from the pattern |
| 591 | |
| 592 | normalize := true |
| 593 | switch matchPath[iPattern] { |
| 594 | case '%': |
| 595 | // escape sequence |
| 596 | |
| 597 | // if not a wildcard ("%*"), compare literally; consume next two bytes of pattern |
| 598 | if len(matchPath) >= iPattern+3 && matchPath[iPattern+1] != '*' { |
| 599 | sb.WriteString(escapedPathCh) |
| 600 | iPath++ |
| 601 | iPattern += 2 |
| 602 | break |
| 603 | } |
| 604 | |
| 605 | // escaped wildcard sequence; consume next byte only ('*') |
| 606 | iPattern++ |
| 607 | normalize = false |
| 608 | |
| 609 | fallthrough |
| 610 | case '*': |
| 611 | // wildcard, so consume until next matching character |
| 612 | remaining := escapedPath[iPath:] |
| 613 | until := len(escapedPath) - iPath // go until end of string... |
| 614 | if iPattern < len(matchPath)-1 { // ...unless the * is not at the end |
| 615 | nextCh := matchPath[iPattern+1] |
| 616 | until = strings.IndexByte(remaining, nextCh) |
| 617 | if until == -1 { |
| 618 | // terminating char of wildcard span not found, so definitely no match |
| 619 | return false |
| 620 | } |
| 621 | } |
| 622 | if until == 0 { |
| 623 | // empty span; nothing to add on this iteration |
| 624 | break |
| 625 | } |
| 626 | next := remaining[:until] |
| 627 | if normalize { |
| 628 | var err error |
| 629 | next, err = url.PathUnescape(next) |
| 630 | if err != nil { |
| 631 | return false // should be impossible anyway |
| 632 | } |
| 633 | } |
| 634 | sb.WriteString(next) |
| 635 | iPath += until |
| 636 | default: |
| 637 | sb.WriteString(pathCh) |
| 638 | iPath++ |
| 639 | } |
| 640 | |
| 641 | iPattern++ |
| 642 | } |
| 643 | |
| 644 | // if the pattern was fully consumed before the path, the remaining path |
| 645 | // bytes still have to be part of the string we match against; otherwise a |
| 646 | // pattern with no trailing wildcard (e.g. "/foo%2fbar") would behave like a |
| 647 | // prefix match and incorrectly match longer paths (e.g. "/foo%2fbarbaz"). |
| 648 | // the pattern has no more escape hints here, so compare in normalised space. |
| 649 | if iPath < len(escapedPath) { |
| 650 | remaining := escapedPath[iPath:] |
| 651 | if unescaped, err := url.PathUnescape(remaining); err == nil { |
| 652 | sb.WriteString(unescaped) |
| 653 | } else { |
| 654 | sb.WriteString(remaining) |
| 655 | } |
| 656 | } |
| 657 | |
| 658 | // we can now treat rawpath globs (%*) as regular globs (*) |
| 659 | matchPath = strings.ReplaceAll(matchPath, "%*", "*") |
| 660 | |
| 661 | // ignore error here because we can't handle it anyway |
| 662 | matches, _ := path.Match(matchPath, strings.ToLower(sb.String())) |
| 663 | return matches |
| 664 | } |
| 665 | |
| 666 | // windowsEscapedPathSeparatorRepl normalizes Windows backslash separators |
| 667 | // while preserving escaped-path matching semantics. |
| 668 | var windowsEscapedPathSeparatorRepl = strings.NewReplacer( |
| 669 | `\`, "%2f", |
| 670 | "%5c", "%2f", |
| 671 | "%5C", "%2f", |
| 672 | ) |
| 673 | |
| 674 | // NormalizedPath returns a decoded request path in the form the path matchers |
| 675 | // evaluate it: rewritten the way the Windows filesystem resolves it (a no-op on |
| 676 | // other platforms), then cleaned. Consumers that act on a path a matcher has |
| 677 | // already judged should derive it with this, so the two cannot disagree about |
| 678 | // which resource a request names. An empty path is returned unchanged. |
| 679 | func NormalizedPath(p string, mergeSlashes bool) string { |
| 680 | if p == "" { |
| 681 | return "" |
| 682 | } |
| 683 | return CleanPath(normalizeWindowsPath(p), mergeSlashes) |
| 684 | } |
| 685 | |
| 686 | // normalizeWindowsPath rewrites a decoded request path the way the Windows |
| 687 | // filesystem resolves it, so that path matchers (both `path` and `path_regexp`) |
| 688 | // cannot be bypassed on Windows: backslashes are treated as path separators, |
| 689 | // and trailing dots and spaces are ignored on EVERY path component (not only at |
| 690 | // the end of the whole path — e.g. a guard on `/private/` must also cover |
| 691 | // `/private./secret`). Path navigation segments ("." and "..") are left intact |
| 692 | // for cleanPath to resolve. It is a no-op on non-Windows systems. See #5613. |
| 693 | func normalizeWindowsPath(p string) string { |
| 694 | if runtime.GOOS != "windows" { |
| 695 | return p |
| 696 | } |
| 697 | p = strings.ReplaceAll(p, `\`, "/") |
| 698 | segments := strings.Split(p, "/") |
| 699 | for i, s := range segments { |
| 700 | if s == "." || s == ".." { |
| 701 | continue |
| 702 | } |
| 703 | segments[i] = strings.TrimRight(s, ". ") |
| 704 | } |
| 705 | return strings.Join(segments, "/") |
| 706 | } |
| 707 | |
| 708 | const windowsShortNamePunctuation = "$%'-_@~`!(){}^#&" |
| 709 | |
| 710 | // possibleWindowsShortNamePart reports whether s could be part of an 8.3 |
| 711 | // alias. Extended characters are allowed when Windows' allowextchar behavior |
| 712 | // is enabled, so non-ASCII characters must be accepted here to fail closed. |
| 713 | func possibleWindowsShortNamePart(s string) bool { |
| 714 | for _, c := range s { |
| 715 | if c >= utf8.RuneSelf || |
| 716 | (c >= 'a' && c <= 'z') || |
| 717 | (c >= 'A' && c <= 'Z') || |
| 718 | (c >= '0' && c <= '9') || |
| 719 | strings.ContainsRune(windowsShortNamePunctuation, c) { |
| 720 | continue |
| 721 | } |
| 722 | return false |
| 723 | } |
| 724 | return true |
| 725 | } |
| 726 | |
| 727 | // HasWindowsShortName reports whether any component of p has the syntax of an |
| 728 | // NTFS 8.3 short name. Both slash types separate components on Windows, which |
| 729 | // also ignores trailing dots and spaces in each component. |
| 730 | func HasWindowsShortName(p string) bool { |
| 731 | for _, component := range strings.FieldsFunc(p, func(r rune) bool { |
| 732 | return r == '/' || r == '\\' |
| 733 | }) { |
| 734 | component = strings.TrimRight(component, ". ") |
| 735 | base, extension, hasExtension := strings.Cut(component, ".") |
| 736 | if utf8.RuneCountInString(base) > 8 || !possibleWindowsShortNamePart(base) || |
| 737 | (hasExtension && (utf8.RuneCountInString(extension) > 3 || !possibleWindowsShortNamePart(extension))) { |
| 738 | continue |
| 739 | } |
| 740 | |
| 741 | tilde := strings.LastIndexByte(base, '~') |
| 742 | if tilde <= 0 || tilde == len(base)-1 { |
| 743 | continue |
| 744 | } |
| 745 | shortNameNumber := base[tilde+1:] |
| 746 | for _, c := range shortNameNumber { |
| 747 | if c < '0' || c > '9' { |
| 748 | shortNameNumber = "" |
| 749 | break |
| 750 | } |
| 751 | } |
| 752 | if shortNameNumber != "" { |
| 753 | return true |
| 754 | } |
| 755 | } |
| 756 | return false |
| 757 | } |
| 758 | |
| 759 | // normalizeWindowsEscapedPath applies the same Windows normalization as |
| 760 | // normalizeWindowsPath, but in raw/escaped space (used when a matcher pattern |
| 761 | // contains '%'), where path separators may be percent-encoded ("%2f") and a |
| 762 | // component's trailing dots and spaces may be literal ("." / " ") or |
| 763 | // percent-encoded ("%2e" / "%20"). Without this, a '%'-containing matcher such |
| 764 | // as `/private%2f*` is bypassed on Windows by e.g. GET /private.%5csecret.txt, |
| 765 | // because only backslash separators — not the per-component trailing dots and |
| 766 | // spaces — were folded. The caller must have already folded backslash |
| 767 | // separators to "%2f" via windowsEscapedPathSeparatorRepl. Navigation segments |
| 768 | // ("." and "..") are left intact for CleanPath to resolve. Only the request |
| 769 | // path is normalized, not the matcher pattern (mirroring normalizeWindowsPath). |
| 770 | // It is a no-op on non-Windows systems. See #5613. |
| 771 | func normalizeWindowsEscapedPath(p string) string { |
| 772 | if runtime.GOOS != "windows" { |
| 773 | return p |
| 774 | } |
| 775 | var sb strings.Builder |
| 776 | sb.Grow(len(p)) |
| 777 | // walk the path, splitting on separators that are meaningful on Windows: |
| 778 | // the literal '/' and the encoded '%2f'/'%2F' (backslashes were already |
| 779 | // folded to "%2f" by the caller). Trim each component, preserving the |
| 780 | // separators exactly so escaped-space comparison still lines up. |
| 781 | start := 0 |
| 782 | flush := func(end int) { |
| 783 | seg := p[start:end] |
| 784 | if seg != "." && seg != ".." { |
| 785 | seg = trimWindowsEscapedTrailingDotSpace(seg) |
| 786 | } |
| 787 | sb.WriteString(seg) |
| 788 | } |
| 789 | for i := 0; i < len(p); { |
| 790 | switch { |
| 791 | case p[i] == '/': |
| 792 | flush(i) |
| 793 | sb.WriteByte('/') |
| 794 | i++ |
| 795 | start = i |
| 796 | case p[i] == '%' && i+3 <= len(p) && p[i+1] == '2' && (p[i+2] == 'f' || p[i+2] == 'F'): |
| 797 | flush(i) |
| 798 | sb.WriteString(p[i : i+3]) |
| 799 | i += 3 |
| 800 | start = i |
| 801 | default: |
| 802 | i++ |
| 803 | } |
| 804 | } |
| 805 | flush(len(p)) |
| 806 | return sb.String() |
| 807 | } |
| 808 | |
| 809 | // trimWindowsEscapedTrailingDotSpace strips trailing dots and spaces — whether |
| 810 | // literal ("." / " ") or percent-encoded ("%2e"/"%2E" / "%20") — from a single |
| 811 | // escaped-space path component, matching how Windows ignores them. |
| 812 | func trimWindowsEscapedTrailingDotSpace(s string) string { |
| 813 | for len(s) > 0 { |
| 814 | if c := s[len(s)-1]; c == '.' || c == ' ' { |
| 815 | s = s[:len(s)-1] |
| 816 | continue |
| 817 | } |
| 818 | if len(s) >= 3 { |
| 819 | if tail := s[len(s)-3:]; strings.EqualFold(tail, "%2e") || tail == "%20" { |
| 820 | s = s[:len(s)-3] |
| 821 | continue |
| 822 | } |
| 823 | } |
| 824 | return s |
| 825 | } |
| 826 | return s |
| 827 | } |
| 828 | |
| 829 | // CELLibrary produces options that expose this matcher for use in CEL |
| 830 | // expression matchers. |
| 831 | // |
| 832 | // Example: |
| 833 | // |
| 834 | // expression path('*substring*', '*suffix') |
| 835 | func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { |
| 836 | return CELMatcherImpl( |
| 837 | // name of the macro, this is the function name that users see when writing expressions. |
| 838 | "path", |
| 839 | // name of the function that the macro will be rewritten to call. |
| 840 | "path_match_request_list", |
| 841 | // internal data type of the MatchPath value. |
| 842 | []*cel.Type{cel.ListType(cel.StringType)}, |
| 843 | // function to convert a constant list of strings to a MatchPath instance. |
| 844 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 845 | refStringList := stringSliceType |
| 846 | strList, err := data.ConvertToNative(refStringList) |
| 847 | if err != nil { |
| 848 | return nil, err |
| 849 | } |
| 850 | matcher := MatchPath(strList.([]string)) |
| 851 | err = matcher.Provision(ctx) |
| 852 | return matcher, err |
| 853 | }, |
| 854 | ) |
| 855 | } |
| 856 | |
| 857 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 858 | func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 859 | // iterate to merge multiple matchers into one |
| 860 | for d.Next() { |
| 861 | *m = append(*m, d.RemainingArgs()...) |
| 862 | if d.NextBlock(0) { |
| 863 | return d.Err("malformed path matcher: blocks are not supported") |
| 864 | } |
| 865 | } |
| 866 | return nil |
| 867 | } |
| 868 | |
| 869 | // CaddyModule returns the Caddy module information. |
| 870 | func (MatchPathRE) CaddyModule() caddy.ModuleInfo { |
| 871 | return caddy.ModuleInfo{ |
| 872 | ID: "http.matchers.path_regexp", |
| 873 | New: func() caddy.Module { return new(MatchPathRE) }, |
| 874 | } |
| 875 | } |
| 876 | |
| 877 | // Match returns true if r matches m. |
| 878 | func (m MatchPathRE) Match(r *http.Request) bool { |
| 879 | match, _ := m.MatchWithError(r) |
| 880 | return match |
| 881 | } |
| 882 | |
| 883 | // MatchWithError returns true if r matches m. |
| 884 | func (m MatchPathRE) MatchWithError(r *http.Request) (bool, error) { |
| 885 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 886 | |
| 887 | // Clean the path, merges doubled slashes, etc. |
| 888 | // This ensures maliciously crafted requests can't bypass |
| 889 | // the path matcher. See #4407 (path cleaning) and #5613 |
| 890 | // (Windows backslash / trailing dot-and-space normalization, |
| 891 | // shared with the path matcher). |
| 892 | cleanedPath := cleanPath(normalizeWindowsPath(r.URL.Path)) |
| 893 | |
| 894 | return m.MatchRegexp.Match(cleanedPath, repl), nil |
| 895 | } |
| 896 | |
| 897 | // CELLibrary produces options that expose this matcher for use in CEL |
| 898 | // expression matchers. |
| 899 | // |
| 900 | // Example: |
| 901 | // |
| 902 | // expression path_regexp('^/bar') |
| 903 | // |
| 904 | // The pattern is a CEL string literal, so CEL's own escape sequences |
| 905 | // (e.g. \b, \n, \t) are interpreted before the result reaches the regexp |
| 906 | // engine. A pattern that needs a literal backslash escape meant for the |
| 907 | // regexp engine, such as \b for a word boundary, should be written as a |
| 908 | // CEL raw string (prefixed with r) so CEL passes the backslash through |
| 909 | // unchanged: expression path_regexp(r'^/bar\b'). |
| 910 | func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { |
| 911 | unnamedPattern, err := CELMatcherImpl( |
| 912 | "path_regexp", |
| 913 | "path_regexp_request_string", |
| 914 | []*cel.Type{cel.StringType}, |
| 915 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 916 | pattern := data.(types.String) |
| 917 | matcher := MatchPathRE{MatchRegexp{ |
| 918 | Name: ctx.Value(MatcherNameCtxKey).(string), |
| 919 | Pattern: string(pattern), |
| 920 | }} |
| 921 | err := matcher.Provision(ctx) |
| 922 | return matcher, err |
| 923 | }, |
| 924 | ) |
| 925 | if err != nil { |
| 926 | return nil, err |
| 927 | } |
| 928 | namedPattern, err := CELMatcherImpl( |
| 929 | "path_regexp", |
| 930 | "path_regexp_request_string_string", |
| 931 | []*cel.Type{cel.StringType, cel.StringType}, |
| 932 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 933 | refStringList := stringSliceType |
| 934 | params, err := data.ConvertToNative(refStringList) |
| 935 | if err != nil { |
| 936 | return nil, err |
| 937 | } |
| 938 | strParams := params.([]string) |
| 939 | name := strParams[0] |
| 940 | if name == "" { |
| 941 | name = ctx.Value(MatcherNameCtxKey).(string) |
| 942 | } |
| 943 | matcher := MatchPathRE{MatchRegexp{ |
| 944 | Name: name, |
| 945 | Pattern: strParams[1], |
| 946 | }} |
| 947 | err = matcher.Provision(ctx) |
| 948 | return matcher, err |
| 949 | }, |
| 950 | ) |
| 951 | if err != nil { |
| 952 | return nil, err |
| 953 | } |
| 954 | envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...) |
| 955 | prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...) |
| 956 | return NewMatcherCELLibrary(envOpts, prgOpts), nil |
| 957 | } |
| 958 | |
| 959 | // CaddyModule returns the Caddy module information. |
| 960 | func (MatchMethod) CaddyModule() caddy.ModuleInfo { |
| 961 | return caddy.ModuleInfo{ |
| 962 | ID: "http.matchers.method", |
| 963 | New: func() caddy.Module { return new(MatchMethod) }, |
| 964 | } |
| 965 | } |
| 966 | |
| 967 | // Provision uppercases all configured methods to ensure case-insensitive |
| 968 | // matching against incoming requests, whose methods are always uppercase. |
| 969 | func (m MatchMethod) Provision(_ caddy.Context) error { |
| 970 | for i := range m { |
| 971 | m[i] = strings.ToUpper(m[i]) |
| 972 | } |
| 973 | return nil |
| 974 | } |
| 975 | |
| 976 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 977 | func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 978 | // iterate to merge multiple matchers into one |
| 979 | for d.Next() { |
| 980 | *m = append(*m, d.RemainingArgs()...) |
| 981 | if d.NextBlock(0) { |
| 982 | return d.Err("malformed method matcher: blocks are not supported") |
| 983 | } |
| 984 | } |
| 985 | return nil |
| 986 | } |
| 987 | |
| 988 | // Match returns true if r matches m. |
| 989 | func (m MatchMethod) Match(r *http.Request) bool { |
| 990 | match, _ := m.MatchWithError(r) |
| 991 | return match |
| 992 | } |
| 993 | |
| 994 | // MatchWithError returns true if r matches m. |
| 995 | func (m MatchMethod) MatchWithError(r *http.Request) (bool, error) { |
| 996 | return slices.Contains(m, r.Method), nil |
| 997 | } |
| 998 | |
| 999 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1000 | // expression matchers. |
| 1001 | // |
| 1002 | // Example: |
| 1003 | // |
| 1004 | // expression method('PUT', 'POST') |
| 1005 | func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { |
| 1006 | return CELMatcherImpl( |
| 1007 | "method", |
| 1008 | "method_request_list", |
| 1009 | []*cel.Type{cel.ListType(cel.StringType)}, |
| 1010 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1011 | refStringList := stringSliceType |
| 1012 | strList, err := data.ConvertToNative(refStringList) |
| 1013 | if err != nil { |
| 1014 | return nil, err |
| 1015 | } |
| 1016 | return MatchMethod(strList.([]string)), nil |
| 1017 | }, |
| 1018 | ) |
| 1019 | } |
| 1020 | |
| 1021 | // CaddyModule returns the Caddy module information. |
| 1022 | func (MatchQuery) CaddyModule() caddy.ModuleInfo { |
| 1023 | return caddy.ModuleInfo{ |
| 1024 | ID: "http.matchers.query", |
| 1025 | New: func() caddy.Module { return new(MatchQuery) }, |
| 1026 | } |
| 1027 | } |
| 1028 | |
| 1029 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1030 | func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1031 | if *m == nil { |
| 1032 | *m = make(map[string][]string) |
| 1033 | } |
| 1034 | // iterate to merge multiple matchers into one |
| 1035 | for d.Next() { |
| 1036 | for _, query := range d.RemainingArgs() { |
| 1037 | if query == "" { |
| 1038 | continue |
| 1039 | } |
| 1040 | before, after, found := strings.Cut(query, "=") |
| 1041 | if !found { |
| 1042 | return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val()) |
| 1043 | } |
| 1044 | url.Values(*m).Add(before, after) |
| 1045 | } |
| 1046 | if d.NextBlock(0) { |
| 1047 | return d.Err("malformed query matcher: blocks are not supported") |
| 1048 | } |
| 1049 | } |
| 1050 | return nil |
| 1051 | } |
| 1052 | |
| 1053 | // Match returns true if r matches m. An empty m matches an empty query string. |
| 1054 | func (m MatchQuery) Match(r *http.Request) bool { |
| 1055 | match, _ := m.MatchWithError(r) |
| 1056 | return match |
| 1057 | } |
| 1058 | |
| 1059 | // MatchWithError returns true if r matches m. |
| 1060 | // An empty m matches an empty query string. |
| 1061 | func (m MatchQuery) MatchWithError(r *http.Request) (bool, error) { |
| 1062 | // If no query keys are configured, this only |
| 1063 | // matches an empty query string. |
| 1064 | if len(m) == 0 { |
| 1065 | return len(r.URL.Query()) == 0, nil |
| 1066 | } |
| 1067 | |
| 1068 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 1069 | |
| 1070 | // parse query string just once, for efficiency |
| 1071 | parsed, err := url.ParseQuery(r.URL.RawQuery) |
| 1072 | if err != nil { |
| 1073 | // Illegal query string. Likely bad escape sequence or unescaped literals. |
| 1074 | // Note that semicolons in query string have a controversial history. Summaries: |
| 1075 | // - https://github.com/golang/go/issues/50034 |
| 1076 | // - https://github.com/golang/go/issues/25192 |
| 1077 | // Despite the URL WHATWG spec mandating the use of & separators for query strings, |
| 1078 | // every URL parser implementation is different, and Filippo Valsorda rightly wrote: |
| 1079 | // "Relying on parser alignment for security is doomed." Overall conclusion is that |
| 1080 | // splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;. |
| 1081 | // We regard the Go team's decision as sound and thus reject malformed query strings. |
| 1082 | return false, nil |
| 1083 | } |
| 1084 | |
| 1085 | // Count the amount of matched keys, to ensure we AND |
| 1086 | // between all configured query keys; all keys must |
| 1087 | // match at least one value. |
| 1088 | matchedKeys := 0 |
| 1089 | for param, vals := range m { |
| 1090 | param = repl.ReplaceAll(param, "") |
| 1091 | paramVal, found := parsed[param] |
| 1092 | if !found { |
| 1093 | return false, nil |
| 1094 | } |
| 1095 | for _, v := range vals { |
| 1096 | v = repl.ReplaceAll(v, "") |
| 1097 | if slices.Contains(paramVal, v) || v == "*" { |
| 1098 | matchedKeys++ |
| 1099 | break |
| 1100 | } |
| 1101 | } |
| 1102 | } |
| 1103 | return matchedKeys == len(m), nil |
| 1104 | } |
| 1105 | |
| 1106 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1107 | // expression matchers. |
| 1108 | // |
| 1109 | // Example: |
| 1110 | // |
| 1111 | // expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']}) |
| 1112 | func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) { |
| 1113 | return CELMatcherImpl( |
| 1114 | "query", |
| 1115 | "query_matcher_request_map", |
| 1116 | []*cel.Type{CELTypeJSON}, |
| 1117 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1118 | mapStrListStr, err := CELValueToMapStrList(data) |
| 1119 | if err != nil { |
| 1120 | return nil, err |
| 1121 | } |
| 1122 | return MatchQuery(url.Values(mapStrListStr)), nil |
| 1123 | }, |
| 1124 | ) |
| 1125 | } |
| 1126 | |
| 1127 | // CaddyModule returns the Caddy module information. |
| 1128 | func (MatchHeader) CaddyModule() caddy.ModuleInfo { |
| 1129 | return caddy.ModuleInfo{ |
| 1130 | ID: "http.matchers.header", |
| 1131 | New: func() caddy.Module { return new(MatchHeader) }, |
| 1132 | } |
| 1133 | } |
| 1134 | |
| 1135 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1136 | func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1137 | if *m == nil { |
| 1138 | *m = make(map[string][]string) |
| 1139 | } |
| 1140 | // iterate to merge multiple matchers into one |
| 1141 | for d.Next() { |
| 1142 | var field, val string |
| 1143 | if !d.Args(&field) { |
| 1144 | return d.Errf("malformed header matcher: expected field") |
| 1145 | } |
| 1146 | |
| 1147 | if strings.HasPrefix(field, "!") { |
| 1148 | if len(field) == 1 { |
| 1149 | return d.Errf("malformed header matcher: must have field name following ! character") |
| 1150 | } |
| 1151 | |
| 1152 | field = field[1:] |
| 1153 | headers := *m |
| 1154 | headers[field] = nil |
| 1155 | m = &headers |
| 1156 | if d.NextArg() { |
| 1157 | return d.Errf("malformed header matcher: null matching headers cannot have a field value") |
| 1158 | } |
| 1159 | } else { |
| 1160 | if !d.NextArg() { |
| 1161 | return d.Errf("malformed header matcher: expected both field and value") |
| 1162 | } |
| 1163 | |
| 1164 | // If multiple header matchers with the same header field are defined, |
| 1165 | // we want to add the existing to the list of headers (will be OR'ed) |
| 1166 | val = d.Val() |
| 1167 | http.Header(*m).Add(field, val) |
| 1168 | } |
| 1169 | |
| 1170 | if d.NextBlock(0) { |
| 1171 | return d.Err("malformed header matcher: blocks are not supported") |
| 1172 | } |
| 1173 | } |
| 1174 | return nil |
| 1175 | } |
| 1176 | |
| 1177 | // Match returns true if r matches m. |
| 1178 | func (m MatchHeader) Match(r *http.Request) bool { |
| 1179 | match, _ := m.MatchWithError(r) |
| 1180 | return match |
| 1181 | } |
| 1182 | |
| 1183 | // MatchWithError returns true if r matches m. |
| 1184 | func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) { |
| 1185 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 1186 | return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl), nil |
| 1187 | } |
| 1188 | |
| 1189 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1190 | // expression matchers. |
| 1191 | // |
| 1192 | // Example: |
| 1193 | // |
| 1194 | // expression header({'content-type': 'image/png'}) |
| 1195 | // expression header({'foo': ['bar', 'baz']}) // match bar or baz |
| 1196 | func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) { |
| 1197 | return CELMatcherImpl( |
| 1198 | "header", |
| 1199 | "header_matcher_request_map", |
| 1200 | []*cel.Type{CELTypeJSON}, |
| 1201 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1202 | mapStrListStr, err := CELValueToMapStrList(data) |
| 1203 | if err != nil { |
| 1204 | return nil, err |
| 1205 | } |
| 1206 | return MatchHeader(http.Header(mapStrListStr)), nil |
| 1207 | }, |
| 1208 | ) |
| 1209 | } |
| 1210 | |
| 1211 | // getHeaderFieldVals returns the field values for the given fieldName from input. |
| 1212 | // The host parameter should be obtained from the http.Request.Host field, and the |
| 1213 | // transferEncoding from http.Request.TransferEncoding, since net/http removes them |
| 1214 | // from the header map. |
| 1215 | func getHeaderFieldVals(input http.Header, fieldName, host string, transferEncoding []string) []string { |
| 1216 | fieldName = textproto.CanonicalMIMEHeaderKey(fieldName) |
| 1217 | if fieldName == "Host" && host != "" { |
| 1218 | return []string{host} |
| 1219 | } |
| 1220 | if fieldName == "Transfer-Encoding" && input[fieldName] == nil { |
| 1221 | return transferEncoding |
| 1222 | } |
| 1223 | return input[fieldName] |
| 1224 | } |
| 1225 | |
| 1226 | // matchHeaders returns true if input matches the criteria in against without regex. |
| 1227 | // The host parameter should be obtained from the http.Request.Host field since |
| 1228 | // net/http removes it from the header map. |
| 1229 | func matchHeaders(input, against http.Header, host string, transferEncoding []string, repl *caddy.Replacer) bool { |
| 1230 | for field, allowedFieldVals := range against { |
| 1231 | actualFieldVals := getHeaderFieldVals(input, field, host, transferEncoding) |
| 1232 | if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil { |
| 1233 | // a non-nil but empty list of allowed values means |
| 1234 | // match if the header field exists at all |
| 1235 | continue |
| 1236 | } |
| 1237 | if allowedFieldVals == nil && actualFieldVals == nil { |
| 1238 | // a nil list means match if the header does not exist at all |
| 1239 | continue |
| 1240 | } |
| 1241 | var match bool |
| 1242 | fieldVals: |
| 1243 | for _, actualFieldVal := range actualFieldVals { |
| 1244 | for _, allowedFieldVal := range allowedFieldVals { |
| 1245 | if repl != nil { |
| 1246 | allowedFieldVal = repl.ReplaceAll(allowedFieldVal, "") |
| 1247 | } |
| 1248 | switch { |
| 1249 | case allowedFieldVal == "*": |
| 1250 | match = true |
| 1251 | case strings.HasPrefix(allowedFieldVal, "*") && strings.HasSuffix(allowedFieldVal, "*"): |
| 1252 | match = strings.Contains(actualFieldVal, allowedFieldVal[1:len(allowedFieldVal)-1]) |
| 1253 | case strings.HasPrefix(allowedFieldVal, "*"): |
| 1254 | match = strings.HasSuffix(actualFieldVal, allowedFieldVal[1:]) |
| 1255 | case strings.HasSuffix(allowedFieldVal, "*"): |
| 1256 | match = strings.HasPrefix(actualFieldVal, allowedFieldVal[:len(allowedFieldVal)-1]) |
| 1257 | default: |
| 1258 | match = actualFieldVal == allowedFieldVal |
| 1259 | } |
| 1260 | if match { |
| 1261 | break fieldVals |
| 1262 | } |
| 1263 | } |
| 1264 | } |
| 1265 | if !match { |
| 1266 | return false |
| 1267 | } |
| 1268 | } |
| 1269 | return true |
| 1270 | } |
| 1271 | |
| 1272 | // CaddyModule returns the Caddy module information. |
| 1273 | func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { |
| 1274 | return caddy.ModuleInfo{ |
| 1275 | ID: "http.matchers.header_regexp", |
| 1276 | New: func() caddy.Module { return new(MatchHeaderRE) }, |
| 1277 | } |
| 1278 | } |
| 1279 | |
| 1280 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1281 | func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1282 | if *m == nil { |
| 1283 | *m = make(map[string]*MatchRegexp) |
| 1284 | } |
| 1285 | // iterate to merge multiple matchers into one |
| 1286 | for d.Next() { |
| 1287 | var first, second, third string |
| 1288 | if !d.Args(&first, &second) { |
| 1289 | return d.ArgErr() |
| 1290 | } |
| 1291 | |
| 1292 | var name, field, val string |
| 1293 | if d.Args(&third) { |
| 1294 | name = first |
| 1295 | field = second |
| 1296 | val = third |
| 1297 | } else { |
| 1298 | field = first |
| 1299 | val = second |
| 1300 | } |
| 1301 | |
| 1302 | // Default to the named matcher's name, if no regexp name is provided |
| 1303 | if name == "" { |
| 1304 | name = d.GetContextString(caddyfile.MatcherNameCtxKey) |
| 1305 | } |
| 1306 | |
| 1307 | // If there's already a pattern for this field |
| 1308 | // then we would end up overwriting the old one |
| 1309 | if (*m)[field] != nil { |
| 1310 | return d.Errf("header_regexp matcher can only be used once per named matcher, per header field: %s", field) |
| 1311 | } |
| 1312 | |
| 1313 | (*m)[field] = &MatchRegexp{Pattern: val, Name: name} |
| 1314 | |
| 1315 | if d.NextBlock(0) { |
| 1316 | return d.Err("malformed header_regexp matcher: blocks are not supported") |
| 1317 | } |
| 1318 | } |
| 1319 | return nil |
| 1320 | } |
| 1321 | |
| 1322 | // Match returns true if r matches m. |
| 1323 | func (m MatchHeaderRE) Match(r *http.Request) bool { |
| 1324 | match, _ := m.MatchWithError(r) |
| 1325 | return match |
| 1326 | } |
| 1327 | |
| 1328 | // MatchWithError returns true if r matches m. |
| 1329 | func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) { |
| 1330 | for field, rm := range m { |
| 1331 | actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host, r.TransferEncoding) |
| 1332 | match := false |
| 1333 | fieldVal: |
| 1334 | for _, actualFieldVal := range actualFieldVals { |
| 1335 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 1336 | if rm.Match(actualFieldVal, repl) { |
| 1337 | match = true |
| 1338 | break fieldVal |
| 1339 | } |
| 1340 | } |
| 1341 | if !match { |
| 1342 | return false, nil |
| 1343 | } |
| 1344 | } |
| 1345 | return true, nil |
| 1346 | } |
| 1347 | |
| 1348 | // Provision compiles m's regular expressions. |
| 1349 | func (m MatchHeaderRE) Provision(ctx caddy.Context) error { |
| 1350 | for _, rm := range m { |
| 1351 | err := rm.Provision(ctx) |
| 1352 | if err != nil { |
| 1353 | return err |
| 1354 | } |
| 1355 | } |
| 1356 | return nil |
| 1357 | } |
| 1358 | |
| 1359 | // Validate validates m's regular expressions. |
| 1360 | func (m MatchHeaderRE) Validate() error { |
| 1361 | for _, rm := range m { |
| 1362 | err := rm.Validate() |
| 1363 | if err != nil { |
| 1364 | return err |
| 1365 | } |
| 1366 | } |
| 1367 | return nil |
| 1368 | } |
| 1369 | |
| 1370 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1371 | // expression matchers. |
| 1372 | // |
| 1373 | // Example: |
| 1374 | // |
| 1375 | // expression header_regexp('foo', 'Field', 'fo+') |
| 1376 | // |
| 1377 | // The pattern is a CEL string literal, so CEL's own escape sequences |
| 1378 | // (e.g. \b, \n, \t) are interpreted before the result reaches the regexp |
| 1379 | // engine. A pattern that needs a literal backslash escape meant for the |
| 1380 | // regexp engine, such as \b for a word boundary, should be written as a |
| 1381 | // CEL raw string (prefixed with r) so CEL passes the backslash through |
| 1382 | // unchanged: expression header_regexp('foo', 'User-Agent', r'bot\b'). |
| 1383 | // Without the r prefix, \b is interpreted by CEL as a backspace |
| 1384 | // character before the regexp engine ever sees the pattern. |
| 1385 | func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { |
| 1386 | unnamedPattern, err := CELMatcherImpl( |
| 1387 | "header_regexp", |
| 1388 | "header_regexp_request_string_string", |
| 1389 | []*cel.Type{cel.StringType, cel.StringType}, |
| 1390 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1391 | refStringList := stringSliceType |
| 1392 | params, err := data.ConvertToNative(refStringList) |
| 1393 | if err != nil { |
| 1394 | return nil, err |
| 1395 | } |
| 1396 | strParams := params.([]string) |
| 1397 | matcher := MatchHeaderRE{} |
| 1398 | matcher[strParams[0]] = &MatchRegexp{ |
| 1399 | Pattern: strParams[1], |
| 1400 | Name: ctx.Value(MatcherNameCtxKey).(string), |
| 1401 | } |
| 1402 | err = matcher.Provision(ctx) |
| 1403 | return matcher, err |
| 1404 | }, |
| 1405 | ) |
| 1406 | if err != nil { |
| 1407 | return nil, err |
| 1408 | } |
| 1409 | namedPattern, err := CELMatcherImpl( |
| 1410 | "header_regexp", |
| 1411 | "header_regexp_request_string_string_string", |
| 1412 | []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, |
| 1413 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1414 | refStringList := stringSliceType |
| 1415 | params, err := data.ConvertToNative(refStringList) |
| 1416 | if err != nil { |
| 1417 | return nil, err |
| 1418 | } |
| 1419 | strParams := params.([]string) |
| 1420 | name := strParams[0] |
| 1421 | if name == "" { |
| 1422 | name = ctx.Value(MatcherNameCtxKey).(string) |
| 1423 | } |
| 1424 | matcher := MatchHeaderRE{} |
| 1425 | matcher[strParams[1]] = &MatchRegexp{ |
| 1426 | Pattern: strParams[2], |
| 1427 | Name: name, |
| 1428 | } |
| 1429 | err = matcher.Provision(ctx) |
| 1430 | return matcher, err |
| 1431 | }, |
| 1432 | ) |
| 1433 | if err != nil { |
| 1434 | return nil, err |
| 1435 | } |
| 1436 | envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...) |
| 1437 | prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...) |
| 1438 | return NewMatcherCELLibrary(envOpts, prgOpts), nil |
| 1439 | } |
| 1440 | |
| 1441 | // CaddyModule returns the Caddy module information. |
| 1442 | func (MatchProtocol) CaddyModule() caddy.ModuleInfo { |
| 1443 | return caddy.ModuleInfo{ |
| 1444 | ID: "http.matchers.protocol", |
| 1445 | New: func() caddy.Module { return new(MatchProtocol) }, |
| 1446 | } |
| 1447 | } |
| 1448 | |
| 1449 | // Match returns true if r matches m. |
| 1450 | func (m MatchProtocol) Match(r *http.Request) bool { |
| 1451 | match, _ := m.MatchWithError(r) |
| 1452 | return match |
| 1453 | } |
| 1454 | |
| 1455 | // MatchWithError returns true if r matches m. |
| 1456 | func (m MatchProtocol) MatchWithError(r *http.Request) (bool, error) { |
| 1457 | switch string(m) { |
| 1458 | case "grpc": |
| 1459 | return strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc"), nil |
| 1460 | case "https": |
| 1461 | return r.TLS != nil, nil |
| 1462 | case "http": |
| 1463 | return r.TLS == nil, nil |
| 1464 | case "http/1.0": |
| 1465 | return r.ProtoMajor == 1 && r.ProtoMinor == 0, nil |
| 1466 | case "http/1.0+": |
| 1467 | return r.ProtoAtLeast(1, 0), nil |
| 1468 | case "http/1.1": |
| 1469 | return r.ProtoMajor == 1 && r.ProtoMinor == 1, nil |
| 1470 | case "http/1.1+": |
| 1471 | return r.ProtoAtLeast(1, 1), nil |
| 1472 | case "http/2": |
| 1473 | return r.ProtoMajor == 2, nil |
| 1474 | case "http/2+": |
| 1475 | return r.ProtoAtLeast(2, 0), nil |
| 1476 | case "http/3": |
| 1477 | return r.ProtoMajor == 3, nil |
| 1478 | case "http/3+": |
| 1479 | return r.ProtoAtLeast(3, 0), nil |
| 1480 | } |
| 1481 | return false, nil |
| 1482 | } |
| 1483 | |
| 1484 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1485 | func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1486 | // iterate to merge multiple matchers into one |
| 1487 | for d.Next() { |
| 1488 | var proto string |
| 1489 | if !d.Args(&proto) { |
| 1490 | return d.Err("expected exactly one protocol") |
| 1491 | } |
| 1492 | *m = MatchProtocol(proto) |
| 1493 | } |
| 1494 | return nil |
| 1495 | } |
| 1496 | |
| 1497 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1498 | // expression matchers. |
| 1499 | // |
| 1500 | // Example: |
| 1501 | // |
| 1502 | // expression protocol('https') |
| 1503 | func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { |
| 1504 | return CELMatcherImpl( |
| 1505 | "protocol", |
| 1506 | "protocol_request_string", |
| 1507 | []*cel.Type{cel.StringType}, |
| 1508 | func(data ref.Val) (RequestMatcherWithError, error) { |
| 1509 | protocolStr, ok := data.(types.String) |
| 1510 | if !ok { |
| 1511 | return nil, errors.New("protocol argument was not a string") |
| 1512 | } |
| 1513 | return MatchProtocol(strings.ToLower(string(protocolStr))), nil |
| 1514 | }, |
| 1515 | ) |
| 1516 | } |
| 1517 | |
| 1518 | // CaddyModule returns the Caddy module information. |
| 1519 | func (MatchTLS) CaddyModule() caddy.ModuleInfo { |
| 1520 | return caddy.ModuleInfo{ |
| 1521 | ID: "http.matchers.tls", |
| 1522 | New: func() caddy.Module { return new(MatchTLS) }, |
| 1523 | } |
| 1524 | } |
| 1525 | |
| 1526 | // Match returns true if r matches m. |
| 1527 | func (m MatchTLS) Match(r *http.Request) bool { |
| 1528 | match, _ := m.MatchWithError(r) |
| 1529 | return match |
| 1530 | } |
| 1531 | |
| 1532 | // MatchWithError returns true if r matches m. |
| 1533 | func (m MatchTLS) MatchWithError(r *http.Request) (bool, error) { |
| 1534 | if r.TLS == nil { |
| 1535 | return false, nil |
| 1536 | } |
| 1537 | if m.HandshakeComplete != nil { |
| 1538 | if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) || |
| 1539 | (*m.HandshakeComplete && !r.TLS.HandshakeComplete) { |
| 1540 | return false, nil |
| 1541 | } |
| 1542 | } |
| 1543 | return true, nil |
| 1544 | } |
| 1545 | |
| 1546 | // UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax: |
| 1547 | // |
| 1548 | // ... tls [early_data] |
| 1549 | // |
| 1550 | // EXPERIMENTAL SYNTAX: Subject to change. |
| 1551 | func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1552 | // iterate to merge multiple matchers into one |
| 1553 | for d.Next() { |
| 1554 | if d.NextArg() { |
| 1555 | switch d.Val() { |
| 1556 | case "early_data": |
| 1557 | var false bool |
| 1558 | m.HandshakeComplete = &false |
| 1559 | default: |
| 1560 | return d.Errf("unrecognized option '%s'", d.Val()) |
| 1561 | } |
| 1562 | } |
| 1563 | if d.NextArg() { |
| 1564 | return d.ArgErr() |
| 1565 | } |
| 1566 | if d.NextBlock(0) { |
| 1567 | return d.Err("malformed tls matcher: blocks are not supported yet") |
| 1568 | } |
| 1569 | } |
| 1570 | return nil |
| 1571 | } |
| 1572 | |
| 1573 | // CELLibrary produces options that expose this matcher for use in CEL |
| 1574 | // expression matchers. It mirrors the three states the Caddyfile `tls` |
| 1575 | // matcher itself supports: bare `tls` (any TLS connection, regardless of |
| 1576 | // handshake completion), `tls early_data` (handshake not yet complete), and |
| 1577 | // the otherwise-implicit "handshake complete" case. `tls()` is sugar for |
| 1578 | // `tls('any')`; both spellings are equivalent and supported. |
| 1579 | // |
| 1580 | // Example: |
| 1581 | // |
| 1582 | // expression tls() && !tls('early_data') |
| 1583 | func (MatchTLS) CELLibrary(ctx caddy.Context) (cel.Library, error) { |
| 1584 | const funcName = "tls_request_string" |
| 1585 | factory := func(data ref.Val) (RequestMatcherWithError, error) { |
| 1586 | str, ok := data.(types.String) |
| 1587 | if !ok { |
| 1588 | return nil, errors.New("tls argument was not a string") |
| 1589 | } |
| 1590 | switch string(str) { |
| 1591 | case "any": |
| 1592 | // Also reachable directly as tls('any'), not just via the |
| 1593 | // zero-arg tls() macro below -- both are valid and equivalent. |
| 1594 | return MatchTLS{}, nil |
| 1595 | case "complete": |
| 1596 | complete := true |
| 1597 | return MatchTLS{HandshakeComplete: &complete}, nil |
| 1598 | case "early_data": |
| 1599 | complete := false |
| 1600 | return MatchTLS{HandshakeComplete: &complete}, nil |
| 1601 | default: |
| 1602 | return nil, fmt.Errorf("tls argument must be 'any', 'complete', or 'early_data', got %q", str) |
| 1603 | } |
| 1604 | } |
| 1605 | |
| 1606 | // tls() is sugar for tls('any'): it expands to the same 2-arg call the |
| 1607 | // one-argument form produces, just with the argument injected by the |
| 1608 | // macro itself instead of typed out by the caller. |
| 1609 | zeroArgMacro := parser.NewGlobalMacro("tls", 0, |
| 1610 | func(eh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { |
| 1611 | return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewLiteral(types.String("any"))), nil |
| 1612 | }) |
| 1613 | oneArgMacro := parser.NewGlobalMacro("tls", 1, celMatcherStringMacroExpander(funcName)) |
| 1614 | |
| 1615 | envOptions := []cel.EnvOption{ |
| 1616 | cel.Macros(zeroArgMacro, oneArgMacro), |
| 1617 | cel.Function(funcName, |
| 1618 | cel.Overload(funcName, []*cel.Type{cel.ObjectType("http.Request"), cel.StringType}, cel.BoolType), |
| 1619 | cel.SingletonBinaryBinding(CELMatcherRuntimeFunction(funcName, factory))), |
| 1620 | } |
| 1621 | programOptions := []cel.ProgramOption{ |
| 1622 | cel.CustomDecorator(CELMatcherDecorator(funcName, factory)), |
| 1623 | } |
| 1624 | return NewMatcherCELLibrary(envOptions, programOptions), nil |
| 1625 | } |
| 1626 | |
| 1627 | // CaddyModule returns the Caddy module information. |
| 1628 | func (MatchNot) CaddyModule() caddy.ModuleInfo { |
| 1629 | return caddy.ModuleInfo{ |
| 1630 | ID: "http.matchers.not", |
| 1631 | New: func() caddy.Module { return new(MatchNot) }, |
| 1632 | } |
| 1633 | } |
| 1634 | |
| 1635 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1636 | func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1637 | // iterate to merge multiple matchers into one |
| 1638 | for d.Next() { |
| 1639 | matcherSet, err := ParseCaddyfileNestedMatcherSet(d) |
| 1640 | if err != nil { |
| 1641 | return err |
| 1642 | } |
| 1643 | m.MatcherSetsRaw = append(m.MatcherSetsRaw, matcherSet) |
| 1644 | } |
| 1645 | return nil |
| 1646 | } |
| 1647 | |
| 1648 | // UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON |
| 1649 | // bytes directly into m's MatcherSetsRaw field. |
| 1650 | func (m *MatchNot) UnmarshalJSON(data []byte) error { |
| 1651 | return json.Unmarshal(data, &m.MatcherSetsRaw) |
| 1652 | } |
| 1653 | |
| 1654 | // MarshalJSON satisfies json.Marshaler by marshaling |
| 1655 | // m's raw matcher sets. |
| 1656 | func (m MatchNot) MarshalJSON() ([]byte, error) { |
| 1657 | return json.Marshal(m.MatcherSetsRaw) |
| 1658 | } |
| 1659 | |
| 1660 | // Provision loads the matcher modules to be negated. |
| 1661 | func (m *MatchNot) Provision(ctx caddy.Context) error { |
| 1662 | matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw") |
| 1663 | if err != nil { |
| 1664 | return fmt.Errorf("loading matcher sets: %v", err) |
| 1665 | } |
| 1666 | for _, modMap := range matcherSets.([]map[string]any) { |
| 1667 | var ms MatcherSet |
| 1668 | for _, modIface := range modMap { |
| 1669 | if mod, ok := modIface.(RequestMatcherWithError); ok { |
| 1670 | ms = append(ms, mod) |
| 1671 | continue |
| 1672 | } |
| 1673 | if mod, ok := modIface.(RequestMatcher); ok { |
| 1674 | ms = append(ms, mod) |
| 1675 | continue |
| 1676 | } |
| 1677 | return fmt.Errorf("module is not a request matcher: %T", modIface) |
| 1678 | } |
| 1679 | m.MatcherSets = append(m.MatcherSets, ms) |
| 1680 | } |
| 1681 | return nil |
| 1682 | } |
| 1683 | |
| 1684 | // Match returns true if r matches m. Since this matcher negates |
| 1685 | // the embedded matchers, false is returned if any of its matcher |
| 1686 | // sets return true. |
| 1687 | func (m MatchNot) Match(r *http.Request) bool { |
| 1688 | match, _ := m.MatchWithError(r) |
| 1689 | return match |
| 1690 | } |
| 1691 | |
| 1692 | // MatchWithError returns true if r matches m. Since this matcher |
| 1693 | // negates the embedded matchers, false is returned if any of its |
| 1694 | // matcher sets return true. |
| 1695 | func (m MatchNot) MatchWithError(r *http.Request) (bool, error) { |
| 1696 | for _, ms := range m.MatcherSets { |
| 1697 | matches, err := ms.MatchWithError(r) |
| 1698 | if err != nil { |
| 1699 | return false, err |
| 1700 | } |
| 1701 | if matches { |
| 1702 | return false, nil |
| 1703 | } |
| 1704 | } |
| 1705 | return true, nil |
| 1706 | } |
| 1707 | |
| 1708 | // MatchRegexp is an embedable type for matching |
| 1709 | // using regular expressions. It adds placeholders |
| 1710 | // to the request's replacer. |
| 1711 | type MatchRegexp struct { |
| 1712 | // A unique name for this regular expression. Optional, |
| 1713 | // but useful to prevent overwriting captures from other |
| 1714 | // regexp matchers. |
| 1715 | Name string `json:"name,omitempty"` |
| 1716 | |
| 1717 | // The regular expression to evaluate, in RE2 syntax, |
| 1718 | // which is the same general syntax used by Go, Perl, |
| 1719 | // and Python. For details, see |
| 1720 | // [Go's regexp package](https://golang.org/pkg/regexp/). |
| 1721 | // Captures are accessible via placeholders. Unnamed |
| 1722 | // capture groups are exposed as their numeric, 1-based |
| 1723 | // index, while named capture groups are available by |
| 1724 | // the capture group name. |
| 1725 | Pattern string `json:"pattern"` |
| 1726 | |
| 1727 | compiled *regexp.Regexp |
| 1728 | } |
| 1729 | |
| 1730 | // Provision compiles the regular expression. |
| 1731 | func (mre *MatchRegexp) Provision(caddy.Context) error { |
| 1732 | re, err := regexp.Compile(mre.Pattern) |
| 1733 | if err != nil { |
| 1734 | return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err) |
| 1735 | } |
| 1736 | mre.compiled = re |
| 1737 | return nil |
| 1738 | } |
| 1739 | |
| 1740 | // Validate ensures mre is set up correctly. |
| 1741 | func (mre *MatchRegexp) Validate() error { |
| 1742 | if mre.Name != "" && !wordRE.MatchString(mre.Name) { |
| 1743 | return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name) |
| 1744 | } |
| 1745 | return nil |
| 1746 | } |
| 1747 | |
| 1748 | // Match returns true if input matches the compiled regular |
| 1749 | // expression in mre. It sets values on the replacer repl |
| 1750 | // associated with capture groups, using the given scope |
| 1751 | // (namespace). |
| 1752 | func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool { |
| 1753 | matches := mre.compiled.FindStringSubmatch(input) |
| 1754 | if matches == nil { |
| 1755 | return false |
| 1756 | } |
| 1757 | |
| 1758 | // save all capture groups, first by index |
| 1759 | for i, match := range matches { |
| 1760 | keySuffix := "." + strconv.Itoa(i) |
| 1761 | if mre.Name != "" { |
| 1762 | repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match) |
| 1763 | } |
| 1764 | repl.Set(regexpPlaceholderPrefix+keySuffix, match) |
| 1765 | } |
| 1766 | |
| 1767 | // then by name |
| 1768 | for i, name := range mre.compiled.SubexpNames() { |
| 1769 | // skip the first element (the full match), and empty names |
| 1770 | if i == 0 || name == "" { |
| 1771 | continue |
| 1772 | } |
| 1773 | |
| 1774 | keySuffix := "." + name |
| 1775 | if mre.Name != "" { |
| 1776 | repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i]) |
| 1777 | } |
| 1778 | repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i]) |
| 1779 | } |
| 1780 | |
| 1781 | return true |
| 1782 | } |
| 1783 | |
| 1784 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1785 | func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1786 | // iterate to merge multiple matchers into one |
| 1787 | for d.Next() { |
| 1788 | // If this is the second iteration of the loop |
| 1789 | // then there's more than one path_regexp matcher |
| 1790 | // and we would end up overwriting the old one |
| 1791 | if mre.Pattern != "" { |
| 1792 | return d.Err("regular expression can only be used once per named matcher") |
| 1793 | } |
| 1794 | |
| 1795 | args := d.RemainingArgs() |
| 1796 | switch len(args) { |
| 1797 | case 1: |
| 1798 | mre.Pattern = args[0] |
| 1799 | case 2: |
| 1800 | mre.Name = args[0] |
| 1801 | mre.Pattern = args[1] |
| 1802 | default: |
| 1803 | return d.ArgErr() |
| 1804 | } |
| 1805 | |
| 1806 | // Default to the named matcher's name, if no regexp name is provided |
| 1807 | if mre.Name == "" { |
| 1808 | mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey) |
| 1809 | } |
| 1810 | |
| 1811 | if d.NextBlock(0) { |
| 1812 | return d.Err("malformed path_regexp matcher: blocks are not supported") |
| 1813 | } |
| 1814 | } |
| 1815 | return nil |
| 1816 | } |
| 1817 | |
| 1818 | // ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested |
| 1819 | // matcher set, and returns its raw module map value. |
| 1820 | func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { |
| 1821 | matcherMap := make(map[string]any) |
| 1822 | |
| 1823 | // in case there are multiple instances of the same matcher, concatenate |
| 1824 | // their tokens (we expect that UnmarshalCaddyfile should be able to |
| 1825 | // handle more than one segment); otherwise, we'd overwrite other |
| 1826 | // instances of the matcher in this set |
| 1827 | tokensByMatcherName := make(map[string][]caddyfile.Token) |
| 1828 | for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { |
| 1829 | // if the token is quoted (backtick), treat it as a shorthand |
| 1830 | // for an expression matcher, same as @named matcher parsing |
| 1831 | if d.Token().Quoted() { |
| 1832 | expressionToken := d.Token().Clone() |
| 1833 | expressionToken.Text = "expression" |
| 1834 | tokensByMatcherName["expression"] = append(tokensByMatcherName["expression"], expressionToken, d.Token()) |
| 1835 | continue |
| 1836 | } |
| 1837 | matcherName := d.Val() |
| 1838 | tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) |
| 1839 | } |
| 1840 | |
| 1841 | for matcherName, tokens := range tokensByMatcherName { |
| 1842 | mod, err := caddy.GetModule("http.matchers." + matcherName) |
| 1843 | if err != nil { |
| 1844 | return nil, d.Errf("getting matcher module '%s': %v", matcherName, err) |
| 1845 | } |
| 1846 | unm, ok := mod.New().(caddyfile.Unmarshaler) |
| 1847 | if !ok { |
| 1848 | return nil, d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) |
| 1849 | } |
| 1850 | err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) |
| 1851 | if err != nil { |
| 1852 | return nil, err |
| 1853 | } |
| 1854 | if rm, ok := unm.(RequestMatcherWithError); ok { |
| 1855 | matcherMap[matcherName] = rm |
| 1856 | continue |
| 1857 | } |
| 1858 | if rm, ok := unm.(RequestMatcher); ok { |
| 1859 | matcherMap[matcherName] = rm |
| 1860 | continue |
| 1861 | } |
| 1862 | return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) |
| 1863 | } |
| 1864 | |
| 1865 | // we should now have a functional matcher, but we also |
| 1866 | // need to be able to marshal as JSON, otherwise config |
| 1867 | // adaptation will be missing the matchers! |
| 1868 | matcherSet := make(caddy.ModuleMap) |
| 1869 | for name, matcher := range matcherMap { |
| 1870 | jsonBytes, err := json.Marshal(matcher) |
| 1871 | if err != nil { |
| 1872 | return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err) |
| 1873 | } |
| 1874 | matcherSet[name] = jsonBytes |
| 1875 | } |
| 1876 | |
| 1877 | return matcherSet, nil |
| 1878 | } |
| 1879 | |
| 1880 | var wordRE = regexp.MustCompile(`\w+`) |
| 1881 | |
| 1882 | const regexpPlaceholderPrefix = "http.regexp" |
| 1883 | |
| 1884 | // MatcherErrorVarKey is the key used for the variable that |
| 1885 | // holds an optional error emitted from a request matcher, |
| 1886 | // to short-circuit the handler chain, since matchers cannot |
| 1887 | // return errors via the RequestMatcher interface. |
| 1888 | // |
| 1889 | // Deprecated: Matchers should implement RequestMatcherWithError |
| 1890 | // which can return an error directly, instead of smuggling it |
| 1891 | // through the vars map. |
| 1892 | const MatcherErrorVarKey = "matchers.error" |
| 1893 | |
| 1894 | // Interface guards |
| 1895 | var ( |
| 1896 | _ RequestMatcherWithError = (*MatchHost)(nil) |
| 1897 | _ caddy.Provisioner = (*MatchHost)(nil) |
| 1898 | _ RequestMatcherWithError = (*MatchPath)(nil) |
| 1899 | _ caddy.Provisioner = (*MatchPath)(nil) |
| 1900 | _ RequestMatcherWithError = (*MatchPathRE)(nil) |
| 1901 | _ caddy.Provisioner = (*MatchPathRE)(nil) |
| 1902 | _ RequestMatcherWithError = (*MatchMethod)(nil) |
| 1903 | _ caddy.Provisioner = (*MatchMethod)(nil) |
| 1904 | _ RequestMatcherWithError = (*MatchQuery)(nil) |
| 1905 | _ RequestMatcherWithError = (*MatchHeader)(nil) |
| 1906 | _ RequestMatcherWithError = (*MatchHeaderRE)(nil) |
| 1907 | _ caddy.Provisioner = (*MatchHeaderRE)(nil) |
| 1908 | _ RequestMatcherWithError = (*MatchProtocol)(nil) |
| 1909 | _ RequestMatcherWithError = (*MatchNot)(nil) |
| 1910 | _ caddy.Provisioner = (*MatchNot)(nil) |
| 1911 | _ caddy.Provisioner = (*MatchRegexp)(nil) |
| 1912 | |
| 1913 | _ caddyfile.Unmarshaler = (*MatchHost)(nil) |
| 1914 | _ caddyfile.Unmarshaler = (*MatchPath)(nil) |
| 1915 | _ caddyfile.Unmarshaler = (*MatchPathRE)(nil) |
| 1916 | _ caddyfile.Unmarshaler = (*MatchMethod)(nil) |
| 1917 | _ caddyfile.Unmarshaler = (*MatchQuery)(nil) |
| 1918 | _ caddyfile.Unmarshaler = (*MatchHeader)(nil) |
| 1919 | _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil) |
| 1920 | _ caddyfile.Unmarshaler = (*MatchProtocol)(nil) |
| 1921 | _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) |
| 1922 | _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil) |
| 1923 | |
| 1924 | _ CELLibraryProducer = (*MatchHost)(nil) |
| 1925 | _ CELLibraryProducer = (*MatchPath)(nil) |
| 1926 | _ CELLibraryProducer = (*MatchPathRE)(nil) |
| 1927 | _ CELLibraryProducer = (*MatchMethod)(nil) |
| 1928 | _ CELLibraryProducer = (*MatchQuery)(nil) |
| 1929 | _ CELLibraryProducer = (*MatchHeader)(nil) |
| 1930 | _ CELLibraryProducer = (*MatchHeaderRE)(nil) |
| 1931 | _ CELLibraryProducer = (*MatchProtocol)(nil) |
| 1932 | _ CELLibraryProducer = (*MatchTLS)(nil) |
| 1933 | _ CELLibraryProducer = (*VarsMatcher)(nil) |
| 1934 | _ CELLibraryProducer = (*MatchVarsRE)(nil) |
| 1935 | |
| 1936 | _ json.Marshaler = (*MatchNot)(nil) |
| 1937 | _ json.Unmarshaler = (*MatchNot)(nil) |
| 1938 | ) |