Merge remote-tracking branch 'da-z/patch-1' into chaining
diff --git a/.travis.yml b/.travis.yml index b0cf782..814710d 100644 --- a/.travis.yml +++ b/.travis.yml
@@ -3,4 +3,5 @@ - 1.1 - 1.2 - 1.3 + - 1.4 - tip
diff --git a/README.md b/README.md index 25201e1..f7ca0a3 100644 --- a/README.md +++ b/README.md
@@ -196,8 +196,9 @@ Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/), it's very easy! -Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks-building-upon-httprouter). +Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks--co-based-on-httprouter). +### Multi-domain / Sub-domains Here is a quick example: Does your server serve multiple domains / hosts? You want to use sub-domains? Define a router per host! @@ -235,7 +236,69 @@ } ``` -## Web Frameworks building upon HttpRouter +### Basic Authentication +Another quick example: Basic Authentification (RFC 2617) for handles: + +```go +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "github.com/julienschmidt/httprouter" + "net/http" + "log" + "strings" +) + +func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + const basicAuthPrefix string = "Basic " + + // Get the Basic Authentication credentials + auth := r.Header.Get("Authorization") + if strings.HasPrefix(auth, basicAuthPrefix) { + // Check credentials + payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):]) + if err == nil { + pair := bytes.SplitN(payload, []byte(":"), 2) + if len(pair) == 2 && bytes.Equal(pair[0], user) && bytes.Equal(pair[1], pass) { + // Delegate request to the given handle + h(w, r, ps) + return + } + } + } + + // Request Basic Authentication otherwise + w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } +} + +func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + fmt.Fprint(w, "Not protected!\n") +} + +func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + fmt.Fprint(w, "Protected!\n") +} + +func main() { + user := []byte("gordon") + pass := []byte("secret!") + + router := httprouter.New() + router.GET("/", Index) + router.GET("/protected/", BasicAuth(Protected, user, pass)) + + log.Fatal(http.ListenAndServe(":8080", router)) +} +``` + +## Web Frameworks & Co based on HttpRouter If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine +* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow
diff --git a/router.go b/router.go index 3f3d163..ac3d680 100644 --- a/router.go +++ b/router.go
@@ -123,13 +123,21 @@ // handle is registered for it. // First superfluous path elements like ../ or // are removed. // Afterwards the router does a case-insensitive lookup of the cleaned path. - // If a handle can be found for this route, the router makes a redirection + // If a handle can be found for this route, the router makes a redirection // to the corrected path with status code 301 for GET requests and 307 for // all other request methods. // For example /FOO and /..//Foo could be redirected to /foo. // RedirectTrailingSlash is independent of this option. RedirectFixedPath bool + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool + // Configurable http.HandlerFunc which is called when no matching route is // found. If it is not set, http.NotFound is used. NotFound http.HandlerFunc @@ -149,8 +157,9 @@ // Path auto-correction, including trailing slashes, is enabled by default. func New() *Router { return &Router{ - RedirectTrailingSlash: true, - RedirectFixedPath: true, + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, } } @@ -159,6 +168,11 @@ r.Handle("GET", path, handle) } +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func (r *Router) HEAD(path string, handle Handle) { + r.Handle("HEAD", path, handle) +} + // POST is a shortcut for router.Handle("POST", path, handle) func (r *Router) POST(path string, handle Handle) { r.Handle("POST", path, handle) @@ -284,7 +298,7 @@ } if tsr && r.RedirectTrailingSlash { - if path[len(path)-1] == '/' { + if len(path) > 1 && path[len(path)-1] == '/' { req.URL.Path = path[:len(path)-1] } else { req.URL.Path = path + "/" @@ -308,6 +322,25 @@ } } + // Handle 405 + if r.HandleMethodNotAllowed { + for method := range r.trees { + // Skip the requested method - we already tried this one + if method == req.Method { + continue + } + + handle, _, _ := r.trees[method].getValue(req.URL.Path) + if handle != nil { + http.Error(w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + return + } + } + } + // Handle 404 if r.NotFound != nil { r.NotFound(w, req)
diff --git a/router_test.go b/router_test.go index ca59066..6292ba8 100644 --- a/router_test.go +++ b/router_test.go
@@ -76,7 +76,7 @@ } func TestRouterAPI(t *testing.T) { - var get, post, put, patch, delete, handler, handlerFunc bool + var get, head, post, put, patch, delete, handler, handlerFunc bool httpHandler := handlerStruct{&handler} @@ -84,6 +84,9 @@ router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) + router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { + head = true + }) router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { post = true }) @@ -109,6 +112,12 @@ t.Error("routing GET failed") } + r, _ = http.NewRequest("HEAD", "/GET", nil) + router.ServeHTTP(w, r) + if !head { + t.Error("routing HEAD failed") + } + r, _ = http.NewRequest("POST", "/POST", nil) router.ServeHTTP(w, r) if !post { @@ -156,12 +165,28 @@ } } +func TestRouterNotAllowed(t *testing.T) { + handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} + + router := New() + router.POST("/path", handlerFunc) + + // Test not allowed + r, _ := http.NewRequest("GET", "/path", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + if !(w.Code == http.StatusMethodNotAllowed) { + t.Errorf("NotAllowed handling route %s failed: Code=%d, Header=%v", w.Code, w.Header()) + } +} + func TestRouterNotFound(t *testing.T) { handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} router := New() router.GET("/path", handlerFunc) router.GET("/dir/", handlerFunc) + router.GET("/", handlerFunc) testRoutes := []struct { route string @@ -170,6 +195,7 @@ }{ {"/path/", 301, "map[Location:[/path]]"}, // TSR -/ {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ + {"", 301, "map[Location:[/]]"}, // TSR +/ {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/