@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 239 lines 7.24 KB
Raw
1 package caddyhttp
2
3 import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strings"
8
9 "cel.dev/cel-go/cel"
10 "cel.dev/cel-go/common/types/ref"
11 "github.com/dunglas/go-urlpattern"
12
13 "github.com/caddyserver/caddy/v2"
14 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
15 )
16
17 // urlPatternPlaceholderPrefix namespaces placeholders for captured groups.
18 const urlPatternPlaceholderPrefix = "http.url_pattern"
19
20 func init() {
21 caddy.RegisterModule(&MatchURLPattern{})
22 }
23
24 // MatchURLPattern matches requests against a [URLPattern], giving named
25 // groups, wildcards and regexp components beyond what the simpler path
26 // matcher offers.
27 //
28 // [URLPattern]: https://urlpattern.spec.whatwg.org/
29 type MatchURLPattern struct {
30 // Pattern is the URLPattern to match against. A relative pattern (e.g.
31 // "/books/:id") matches the request path on any host; an absolute pattern
32 // (e.g. "https://example.com/books/:id") also constrains scheme and host.
33 Pattern string `json:"pattern,omitempty"`
34
35 // BaseURL resolves a relative Pattern against a fixed origin, scoping the
36 // match to that scheme and host. Leave empty to match any origin.
37 BaseURL string `json:"base_url,omitempty"`
38
39 // IgnoreCase matches the pattern case-insensitively.
40 IgnoreCase bool `json:"ignore_case,omitempty"`
41
42 compiledPattern *urlpattern.URLPattern
43 mergeSlashes bool
44 }
45
46 // CaddyModule returns the Caddy module information.
47 func (*MatchURLPattern) CaddyModule() caddy.ModuleInfo {
48 return caddy.ModuleInfo{
49 ID: "http.matchers.url_pattern",
50 New: func() caddy.Module { return new(MatchURLPattern) },
51 }
52 }
53
54 // Provision compiles the URL pattern.
55 func (m *MatchURLPattern) Provision(_ caddy.Context) error {
56 input := m.Pattern
57
58 // A relative pattern with no base matches any origin: prefix wildcard
59 // protocol, host and port so only the path, search and hash are
60 // constrained. The port wildcard is needed because a request Host may
61 // carry an explicit port. Host-scoped matching stays opt-in via an
62 // absolute pattern or base_url.
63 if m.BaseURL == "" && !strings.Contains(input, "://") {
64 input = "*://*:*" + input
65 }
66
67 p, err := urlpattern.New(input, m.BaseURL, &urlpattern.Options{IgnoreCase: m.IgnoreCase})
68 if err != nil {
69 return fmt.Errorf("unable to parse URL pattern: %w", err)
70 }
71
72 m.compiledPattern = p
73
74 // Like the path matcher, collapse empty path segments in the request
75 // unless the pattern's path itself relies on them.
76 patternPath := m.Pattern
77 if idx := strings.Index(patternPath, "://"); idx != -1 {
78 patternPath = patternPath[idx+3:]
79 if slash := strings.IndexByte(patternPath, '/'); slash != -1 {
80 patternPath = patternPath[slash:]
81 } else {
82 patternPath = ""
83 }
84 }
85 m.mergeSlashes = !strings.Contains(patternPath, "//")
86
87 return nil
88 }
89
90 // Match returns true if the request matches the URL pattern.
91 func (m *MatchURLPattern) Match(r *http.Request) bool {
92 ok, _ := m.MatchWithError(r)
93
94 return ok
95 }
96
97 // MatchWithError returns true if the request matches the URL pattern. The
98 // request's origin (scheme://host) is the base against which the path is
99 // resolved, so an absolute pattern or base_url can match on scheme and host.
100 // The path is matched in its decoded, cleaned form so that the matcher sees
101 // the same path that handlers resolve.
102 //
103 // On a match, captured groups are exposed as placeholders scoped by URL
104 // component, mirroring the URLPattern result object: a named group :id in the
105 // pathname becomes {http.url_pattern.pathname.id}, a group q in the query
106 // becomes {http.url_pattern.search.q}, and so on.
107 func (m *MatchURLPattern) MatchWithError(r *http.Request) (bool, error) {
108 scheme := "http"
109 if r.TLS != nil {
110 scheme = "https"
111 }
112
113 // Match the same path model that handlers resolve: the decoded path,
114 // normalized and cleaned (see #4407). Re-encoding through url.URL yields
115 // a canonical escaped form, so encoded traversal payloads such as
116 // "..%2f" cannot diverge from the path handlers actually use.
117 uri := url.URL{
118 Path: CleanPath(normalizeWindowsPath(r.URL.Path), m.mergeSlashes),
119 RawQuery: r.URL.RawQuery,
120 }
121
122 result := m.compiledPattern.Exec(uri.RequestURI(), scheme+"://"+r.Host)
123 if result == nil {
124 return false, nil
125 }
126
127 if repl, ok := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer); ok {
128 setURLPatternGroups(repl, "protocol", result.Protocol)
129 setURLPatternGroups(repl, "username", result.Username)
130 setURLPatternGroups(repl, "password", result.Password)
131 setURLPatternGroups(repl, "hostname", result.Hostname)
132 setURLPatternGroups(repl, "port", result.Port)
133 setURLPatternGroups(repl, "pathname", result.Pathname)
134 setURLPatternGroups(repl, "search", result.Search)
135 setURLPatternGroups(repl, "hash", result.Hash)
136 }
137
138 return true, nil
139 }
140
141 // setURLPatternGroups publishes a component's captured groups as
142 // {http.url_pattern.<component>.<group>} placeholders.
143 func setURLPatternGroups(repl *caddy.Replacer, component string, c urlpattern.URLPatternComponentResult) {
144 for name, value := range c.Groups {
145 repl.Set(urlPatternPlaceholderPrefix+"."+component+"."+name, value)
146 }
147 }
148
149 // CELLibrary produces options that expose this matcher for use in CEL
150 // expression matchers.
151 //
152 // Example:
153 //
154 // expression url_pattern('/books/:id')
155 // expression url_pattern('/books/:id', 'https://example.com')
156 func (MatchURLPattern) CELLibrary(ctx caddy.Context) (cel.Library, error) {
157 pattern, err := CELMatcherImpl(
158 "url_pattern",
159 "url_pattern_request_string",
160 []*cel.Type{cel.StringType},
161 func(data ref.Val) (RequestMatcherWithError, error) {
162 pattern, ok := data.Value().(string)
163 if !ok {
164 return nil, fmt.Errorf("url_pattern expects a string argument")
165 }
166
167 matcher := MatchURLPattern{Pattern: pattern}
168 err := matcher.Provision(ctx)
169
170 return &matcher, err
171 },
172 )
173 if err != nil {
174 return nil, err
175 }
176
177 patternWithBase, err := CELMatcherImpl(
178 "url_pattern",
179 "url_pattern_request_string_string",
180 []*cel.Type{cel.StringType, cel.StringType},
181 func(data ref.Val) (RequestMatcherWithError, error) {
182 params, err := data.ConvertToNative(stringSliceType)
183 if err != nil {
184 return nil, err
185 }
186 strParams := params.([]string)
187 matcher := MatchURLPattern{Pattern: strParams[0], BaseURL: strParams[1]}
188 err = matcher.Provision(ctx)
189 return &matcher, err
190 },
191 )
192 if err != nil {
193 return nil, err
194 }
195
196 envOpts := append(pattern.CompileOptions(), patternWithBase.CompileOptions()...)
197 prgOpts := append(pattern.ProgramOptions(), patternWithBase.ProgramOptions()...)
198 return NewMatcherCELLibrary(envOpts, prgOpts), nil
199 }
200
201 // UnmarshalCaddyfile implements caddyfile.Unmarshaler. Syntax:
202 //
203 // url_pattern <pattern> {
204 // base_url <url>
205 // ignore_case
206 // }
207 func (m *MatchURLPattern) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
208 for d.Next() {
209 if !d.Args(&m.Pattern) {
210 return d.Err("expected exactly one URL pattern")
211 }
212
213 for d.NextBlock(0) {
214 switch d.Val() {
215 case "base_url":
216 if !d.Args(&m.BaseURL) {
217 return d.ArgErr()
218 }
219 case "ignore_case":
220 if d.NextArg() {
221 return d.ArgErr()
222 }
223 m.IgnoreCase = true
224 default:
225 return d.Errf("unrecognized url_pattern option '%s'", d.Val())
226 }
227 }
228 }
229
230 return nil
231 }
232
233 // Interface guards
234 var (
235 _ RequestMatcherWithError = (*MatchURLPattern)(nil)
236 _ caddy.Provisioner = (*MatchURLPattern)(nil)
237 _ caddyfile.Unmarshaler = (*MatchURLPattern)(nil)
238 _ CELLibraryProducer = (*MatchURLPattern)(nil)
239 )