-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathrouting.go
358 lines (317 loc) · 9.88 KB
/
routing.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/
package restapi
import (
"encoding/json"
"fmt"
"net/http"
"path"
"regexp"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// RoutePath represents route's path.
type RoutePath struct {
Raw string
NormalizedPath string
RegExpPath *regexp.Regexp
ExactMatch bool
ForwardMatch bool
}
// ParseRoutePath parses string representation of route's path.
// Syntax: [ = | ~ | ^~ ] urlPath
// Semantic for modifier is used the same as in Nginx (https://nginx.org/en/docs/http/ngx_http_core_module.html#location).
func ParseRoutePath(rp string) (RoutePath, error) {
rp = strings.TrimSpace(rp)
if rp == "" {
return RoutePath{}, fmt.Errorf("path is missing")
}
switch {
case strings.HasPrefix(rp, "="):
p := strings.TrimSpace(rp[1:])
if !strings.HasPrefix(p, "/") {
return RoutePath{}, fmt.Errorf("path should be started with \"/\" in case of exact matching")
}
return RoutePath{Raw: rp, NormalizedPath: NormalizeURLPath(p), ExactMatch: true}, nil
case strings.HasPrefix(rp, "^~"):
p := strings.TrimSpace(rp[2:])
if !strings.HasPrefix(p, "/") {
return RoutePath{}, fmt.Errorf("path should be started with \"/\" in case of forward matching")
}
return RoutePath{Raw: rp, NormalizedPath: NormalizeURLPath(p), ForwardMatch: true}, nil
case strings.HasPrefix(rp, "~"):
p := strings.TrimSpace(rp[1:])
if p == "" {
return RoutePath{}, fmt.Errorf("regular expression is missing")
}
re, err := regexp.Compile(p)
if err != nil {
return RoutePath{}, err
}
return RoutePath{Raw: rp, RegExpPath: re}, nil
}
if !strings.HasPrefix(rp, "/") {
return RoutePath{}, fmt.Errorf("path should be started with \"/\" in case of prefixed matching")
}
return RoutePath{Raw: rp, NormalizedPath: NormalizeURLPath(rp)}, nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (rp *RoutePath) UnmarshalText(text []byte) (err error) {
*rp, err = ParseRoutePath(string(text))
return
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (rp *RoutePath) UnmarshalYAML(value *yaml.Node) error {
var text string
if err := value.Decode(&text); err != nil {
return err
}
parsed, err := ParseRoutePath(text)
if err != nil {
return err
}
*rp = parsed
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (rp *RoutePath) UnmarshalJSON(data []byte) error {
var text string
if err := json.Unmarshal(data, &text); err != nil {
return err
}
parsed, err := ParseRoutePath(text)
if err != nil {
return err
}
*rp = parsed
return nil
}
// Route represents route for handling.
type Route struct {
Path RoutePath
Methods []string
Handler http.Handler
Middlewares []func(http.Handler) http.Handler
Excluded bool // Set to true for routes that are matched to be excluded.
}
// NewRoute returns a new route.
func NewRoute(cfg RouteConfig, handler http.Handler, middlewares []func(http.Handler) http.Handler) Route {
return Route{
Path: cfg.Path,
Methods: cfg.MethodsInUpperCase(),
Handler: handler,
Middlewares: middlewares,
}
}
// NewExcludedRoute returns a new route that will be used as exclusion in matching.
func NewExcludedRoute(cfg RouteConfig) Route {
return Route{
Path: cfg.Path,
Methods: cfg.MethodsInUpperCase(),
Excluded: true,
}
}
// RoutesManager contains routes for handling and allows to search among them.
type RoutesManager struct {
exactRoutes map[string][]Route
descSortedPrefixedRoutes []Route
regExpRoutes []Route
}
// NewRoutesManager create new RoutesManager.
func NewRoutesManager(routes []Route) *RoutesManager {
exactRoutes := make(map[string][]Route)
var prefixedRoutes []Route
var regExpRoutes []Route
for _, route := range routes {
switch {
case route.Path.ExactMatch:
exactRoutes[route.Path.NormalizedPath] = append(exactRoutes[route.Path.NormalizedPath], route)
case route.Path.RegExpPath != nil:
regExpRoutes = append(regExpRoutes, route)
default:
prefixedRoutes = append(prefixedRoutes, route)
}
}
// For further searching in each slice first must go routes for which methods are specified.
// That's why we sort all slices here.
for i := range exactRoutes {
pathRoutes := exactRoutes[i]
sort.SliceStable(pathRoutes, func(i, j int) bool {
return len(pathRoutes[i].Methods) != 0 && len(pathRoutes[j].Methods) == 0
})
}
// Sort prefixed routes in desc order.
sort.SliceStable(prefixedRoutes, func(i, j int) bool {
if prefixedRoutes[i].Path.NormalizedPath == prefixedRoutes[j].Path.NormalizedPath {
return len(prefixedRoutes[i].Methods) != 0 && len(prefixedRoutes[j].Methods) == 0
}
return prefixedRoutes[i].Path.NormalizedPath > prefixedRoutes[j].Path.NormalizedPath
})
sort.SliceStable(regExpRoutes, func(i, j int) bool {
return len(regExpRoutes[i].Methods) != 0 && len(regExpRoutes[j].Methods) == 0
})
return &RoutesManager{exactRoutes, prefixedRoutes, regExpRoutes}
}
// SearchMatchedRouteForRequest searches Route that matches the passing http.Request.
// Algorithm is the same as used in Nginx for locations matching (https://nginx.org/en/docs/http/ngx_http_core_module.html#location).
// Excluded routes has priority.
func (r *RoutesManager) SearchMatchedRouteForRequest(req *http.Request) (Route, bool) {
normalizedReqURLPath := NormalizeURLPath(req.URL.Path)
if r, ok := r.SearchRoute(normalizedReqURLPath, req.Method, true); ok {
return r, false
}
return r.SearchRoute(normalizedReqURLPath, req.Method, false)
}
// SearchRoute searches Route by passed path and method.
// Path should be normalized (see NormalizeURLPath for this).
// If the excluded arg is true, search will be done only among excluded routes. If false - only among included routes.
// nolint:gocyclo
func (r *RoutesManager) SearchRoute(normalizedPath string, method string, excluded bool) (Route, bool) {
reqMethodMatchesRoute := func(route *Route) bool {
if len(route.Methods) == 0 {
return true
}
for i := range route.Methods {
if route.Methods[i] == method {
return true
}
}
return false
}
if exactRoutes, ok := r.exactRoutes[normalizedPath]; ok {
for i := range exactRoutes {
if exactRoutes[i].Excluded == excluded && reqMethodMatchesRoute(&exactRoutes[i]) {
return exactRoutes[i], true
}
}
}
var longestPrefixedRoute *Route
for i := range r.descSortedPrefixedRoutes {
match := strings.HasPrefix(normalizedPath, r.descSortedPrefixedRoutes[i].Path.NormalizedPath) &&
r.descSortedPrefixedRoutes[i].Excluded == excluded &&
reqMethodMatchesRoute(&r.descSortedPrefixedRoutes[i])
if match {
longestPrefixedRoute = &r.descSortedPrefixedRoutes[i]
break
}
}
if longestPrefixedRoute != nil && longestPrefixedRoute.Path.ForwardMatch {
return *longestPrefixedRoute, true
}
for i := range r.regExpRoutes {
match := r.regExpRoutes[i].Path.RegExpPath.MatchString(normalizedPath) &&
r.regExpRoutes[i].Excluded == excluded &&
reqMethodMatchesRoute(&r.regExpRoutes[i])
if match {
return r.regExpRoutes[i], true
}
}
if longestPrefixedRoute != nil {
return *longestPrefixedRoute, true
}
return Route{}, false
}
// RouteConfig represents route's configuration.
type RouteConfig struct {
// Path is a struct that contains info about route path.
// ParseRoutePath function should be used for constructing it from the string representation.
Path RoutePath `mapstructure:"path" yaml:"path" json:"path"`
// Methods is a list of case-insensitive HTTP verbs/methods.
Methods MethodsList `mapstructure:"methods" yaml:"methods" json:"methods"`
}
// MethodsInUpperCase returns list of route's methods in upper-case.
func (r *RouteConfig) MethodsInUpperCase() []string {
upperMethods := make([]string, 0, len(r.Methods))
for _, m := range r.Methods {
upperMethods = append(upperMethods, strings.ToUpper(m))
}
return upperMethods
}
var availableHTTPMethods = []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
http.MethodOptions,
http.MethodTrace,
}
// Validate validates RouteConfig
func (r *RouteConfig) Validate() error {
validateMethod := func(method string) error {
for _, am := range availableHTTPMethods {
if method == am {
return nil
}
}
return fmt.Errorf("unknown method %q", method)
}
if r.Path.Raw == "" {
return fmt.Errorf("path is missing")
}
for _, method := range r.MethodsInUpperCase() {
if err := validateMethod(method); err != nil {
return err
}
}
return nil
}
// NormalizeURLPath normalizes URL path (i.e. for example, it convert /foo///bar/.. to /foo).
func NormalizeURLPath(urlPath string) string {
res := path.Clean("/" + urlPath)
if strings.HasSuffix(urlPath, "/") && res != "/" {
res += "/"
}
return res
}
// MethodsList represents a list of HTTP methods.
type MethodsList []string
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (ml *MethodsList) UnmarshalText(text []byte) error {
ml.unmarshal(string(text))
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ml *MethodsList) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
ml.unmarshal(s)
return nil
}
var l []string
if err := json.Unmarshal(data, &l); err == nil {
*ml = l
return nil
}
return fmt.Errorf("invalid methods list: %s", data)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (ml *MethodsList) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err == nil {
ml.unmarshal(s)
return nil
}
var l []string
if err := value.Decode(&l); err == nil {
*ml = l
return nil
}
return fmt.Errorf("invalid methods list: %v", value)
}
func (ml *MethodsList) unmarshal(data string) {
data = strings.TrimSpace(data)
if data == "" {
*ml = MethodsList{}
return
}
methods := strings.Split(data, ",")
for _, m := range methods {
*ml = append(*ml, strings.TrimSpace(m))
}
}