github.com/goplus/yap@v0.8.1/router.go (about) 1 /* 2 * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package yap 18 19 import ( 20 "net/http" 21 "strings" 22 23 "github.com/goplus/yap/internal/url" 24 ) 25 26 // router is a http rounter which can be used to dispatch requests to different 27 // handler functions via configurable routes 28 type router struct { 29 trees map[string]*node 30 31 // An optional http.Handler that is called on automatic OPTIONS requests. 32 // The handler is only called if HandleOPTIONS is true and no OPTIONS 33 // handler for the specific path was set. 34 // The "Allowed" header is set before calling the handler. 35 GlobalOPTIONS http.Handler 36 37 // Cached value of global (*) allowed methods 38 globalAllowed string 39 40 // Configurable http.Handler which is called when a request 41 // cannot be routed and HandleMethodNotAllowed is true. 42 // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 43 // The "Allow" header with allowed request methods is set before the handler 44 // is called. 45 MethodNotAllowed http.Handler 46 47 // Function to handle panics recovered from http handlers. 48 // It should be used to generate a error page and return the http error code 49 // 500 (Internal Server Error). 50 // The handler can be used to keep your server from crashing because of 51 // unrecovered panics. 52 PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 53 54 // Enables automatic redirection if the current route can't be matched but a 55 // handler for the path with (without) the trailing slash exists. 56 // For example if /foo/ is requested but a route only exists for /foo, the 57 // client is redirected to /foo with http status code 301 for GET requests 58 // and 308 for all other request methods. 59 RedirectTrailingSlash bool 60 61 // If enabled, the router tries to fix the current request path, if no 62 // handle is registered for it. 63 // First superfluous path elements like ../ or // are removed. 64 // Afterwards the router does a case-insensitive lookup of the cleaned path. 65 // If a handle can be found for this route, the router makes a redirection 66 // to the corrected path with status code 301 for GET requests and 308 for 67 // all other request methods. 68 // For example /FOO and /..//Foo could be redirected to /foo. 69 // RedirectTrailingSlash is independent of this option. 70 RedirectFixedPath bool 71 72 // If enabled, the router checks if another method is allowed for the 73 // current route, if the current request can not be routed. 74 // If this is the case, the request is answered with 'Method Not Allowed' 75 // and HTTP status code 405. 76 // If no other Method is allowed, the request is delegated to the NotFound 77 // handler. 78 HandleMethodNotAllowed bool 79 80 // If enabled, the router automatically replies to OPTIONS requests. 81 // Custom OPTIONS handlers take priority over automatic replies. 82 HandleOPTIONS bool 83 } 84 85 func (p *router) init() { 86 p.RedirectTrailingSlash = true 87 p.RedirectFixedPath = true 88 p.HandleMethodNotAllowed = true 89 p.HandleOPTIONS = true 90 } 91 92 // GET is a shortcut for router.Route(http.MethodGet, path, handle) 93 func (p *router) GET(path string, handle func(ctx *Context)) { 94 p.Route(http.MethodGet, path, handle) 95 } 96 97 // HEAD is a shortcut for router.Route(http.MethodHead, path, handle) 98 func (p *router) HEAD(path string, handle func(ctx *Context)) { 99 p.Route(http.MethodHead, path, handle) 100 } 101 102 // OPTIONS is a shortcut for router.Route(http.MethodOptions, path, handle) 103 func (p *router) OPTIONS(path string, handle func(ctx *Context)) { 104 p.Route(http.MethodOptions, path, handle) 105 } 106 107 // POST is a shortcut for router.Route(http.MethodPost, path, handle) 108 func (p *router) POST(path string, handle func(ctx *Context)) { 109 p.Route(http.MethodPost, path, handle) 110 } 111 112 // PUT is a shortcut for router.Route(http.MethodPut, path, handle) 113 func (p *router) PUT(path string, handle func(ctx *Context)) { 114 p.Route(http.MethodPut, path, handle) 115 } 116 117 // PATCH is a shortcut for router.Route(http.MethodPatch, path, handle) 118 func (p *router) PATCH(path string, handle func(ctx *Context)) { 119 p.Route(http.MethodPatch, path, handle) 120 } 121 122 // DELETE is a shortcut for router.Route(http.MethodDelete, path, handle) 123 func (p *router) DELETE(path string, handle func(ctx *Context)) { 124 p.Route(http.MethodDelete, path, handle) 125 } 126 127 // Route registers a new request handle with the given path and method. 128 // 129 // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 130 // functions can be used. 131 // 132 // This function is intended for bulk loading and to allow the usage of less 133 // frequently used, non-standardized or custom methods (e.g. for internal 134 // communication with a proxy). 135 func (p *router) Route(method, path string, handle func(ctx *Context)) { 136 if method == "" { 137 panic("method must not be empty") 138 } 139 if len(path) < 1 || path[0] != '/' { 140 panic("path must begin with '/' in path '" + path + "'") 141 } 142 if handle == nil { 143 panic("handle must not be nil") 144 } 145 146 if p.trees == nil { 147 p.trees = make(map[string]*node) 148 } 149 150 root := p.trees[method] 151 if root == nil { 152 root = new(node) 153 p.trees[method] = root 154 155 p.globalAllowed = p.allowed("*", "") 156 } 157 158 root.addRoute(path, handle) 159 } 160 161 func (p *router) recv(w http.ResponseWriter, req *http.Request) { 162 if rcv := recover(); rcv != nil { 163 p.PanicHandler(w, req, rcv) 164 } 165 } 166 167 func (p *router) allowed(path, reqMethod string) (allow string) { 168 allowed := make([]string, 0, 9) 169 170 if path == "*" { // server-wide 171 // empty method is used for internal calls to refresh the cache 172 if reqMethod == "" { 173 for method := range p.trees { 174 if method == http.MethodOptions { 175 continue 176 } 177 // Route request method to list of allowed methods 178 allowed = append(allowed, method) 179 } 180 } else { 181 return p.globalAllowed 182 } 183 } else { // specific path 184 for method := range p.trees { 185 // Skip the requested method - we already tried this one 186 if method == reqMethod || method == http.MethodOptions { 187 continue 188 } 189 190 handle, _ := p.trees[method].getValue(path, nil) 191 if handle != nil { 192 // Route request method to list of allowed methods 193 allowed = append(allowed, method) 194 } 195 } 196 } 197 198 if len(allowed) > 0 { 199 // Route request method to list of allowed methods 200 allowed = append(allowed, http.MethodOptions) 201 202 // Sort allowed methods. 203 // sort.Strings(allowed) unfortunately causes unnecessary allocations 204 // due to allowed being moved to the heap and interface conversion 205 for i, l := 1, len(allowed); i < l; i++ { 206 for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { 207 allowed[j], allowed[j-1] = allowed[j-1], allowed[j] 208 } 209 } 210 211 // return as comma separated list 212 return strings.Join(allowed, ", ") 213 } 214 215 return allow 216 } 217 218 func (p *router) serveHTTP(w http.ResponseWriter, req *http.Request, e *Engine) { 219 if p.PanicHandler != nil { 220 defer p.recv(w, req) 221 } 222 223 path := req.URL.Path 224 root := p.trees[req.Method] 225 if root != nil { 226 ctx := e.NewContext(w, req) 227 if handle, tsr := root.getValue(path, ctx); handle != nil { 228 handle(ctx) 229 return 230 } else if req.Method != http.MethodConnect && path != "/" { 231 // Moved Permanently, request with GET method 232 code := http.StatusMovedPermanently 233 if req.Method != http.MethodGet { 234 // Permanent Redirect, request with same method 235 code = http.StatusPermanentRedirect 236 } 237 238 if tsr && p.RedirectTrailingSlash { 239 if len(path) > 1 && path[len(path)-1] == '/' { 240 req.URL.Path = path[:len(path)-1] 241 } else { 242 req.URL.Path = path + "/" 243 } 244 http.Redirect(w, req, req.URL.String(), code) 245 return 246 } 247 248 // Try to fix the request path 249 if p.RedirectFixedPath { 250 fixedPath, found := root.findCaseInsensitivePath( 251 url.CleanPath(path), 252 p.RedirectTrailingSlash, 253 ) 254 if found { 255 req.URL.Path = fixedPath 256 http.Redirect(w, req, req.URL.String(), code) 257 return 258 } 259 } 260 } 261 } else if req.Method == http.MethodHead { 262 p.head(w, req, e) 263 return 264 } 265 266 if req.Method == http.MethodOptions && p.HandleOPTIONS { 267 // Route OPTIONS requests 268 if allow := p.allowed(path, http.MethodOptions); allow != "" { 269 w.Header().Set("Allow", allow) 270 if p.GlobalOPTIONS != nil { 271 p.GlobalOPTIONS.ServeHTTP(w, req) 272 } 273 return 274 } 275 } else if p.HandleMethodNotAllowed { // Route 405 276 if allow := p.allowed(path, req.Method); allow != "" { 277 w.Header().Set("Allow", allow) 278 if p.MethodNotAllowed != nil { 279 p.MethodNotAllowed.ServeHTTP(w, req) 280 } else { 281 http.Error(w, 282 http.StatusText(http.StatusMethodNotAllowed), 283 http.StatusMethodNotAllowed, 284 ) 285 } 286 return 287 } 288 } 289 290 e.Mux.ServeHTTP(w, req) 291 } 292 293 func (p *router) head(w http.ResponseWriter, req *http.Request, e *Engine) { 294 req.Method = http.MethodGet 295 p.serveHTTP(&headWriter{w}, req, e) 296 } 297 298 type headWriter struct { 299 http.ResponseWriter 300 } 301 302 func (p *headWriter) Write(b []byte) (int, error) { 303 return len(b), nil 304 }