harness-drone/vendor/github.com/dimfeld/httptreemux/router.go
2017-07-31 15:15:05 -04:00

290 lines
9.5 KiB
Go

// This is inspired by Julien Schmidt's httprouter, in that it uses a patricia tree, but the
// implementation is rather different. Specifically, the routing rules are relaxed so that a
// single path segment may be a wildcard in one route and a static token in another. This gives a
// nice combination of high performance with a lot of convenience in designing the routing patterns.
package httptreemux
import (
"fmt"
"net/http"
"net/url"
)
// The params argument contains the parameters parsed from wildcards and catch-alls in the URL.
type HandlerFunc func(http.ResponseWriter, *http.Request, map[string]string)
type PanicHandler func(http.ResponseWriter, *http.Request, interface{})
// RedirectBehavior sets the behavior when the router redirects the request to the
// canonical version of the requested URL using RedirectTrailingSlash or RedirectClean.
// The default behavior is to return a 301 status, redirecting the browser to the version
// of the URL that matches the given pattern.
//
// On a POST request, most browsers that receive a 301 will submit a GET request to
// the redirected URL, meaning that any data will likely be lost. If you want to handle
// and avoid this behavior, you may use Redirect307, which causes most browsers to
// resubmit the request using the original method and request body.
//
// Since 307 is supposed to be a temporary redirect, the new 308 status code has been
// proposed, which is treated the same, except it indicates correctly that the redirection
// is permanent. The big caveat here is that the RFC is relatively recent, and older
// browsers will not know what to do with it. Therefore its use is not recommended
// unless you really know what you're doing.
//
// Finally, the UseHandler value will simply call the handler function for the pattern.
type RedirectBehavior int
type PathSource int
const (
Redirect301 RedirectBehavior = iota // Return 301 Moved Permanently
Redirect307 // Return 307 HTTP/1.1 Temporary Redirect
Redirect308 // Return a 308 RFC7538 Permanent Redirect
UseHandler // Just call the handler function
RequestURI PathSource = iota // Use r.RequestURI
URLPath // Use r.URL.Path
)
// LookupResult contains information about a route lookup, which is returned from Lookup and
// can be passed to ServeLookupResult if the request should be served.
type LookupResult struct {
// StatusCode informs the caller about the result of the lookup.
// This will generally be `http.StatusNotFound` or `http.StatusMethodNotAllowed` for an
// error case. On a normal success, the statusCode will be `http.StatusOK`. A redirect code
// will also be used in the case
StatusCode int
handler HandlerFunc
params map[string]string
leafHandler map[string]HandlerFunc // Only has a value when StatusCode is MethodNotAllowed.
}
// Dump returns a text representation of the routing tree.
func (t *TreeMux) Dump() string {
return t.root.dumpTree("", "")
}
func (t *TreeMux) serveHTTPPanic(w http.ResponseWriter, r *http.Request) {
if err := recover(); err != nil {
t.PanicHandler(w, r, err)
}
}
func (t *TreeMux) redirectStatusCode(method string) (int, bool) {
var behavior RedirectBehavior
var ok bool
if behavior, ok = t.RedirectMethodBehavior[method]; !ok {
behavior = t.RedirectBehavior
}
switch behavior {
case Redirect301:
return http.StatusMovedPermanently, true
case Redirect307:
return http.StatusTemporaryRedirect, true
case Redirect308:
// Go doesn't have a constant for this yet. Yet another sign
// that you probably shouldn't use it.
return 308, true
case UseHandler:
return 0, false
default:
return http.StatusMovedPermanently, true
}
}
func redirectHandler(newPath string, statusCode int) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, params map[string]string) {
redirect(w, r, newPath, statusCode)
}
}
func redirect(w http.ResponseWriter, r *http.Request, newPath string, statusCode int) {
newURL := url.URL{
Path: newPath,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
}
http.Redirect(w, r, newURL.String(), statusCode)
}
func (t *TreeMux) lookup(w http.ResponseWriter, r *http.Request) (result LookupResult, found bool) {
result.StatusCode = http.StatusNotFound
path := r.RequestURI
pathLen := len(path)
if pathLen > 0 && t.PathSource == RequestURI {
rawQueryLen := len(r.URL.RawQuery)
if rawQueryLen != 0 || path[pathLen-1] == '?' {
// Remove any query string and the ?.
path = path[:pathLen-rawQueryLen-1]
pathLen = len(path)
}
} else {
// In testing with http.NewRequest,
// RequestURI is not set so just grab URL.Path instead.
path = r.URL.Path
pathLen = len(path)
}
trailingSlash := path[pathLen-1] == '/' && pathLen > 1
if trailingSlash && t.RedirectTrailingSlash {
path = path[:pathLen-1]
}
n, handler, params := t.root.search(r.Method, path[1:])
if n == nil {
if t.RedirectCleanPath {
// Path was not found. Try cleaning it up and search again.
// TODO Test this
cleanPath := Clean(path)
n, handler, params = t.root.search(r.Method, cleanPath[1:])
if n == nil {
// Still nothing found.
return
}
if statusCode, ok := t.redirectStatusCode(r.Method); ok {
// Redirect to the actual path
return LookupResult{statusCode, redirectHandler(cleanPath, statusCode), nil, nil}, true
}
} else {
// Not found.
return
}
}
if handler == nil {
if r.Method == "OPTIONS" && t.OptionsHandler != nil {
handler = t.OptionsHandler
}
if handler == nil {
result.leafHandler = n.leafHandler
result.StatusCode = http.StatusMethodNotAllowed
return
}
}
if !n.isCatchAll || t.RemoveCatchAllTrailingSlash {
if trailingSlash != n.addSlash && t.RedirectTrailingSlash {
if statusCode, ok := t.redirectStatusCode(r.Method); ok {
var h HandlerFunc
if n.addSlash {
// Need to add a slash.
h = redirectHandler(path+"/", statusCode)
} else if path != "/" {
// We need to remove the slash. This was already done at the
// beginning of the function.
h = redirectHandler(path, statusCode)
}
if h != nil {
return LookupResult{statusCode, h, nil, nil}, true
}
}
}
}
var paramMap map[string]string
if len(params) != 0 {
if len(params) != len(n.leafWildcardNames) {
// Need better behavior here. Should this be a panic?
panic(fmt.Sprintf("httptreemux parameter list length mismatch: %v, %v",
params, n.leafWildcardNames))
}
paramMap = make(map[string]string)
numParams := len(params)
for index := 0; index < numParams; index++ {
paramMap[n.leafWildcardNames[numParams-index-1]] = params[index]
}
}
return LookupResult{http.StatusOK, handler, paramMap, nil}, true
}
// Lookup performs a lookup without actually serving the request or mutating the request or response.
// The return values are a LookupResult and a boolean. The boolean will be true when a handler
// was found or the lookup resulted in a redirect which will point to a real handler. It is false
// for requests which would result in a `StatusNotFound` or `StatusMethodNotAllowed`.
//
// Regardless of the returned boolean's value, the LookupResult may be passed to ServeLookupResult
// to be served appropriately.
func (t *TreeMux) Lookup(w http.ResponseWriter, r *http.Request) (LookupResult, bool) {
if t.SafeAddRoutesWhileRunning {
// In concurrency safe mode, we acquire a read lock on the mutex for any access.
// This is optional to avoid potential performance loss in high-usage scenarios.
t.mutex.RLock()
}
result, found := t.lookup(w, r)
if t.SafeAddRoutesWhileRunning {
t.mutex.RUnlock()
}
return result, found
}
// ServeLookupResult serves a request, given a lookup result from the Lookup function.
func (t *TreeMux) ServeLookupResult(w http.ResponseWriter, r *http.Request, lr LookupResult) {
if lr.handler == nil {
if lr.StatusCode == http.StatusMethodNotAllowed && lr.leafHandler != nil {
t.MethodNotAllowedHandler(w, r, lr.leafHandler)
} else {
t.NotFoundHandler(w, r)
}
} else {
r = t.setDefaultRequestContext(r)
lr.handler(w, r, lr.params)
}
}
func (t *TreeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if t.PanicHandler != nil {
defer t.serveHTTPPanic(w, r)
}
if t.SafeAddRoutesWhileRunning {
// In concurrency safe mode, we acquire a read lock on the mutex for any access.
// This is optional to avoid potential performance loss in high-usage scenarios.
t.mutex.RLock()
}
result, _ := t.lookup(w, r)
if t.SafeAddRoutesWhileRunning {
t.mutex.RUnlock()
}
t.ServeLookupResult(w, r, result)
}
// MethodNotAllowedHandler is the default handler for TreeMux.MethodNotAllowedHandler,
// which is called for patterns that match, but do not have a handler installed for the
// requested method. It simply writes the status code http.StatusMethodNotAllowed and fills
// in the `Allow` header value appropriately.
func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request,
methods map[string]HandlerFunc) {
for m := range methods {
w.Header().Add("Allow", m)
}
w.WriteHeader(http.StatusMethodNotAllowed)
}
func New() *TreeMux {
tm := &TreeMux{
root: &node{path: "/"},
NotFoundHandler: http.NotFound,
MethodNotAllowedHandler: MethodNotAllowedHandler,
HeadCanUseGet: true,
RedirectTrailingSlash: true,
RedirectCleanPath: true,
RedirectBehavior: Redirect301,
RedirectMethodBehavior: make(map[string]RedirectBehavior),
PathSource: RequestURI,
EscapeAddedRoutes: false,
}
tm.Group.mux = tm
return tm
}