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  }