-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
router.go
348 lines (295 loc) · 11.2 KB
/
router.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
package router
import (
"errors"
"net/http"
"sort"
"strings"
"sync"
"time"
"github.com/kataras/iris/v12/context"
"github.com/schollz/closestmatch"
)
// Router is the "director".
// Caller should provide a request handler (router implementation or root handler).
// Router is responsible to build the received request handler and run it
// to serve requests, based on the received context.Pool.
//
// User can refresh the router with `RefreshRouter` whenever a route's field is changed by him.
type Router struct {
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter,
requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible
wrapperFunc WrapperFunc
// wrappers to be built on BuildRouter state,
// first is executed first at this case.
// Case:
// - SubdomainRedirect on user call, registers a wrapper, on design state
// - i18n,if loaded and Subdomain or PathRedirect is true, registers a wrapper too, on build state
// the SubdomainRedirect should be the first(subdomainWrap(i18nWrap)) wrapper
// to be executed instead of last(i18nWrap(subdomainWrap)).
wrapperFuncs []WrapperFunc
cPool *context.Pool // used on RefreshRouter
routesProvider RoutesProvider
// key = subdomain
// value = closest of static routes, filled on `BuildRouter/RefreshRouter`.
closestPaths map[string]*closestmatch.ClosestMatch
}
// NewRouter returns a new empty Router.
func NewRouter() *Router {
return &Router{}
}
// RefreshRouter re-builds the router. Should be called when a route's state
// changed (i.e Method changed at serve-time).
//
// Note that in order to use RefreshRouter while in serve-time,
// you have to set the `EnableDynamicHandler` Iris Application setting to true,
// e.g. `app.Listen(":8080", iris.WithEnableDynamicHandler)`
func (router *Router) RefreshRouter() error {
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true)
}
// AddRouteUnsafe adds a route directly to the router's request handler.
// Works before or after Build state.
// Mainly used for internal cases like `iris.WithSitemap`.
// Do NOT use it on serve-time.
func (router *Router) AddRouteUnsafe(routes ...*Route) error {
if h := router.requestHandler; h != nil {
if v, ok := h.(RouteAdder); ok {
for _, r := range routes {
return v.AddRoute(r)
}
}
}
return ErrNotRouteAdder
}
// FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
//
// Order may change.
func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []string {
if router.closestPaths == nil {
return nil
}
cm, ok := router.closestPaths[subdomain]
if !ok {
return nil
}
list := cm.ClosestN(searchPath, n)
if len(list) == 1 && list[0] == "" {
// yes, it may return empty string as its first slice element when not found.
return nil
}
return list
}
func (router *Router) buildMainHandler(cPool *context.Pool, requestHandler RequestHandler) {
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
router.requestHandler.HandleRequest(ctx)
cPool.Release(ctx)
}
}
func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filter, cPool *context.Pool, requestHandler RequestHandler) {
sortedFilters := make([]*Filter, 0, len(routerFilters))
// key was just there to enforce uniqueness on API level.
for _, f := range routerFilters {
sortedFilters = append(sortedFilters, f)
// append it as one handlers so execution rules are being respected in that step too.
f.Handlers = append(f.Handlers, func(ctx *context.Context) {
// set the handler index back to 0 so the route's handlers can be executed as expected.
ctx.HandlerIndex(0)
// execute the main request handler, this will fire the found route's handlers
// or if error the error code's associated handler.
router.requestHandler.HandleRequest(ctx)
})
}
sort.SliceStable(sortedFilters, func(i, j int) bool {
left, right := sortedFilters[i], sortedFilters[j]
var (
leftSubLen = len(left.Subdomain)
rightSubLen = len(right.Subdomain)
leftSlashLen = strings.Count(left.Path, "/")
rightSlashLen = strings.Count(right.Path, "/")
)
if leftSubLen == rightSubLen {
if leftSlashLen > rightSlashLen {
return true
}
}
if leftSubLen > rightSubLen {
return true
}
if leftSlashLen > rightSlashLen {
return true
}
if leftSlashLen == rightSlashLen {
return len(left.Path) > len(right.Path)
}
return len(left.Path) > len(right.Path)
})
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
filterExecuted := false
for _, f := range sortedFilters { // from subdomain, largest path to shortest.
// fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path)
if f.Matcher.Match(ctx) {
// fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers))
filterExecuted = true
// execute the final handlers chain.
ctx.Do(f.Handlers)
break // and break on first found.
}
}
if !filterExecuted {
// If not at least one match filter found and executed,
// then just run the router.
router.requestHandler.HandleRequest(ctx)
}
cPool.Release(ctx)
}
}
// BuildRouter builds the router based on
// the context factory (explicit pool in this case),
// the request handler which manages how the main handler will multiplexes the routes
// provided by the third parameter, routerProvider (it's the api builder in this case) and
// its wrapper.
//
// Use of RefreshRouter to re-build the router if needed.
func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider, force bool) error {
if requestHandler == nil {
return errors.New("router: request handler is nil")
}
if cPool == nil {
return errors.New("router: context pool is nil")
}
// build the handler using the routesProvider
if err := requestHandler.Build(routesProvider); err != nil {
return err
}
router.mu.Lock()
defer router.mu.Unlock()
// store these for RefreshRouter's needs.
if force {
router.cPool = cPool
router.requestHandler = requestHandler
router.routesProvider = routesProvider
} else {
if router.cPool == nil {
router.cPool = cPool
}
if router.requestHandler == nil {
router.requestHandler = requestHandler
}
if router.routesProvider == nil && routesProvider != nil {
router.routesProvider = routesProvider
}
}
// the important stuff.
if routerFilters := routesProvider.GetRouterFilters(); len(routerFilters) > 0 {
router.buildMainHandlerWithFilters(routerFilters, cPool, requestHandler)
} else {
router.buildMainHandler(cPool, requestHandler)
}
for i := len(router.wrapperFuncs) - 1; i >= 0; i-- {
w := router.wrapperFuncs[i]
if w == nil {
continue
}
router.WrapRouter(w)
}
if router.wrapperFunc != nil { // if wrapper used then attach that as the router service
router.mainHandler = newWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
}
// build closest.
subdomainPaths := make(map[string][]string)
for _, r := range router.routesProvider.GetRoutes() {
if !r.IsStatic() {
continue
}
subdomainPaths[r.Subdomain] = append(subdomainPaths[r.Subdomain], r.Path)
}
router.closestPaths = make(map[string]*closestmatch.ClosestMatch)
for subdomain, paths := range subdomainPaths {
router.closestPaths[subdomain] = closestmatch.New(paths, []int{3, 4, 6})
}
return nil
}
// Downgrade "downgrades", alters the router supervisor service(Router.mainHandler)
// algorithm to a custom one,
// be aware to change the global variables of 'ParamStart' and 'ParamWildcardStart'.
// can be used to implement a custom proxy or
// a custom router which should work with raw ResponseWriter, *Request
// instead of the Context(which again, can be retrieved by the Framework's context pool).
//
// Note: Downgrade will by-pass the Wrapper, the caller is responsible for everything.
// Downgrade is thread-safe.
func (router *Router) Downgrade(newMainHandler http.HandlerFunc) {
router.mu.Lock()
router.mainHandler = newMainHandler
router.mu.Unlock()
}
// Downgraded returns true if this router is downgraded.
func (router *Router) Downgraded() bool {
return router.mainHandler != nil && router.requestHandler == nil
}
// SetTimeoutHandler overrides the main handler with a timeout handler.
//
// TimeoutHandler supports the Pusher interface but does not support
// the Hijacker or Flusher interfaces.
//
// All previous registered wrappers and middlewares are still executed as expected.
func (router *Router) SetTimeoutHandler(timeout time.Duration, msg string) {
if timeout <= 0 {
return
}
mainHandler := router.mainHandler
h := func(w http.ResponseWriter, r *http.Request) {
mainHandler(w, r)
}
router.mainHandler = http.TimeoutHandler(http.HandlerFunc(h), timeout, msg).ServeHTTP
}
// WrapRouter adds a wrapper on the top of the main router.
// Usually it's useful for third-party middleware
// when need to wrap the entire application with a middleware like CORS.
//
// Developers can add more than one wrappers,
// those wrappers' execution comes from last to first.
// That means that the second wrapper will wrap the first, and so on.
//
// Before build.
func (router *Router) WrapRouter(wrapperFunc WrapperFunc) {
// logger := context.DefaultLogger("router wrapper")
// file, line := context.HandlerFileLineRel(wrapperFunc)
// if router.wrapperFunc != nil {
// wrappedFile, wrappedLine := context.HandlerFileLineRel(router.wrapperFunc)
// logger.Infof("%s:%d wraps %s:%d", file, line, wrappedFile, wrappedLine)
// } else {
// logger.Infof("%s:%d wraps the main router", file, line)
// }
router.wrapperFunc = makeWrapperFunc(router.wrapperFunc, wrapperFunc)
}
// AddRouterWrapper adds a router wrapper.
// Unlike `WrapRouter` the first registered will be executed first
// so a wrapper wraps its next not the previous one.
// it defers the wrapping until the `BuildRouter`.
// Redirection wrappers should be added using this method
// e.g. SubdomainRedirect.
func (router *Router) AddRouterWrapper(wrapperFunc WrapperFunc) {
router.wrapperFuncs = append(router.wrapperFuncs, wrapperFunc)
}
// PrependRouterWrapper like `AddRouterWrapper` but this wrapperFunc
// will always be executed before the previous `AddRouterWrapper`.
// Path form (no modification) wrappers should be added using this method
// e.g. ForceLowercaseRouting.
func (router *Router) PrependRouterWrapper(wrapperFunc WrapperFunc) {
router.wrapperFuncs = append([]WrapperFunc{wrapperFunc}, router.wrapperFuncs...)
}
// ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper.
func (router *Router) ServeHTTPC(ctx *context.Context) {
router.requestHandler.HandleRequest(ctx)
}
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
router.mainHandler(w, r)
}
// RouteExists reports whether a particular route exists
// It will search from the current subdomain of context's host, if not inside the root domain.
func (router *Router) RouteExists(ctx *context.Context, method, path string) bool {
return router.requestHandler.RouteExists(ctx, method, path)
}