diff -Nru golang-github-gorilla-mux-1.7.3/.circleci/config.yml golang-github-gorilla-mux-1.7.4/.circleci/config.yml --- golang-github-gorilla-mux-1.7.3/.circleci/config.yml 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/.circleci/config.yml 2020-01-12 19:17:43.000000000 +0000 @@ -8,23 +8,35 @@ - image: circleci/golang:latest working_directory: /go/src/github.com/gorilla/mux steps: &steps + # Our build steps: we checkout the repo, fetch our deps, lint, and finally + # run "go test" on the package. - checkout + # Logs the version in our build logs, for posterity - run: go version - - run: go get -t -v ./... + - run: + name: "Fetch dependencies" + command: > + go get -t -v ./... # Only run gofmt, vet & lint against the latest Go version - - run: > - if [[ "$LATEST" = true ]]; then - go get -u golang.org/x/lint/golint - golint ./... - fi - - run: > - if [[ "$LATEST" = true ]]; then - diff -u <(echo -n) <(gofmt -d .) - fi - - run: > - if [[ "$LATEST" = true ]]; then - go vet -v . - fi + - run: + name: "Run golint" + command: > + if [ "${LATEST}" = true ] && [ -z "${SKIP_GOLINT}" ]; then + go get -u golang.org/x/lint/golint + golint ./... + fi + - run: + name: "Run gofmt" + command: > + if [[ "${LATEST}" = true ]]; then + diff -u <(echo -n) <(gofmt -d -e .) + fi + - run: + name: "Run go vet" + command: > + if [[ "${LATEST}" = true ]]; then + go vet -v ./... + fi - run: go test -v -race ./... "latest": diff -Nru golang-github-gorilla-mux-1.7.3/context.go golang-github-gorilla-mux-1.7.4/context.go --- golang-github-gorilla-mux-1.7.3/context.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/context.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -package mux - -import ( - "context" - "net/http" -) - -func contextGet(r *http.Request, key interface{}) interface{} { - return r.Context().Value(key) -} - -func contextSet(r *http.Request, key, val interface{}) *http.Request { - if val == nil { - return r - } - - return r.WithContext(context.WithValue(r.Context(), key, val)) -} diff -Nru golang-github-gorilla-mux-1.7.3/context_test.go golang-github-gorilla-mux-1.7.4/context_test.go --- golang-github-gorilla-mux-1.7.3/context_test.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/context_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -package mux - -import ( - "context" - "net/http" - "testing" - "time" -) - -func TestNativeContextMiddleware(t *testing.T) { - withTimeout := func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), time.Minute) - defer cancel() - h.ServeHTTP(w, r.WithContext(ctx)) - }) - } - - r := NewRouter() - r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := Vars(r) - if vars["foo"] != "bar" { - t.Fatal("Expected foo var to be set") - } - }))) - - rec := NewRecorder() - req := newRequest("GET", "/path/bar") - r.ServeHTTP(rec, req) -} diff -Nru golang-github-gorilla-mux-1.7.3/debian/changelog golang-github-gorilla-mux-1.7.4/debian/changelog --- golang-github-gorilla-mux-1.7.3/debian/changelog 2019-10-23 08:56:42.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/debian/changelog 2020-07-11 13:58:58.000000000 +0000 @@ -1,3 +1,17 @@ +golang-github-gorilla-mux (1.7.4-1) unstable; urgency=medium + + * Team upload. + [ Debian Janitor ] + * Wrap long lines in changelog entries: 1.1-3. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + * Fix day-of-week for changelog entry 0.0~git20130701-1. + + [ Reinhard Tartler ] + * New upstream version 1.7.4 + + -- Reinhard Tartler Sat, 11 Jul 2020 09:58:58 -0400 + golang-github-gorilla-mux (1.7.3-1) unstable; urgency=medium [ Alexandre Viau ] @@ -29,7 +43,8 @@ * Add me to uploaders [ Konstantinos Margaritis ] - * Replace golang-go with golang-any in Build-Depends, remove golang-go from Depends + * Replace golang-go with golang-any in Build-Depends, remove golang-go from + Depends -- Konstantinos Margaritis Tue, 08 Aug 2017 14:07:18 +0300 @@ -82,4 +97,4 @@ * Initial release (Closes: #714805) - -- Daniel Mizyrycki Fri, 13 Jul 2013 11:19:06 -0700 + -- Daniel Mizyrycki Sat, 13 Jul 2013 11:19:06 -0700 diff -Nru golang-github-gorilla-mux-1.7.3/debian/.gitignore golang-github-gorilla-mux-1.7.4/debian/.gitignore --- golang-github-gorilla-mux-1.7.3/debian/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/debian/.gitignore 2020-07-11 13:58:58.000000000 +0000 @@ -0,0 +1,5 @@ +*.debhelper.log +*.substvars +golang-github-gorilla-mux-dev/ +files +tmp/ diff -Nru golang-github-gorilla-mux-1.7.3/debian/upstream/metadata golang-github-gorilla-mux-1.7.4/debian/upstream/metadata --- golang-github-gorilla-mux-1.7.3/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/debian/upstream/metadata 2020-07-11 13:58:58.000000000 +0000 @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/gorilla/mux/issues +Bug-Submit: https://github.com/gorilla/mux/issues/new +Repository: https://github.com/gorilla/mux.git +Repository-Browse: https://github.com/gorilla/mux diff -Nru golang-github-gorilla-mux-1.7.3/debian/watch golang-github-gorilla-mux-1.7.4/debian/watch --- golang-github-gorilla-mux-1.7.3/debian/watch 2019-10-23 08:55:13.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/debian/watch 2020-07-11 13:58:58.000000000 +0000 @@ -1,5 +1,5 @@ # uscan(1) configuration file. -version=3 - - https://github.com/gorilla/mux/releases \ - .*/archive/v?(\d[\d\.]+)\.tar\.gz +version=4 +opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%golang-github-gorilla-mux-$1.tar.gz%" \ + https://github.com/gorilla/mux/tags \ + (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian diff -Nru golang-github-gorilla-mux-1.7.3/go.mod golang-github-gorilla-mux-1.7.4/go.mod --- golang-github-gorilla-mux-1.7.3/go.mod 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/go.mod 2020-01-12 19:17:43.000000000 +0000 @@ -1 +1,3 @@ module github.com/gorilla/mux + +go 1.12 diff -Nru golang-github-gorilla-mux-1.7.3/middleware.go golang-github-gorilla-mux-1.7.4/middleware.go --- golang-github-gorilla-mux-1.7.3/middleware.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/middleware.go 2020-01-12 19:17:43.000000000 +0000 @@ -58,22 +58,17 @@ func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { var allMethods []string - err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { - for _, m := range route.matchers { - if _, ok := m.(*routeRegexp); ok { - if m.Match(req, &RouteMatch{}) { - methods, err := route.GetMethods() - if err != nil { - return err - } - - allMethods = append(allMethods, methods...) - } - break + for _, route := range r.routes { + var match RouteMatch + if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { + methods, err := route.GetMethods() + if err != nil { + return nil, err } + + allMethods = append(allMethods, methods...) } - return nil - }) + } - return allMethods, err + return allMethods, nil } diff -Nru golang-github-gorilla-mux-1.7.3/middleware_test.go golang-github-gorilla-mux-1.7.4/middleware_test.go --- golang-github-gorilla-mux-1.7.3/middleware_test.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/middleware_test.go 2020-01-12 19:17:43.000000000 +0000 @@ -478,6 +478,26 @@ } } +func TestCORSMethodMiddlewareSubrouter(t *testing.T) { + router := NewRouter().StrictSlash(true) + + subrouter := router.PathPrefix("/test").Subrouter() + subrouter.HandleFunc("/hello", stringHandler("a")).Methods(http.MethodGet, http.MethodOptions, http.MethodPost) + subrouter.HandleFunc("/hello/{name}", stringHandler("b")).Methods(http.MethodGet, http.MethodOptions) + + subrouter.Use(CORSMethodMiddleware(subrouter)) + + rw := NewRecorder() + req := newRequest("GET", "/test/hello/asdf") + router.ServeHTTP(rw, req) + + actualMethods := rw.Header().Get("Access-Control-Allow-Methods") + expectedMethods := "GET,OPTIONS" + if actualMethods != expectedMethods { + t.Fatalf("expected methods %q but got: %q", expectedMethods, actualMethods) + } +} + func TestMiddlewareOnMultiSubrouter(t *testing.T) { first := "first" second := "second" diff -Nru golang-github-gorilla-mux-1.7.3/mux.go golang-github-gorilla-mux-1.7.4/mux.go --- golang-github-gorilla-mux-1.7.3/mux.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/mux.go 2020-01-12 19:17:43.000000000 +0000 @@ -5,6 +5,7 @@ package mux import ( + "context" "errors" "fmt" "net/http" @@ -58,8 +59,7 @@ // If true, do not clear the request context after handling the request. // - // Deprecated: No effect when go1.7+ is used, since the context is stored - // on the request itself. + // Deprecated: No effect, since the context is stored on the request itself. KeepContext bool // Slice of middlewares to be called after a match is found @@ -111,10 +111,8 @@ c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) } - c.matchers = make([]matcher, 0, len(r.matchers)) - for _, m := range r.matchers { - c.matchers = append(c.matchers, m) - } + c.matchers = make([]matcher, len(r.matchers)) + copy(c.matchers, r.matchers) return c } @@ -197,8 +195,8 @@ var handler http.Handler if r.Match(req, &match) { handler = match.Handler - req = setVars(req, match.Vars) - req = setCurrentRoute(req, match.Route) + req = requestWithVars(req, match.Vars) + req = requestWithRoute(req, match.Route) } if handler == nil && match.MatchErr == ErrMethodMismatch { @@ -428,7 +426,7 @@ // Vars returns the route variables for the current request, if any. func Vars(r *http.Request) map[string]string { - if rv := contextGet(r, varsKey); rv != nil { + if rv := r.Context().Value(varsKey); rv != nil { return rv.(map[string]string) } return nil @@ -440,18 +438,20 @@ // after the handler returns, unless the KeepContext option is set on the // Router. func CurrentRoute(r *http.Request) *Route { - if rv := contextGet(r, routeKey); rv != nil { + if rv := r.Context().Value(routeKey); rv != nil { return rv.(*Route) } return nil } -func setVars(r *http.Request, val interface{}) *http.Request { - return contextSet(r, varsKey, val) +func requestWithVars(r *http.Request, vars map[string]string) *http.Request { + ctx := context.WithValue(r.Context(), varsKey, vars) + return r.WithContext(ctx) } -func setCurrentRoute(r *http.Request, val interface{}) *http.Request { - return contextSet(r, routeKey, val) +func requestWithRoute(r *http.Request, route *Route) *http.Request { + ctx := context.WithValue(r.Context(), routeKey, route) + return r.WithContext(ctx) } // ---------------------------------------------------------------------------- diff -Nru golang-github-gorilla-mux-1.7.3/mux_httpserver_test.go golang-github-gorilla-mux-1.7.4/mux_httpserver_test.go --- golang-github-gorilla-mux-1.7.3/mux_httpserver_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/mux_httpserver_test.go 2020-01-12 19:17:43.000000000 +0000 @@ -0,0 +1,49 @@ +// +build go1.9 + +package mux + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSchemeMatchers(t *testing.T) { + router := NewRouter() + router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("hello http world")) + }).Schemes("http") + router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("hello https world")) + }).Schemes("https") + + assertResponseBody := func(t *testing.T, s *httptest.Server, expectedBody string) { + resp, err := s.Client().Get(s.URL) + if err != nil { + t.Fatalf("unexpected error getting from server: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("expected a status code of 200, got %v", resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error reading body: %v", err) + } + if !bytes.Equal(body, []byte(expectedBody)) { + t.Fatalf("response should be hello world, was: %q", string(body)) + } + } + + t.Run("httpServer", func(t *testing.T) { + s := httptest.NewServer(router) + defer s.Close() + assertResponseBody(t, s, "hello http world") + }) + t.Run("httpsServer", func(t *testing.T) { + s := httptest.NewTLSServer(router) + defer s.Close() + assertResponseBody(t, s, "hello https world") + }) +} diff -Nru golang-github-gorilla-mux-1.7.3/mux_test.go golang-github-gorilla-mux-1.7.4/mux_test.go --- golang-github-gorilla-mux-1.7.3/mux_test.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/mux_test.go 2020-01-12 19:17:43.000000000 +0000 @@ -7,13 +7,17 @@ import ( "bufio" "bytes" + "context" "errors" "fmt" + "io/ioutil" "net/http" + "net/http/httptest" "net/url" "reflect" "strings" "testing" + "time" ) func (r *Route) GoString() string { @@ -680,8 +684,8 @@ }, { title: "Headers route, regex header values to match", - route: new(Route).Headers("foo", "ba[zr]"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar"}), + route: new(Route).HeadersRegexp("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baw"}), vars: map[string]string{}, host: "", path: "", @@ -1943,7 +1947,7 @@ } func (ho *TestA301ResponseWriter) Header() http.Header { - return http.Header(ho.hh) + return ho.hh } func (ho *TestA301ResponseWriter) Write(b []byte) (int, error) { @@ -2045,7 +2049,7 @@ resp := NewRecorder() r.ServeHTTP(resp, req) - if resp.Code != 405 { + if resp.Code != http.StatusMethodNotAllowed { t.Errorf("Expecting code %v", 405) } @@ -2724,11 +2728,68 @@ router.ServeHTTP(w, req) - if w.Code != 405 { + if w.Code != http.StatusMethodNotAllowed { t.Fatalf("Expected status code 405 (got %d)", w.Code) } } +type customMethodNotAllowedHandler struct { + msg string +} + +func (h customMethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprint(w, h.msg) +} + +func TestSubrouterCustomMethodNotAllowed(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + + router := NewRouter() + router.HandleFunc("/test", handler).Methods(http.MethodGet) + router.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom router handler"} + + subrouter := router.PathPrefix("/sub").Subrouter() + subrouter.HandleFunc("/test", handler).Methods(http.MethodGet) + subrouter.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom sub router handler"} + + testCases := map[string]struct { + path string + expMsg string + }{ + "router method not allowed": { + path: "/test", + expMsg: "custom router handler", + }, + "subrouter method not allowed": { + path: "/sub/test", + expMsg: "custom sub router handler", + }, + } + + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + w := NewRecorder() + req := newRequest(http.MethodPut, tc.path) + + router.ServeHTTP(w, req) + + if w.Code != http.StatusMethodNotAllowed { + tt.Errorf("Expected status code 405 (got %d)", w.Code) + } + + b, err := ioutil.ReadAll(w.Body) + if err != nil { + tt.Errorf("failed to read body: %v", err) + } + + if string(b) != tc.expMsg { + tt.Errorf("expected msg %q, got %q", tc.expMsg, string(b)) + } + }) + } +} + func TestSubrouterNotFound(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } router := NewRouter() @@ -2740,11 +2801,33 @@ router.ServeHTTP(w, req) - if w.Code != 404 { + if w.Code != http.StatusNotFound { t.Fatalf("Expected status code 404 (got %d)", w.Code) } } +func TestContextMiddleware(t *testing.T) { + withTimeout := func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + h.ServeHTTP(w, r.WithContext(ctx)) + }) + } + + r := NewRouter() + r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := Vars(r) + if vars["foo"] != "bar" { + t.Fatal("Expected foo var to be set") + } + }))) + + rec := NewRecorder() + req := newRequest("GET", "/path/bar") + r.ServeHTTP(rec, req) +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int @@ -2837,10 +2920,7 @@ // newRequestHost a new request with a method, url, and host header func newRequestHost(method, url, host string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) - } + req := httptest.NewRequest(method, url, nil) req.Host = host return req } diff -Nru golang-github-gorilla-mux-1.7.3/old_test.go golang-github-gorilla-mux-1.7.4/old_test.go --- golang-github-gorilla-mux-1.7.3/old_test.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/old_test.go 2020-01-12 19:17:43.000000000 +0000 @@ -385,6 +385,11 @@ vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, url: "http://foo.domain.com/articles/technology/42", }, + { + route: new(Route).Host("example.com").Schemes("https", "http"), + vars: []string{}, + url: "https://example.com", + }, } func TestHeaderMatcher(t *testing.T) { @@ -502,18 +507,6 @@ url := u.String() if url != v.url { t.Errorf("expected %v, got %v", v.url, url) - /* - reversePath := "" - reverseHost := "" - if v.route.pathTemplate != nil { - reversePath = v.route.pathTemplate.Reverse - } - if v.route.hostTemplate != nil { - reverseHost = v.route.hostTemplate.Reverse - } - - t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) - */ } } diff -Nru golang-github-gorilla-mux-1.7.3/README.md golang-github-gorilla-mux-1.7.4/README.md --- golang-github-gorilla-mux-1.7.3/README.md 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/README.md 2020-01-12 19:17:43.000000000 +0000 @@ -1,11 +1,10 @@ # gorilla/mux [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) [![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) -![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) +![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) https://www.gorillatoolkit.org/pkg/mux @@ -26,6 +25,7 @@ * [Examples](#examples) * [Matching Routes](#matching-routes) * [Static Files](#static-files) +* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) @@ -212,6 +212,93 @@ } ``` +### Serving Single Page Applications + +Most of the time it makes sense to serve your SPA on a separate web server from your API, +but sometimes it's desirable to serve them both from one place. It's possible to write a simple +handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage +mux's powerful routing for your API endpoints. + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) + if err != nil { + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // prepend the path with the path to the static directory + path = filepath.Join(h.staticPath, path) + + // check whether a file exists at the given path + _, err = os.Stat(path) + if os.IsNotExist(err) { + // file does not exist, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + // an example API handler + json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + }) + + spa := spaHandler{staticPath: "build", indexPath: "index.html"} + router.PathPrefix("/").Handler(spa) + + srv := &http.Server{ + Handler: router, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + ### Registered URLs Now let's see how to build registered URLs. diff -Nru golang-github-gorilla-mux-1.7.3/regexp.go golang-github-gorilla-mux-1.7.4/regexp.go --- golang-github-gorilla-mux-1.7.3/regexp.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/regexp.go 2020-01-12 19:17:43.000000000 +0000 @@ -181,21 +181,21 @@ } } return r.regexp.MatchString(host) - } else { - if r.regexpType == regexpTypeQuery { - return r.matchQueryString(req) - } - path := req.URL.Path - if r.options.useEncodedPath { - path = req.URL.EscapedPath() - } - return r.regexp.MatchString(path) } + + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) } // url builds a URL part using the given values. func (r *routeRegexp) url(values map[string]string) (string, error) { - urlValues := make([]interface{}, len(r.varsN)) + urlValues := make([]interface{}, len(r.varsN), len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] if !ok { @@ -230,14 +230,51 @@ return "" } templateKey := strings.SplitN(r.template, "=", 2)[0] - for key, vals := range req.URL.Query() { - if key == templateKey && len(vals) > 0 { - return key + "=" + vals[0] - } + val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) + if ok { + return templateKey + "=" + val } return "" } +// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. +// If key was not found, empty string and false is returned. +func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { + query := []byte(rawQuery) + for len(query) > 0 { + foundKey := query + if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { + foundKey, query = foundKey[:i], foundKey[i+1:] + } else { + query = query[:0] + } + if len(foundKey) == 0 { + continue + } + var value []byte + if i := bytes.IndexByte(foundKey, '='); i >= 0 { + foundKey, value = foundKey[:i], foundKey[i+1:] + } + if len(foundKey) < len(key) { + // Cannot possibly be key. + continue + } + keyString, err := url.QueryUnescape(string(foundKey)) + if err != nil { + continue + } + if keyString != key { + continue + } + valueString, err := url.QueryUnescape(string(value)) + if err != nil { + continue + } + return valueString, true + } + return "", false +} + func (r *routeRegexp) matchQueryString(req *http.Request) bool { return r.regexp.MatchString(r.getURLQuery(req)) } diff -Nru golang-github-gorilla-mux-1.7.3/regexp_test.go golang-github-gorilla-mux-1.7.4/regexp_test.go --- golang-github-gorilla-mux-1.7.3/regexp_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/regexp_test.go 2020-01-12 19:17:43.000000000 +0000 @@ -0,0 +1,91 @@ +package mux + +import ( + "net/url" + "reflect" + "strconv" + "testing" +) + +func Test_findFirstQueryKey(t *testing.T) { + tests := []string{ + "a=1&b=2", + "a=1&a=2&a=banana", + "ascii=%3Ckey%3A+0x90%3E", + "a=1;b=2", + "a=1&a=2;a=banana", + "a==", + "a=%2", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=1& ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;&a=5", + "a=xxxxxxxxxxxxxxxx&b=YYYYYYYYYYYYYYY&c=ppppppppppppppppppp&f=ttttttttttttttttt&a=uuuuuuuuuuuuu", + } + for _, query := range tests { + t.Run(query, func(t *testing.T) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + for key, want := range all { + t.Run(key, func(t *testing.T) { + got, ok := findFirstQueryKey(query, key) + if !ok { + t.Error("Did not get expected key", key) + } + if !reflect.DeepEqual(got, want[0]) { + t.Errorf("findFirstQueryKey(%s,%s) = %v, want %v", query, key, got, want[0]) + } + }) + } + }) + } +} + +func Benchmark_findQueryKey(b *testing.B) { + tests := []string{ + "a=1&b=2", + "ascii=%3Ckey%3A+0x90%3E", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", + "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", + } + for i, query := range tests { + b.Run(strconv.Itoa(i), func(b *testing.B) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for key, _ := range all { + _, _ = findFirstQueryKey(query, key) + } + } + }) + } +} + +func Benchmark_findQueryKeyGoLib(b *testing.B) { + tests := []string{ + "a=1&b=2", + "ascii=%3Ckey%3A+0x90%3E", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", + "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", + } + for i, query := range tests { + b.Run(strconv.Itoa(i), func(b *testing.B) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + var u url.URL + u.RawQuery = query + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for key, _ := range all { + v := u.Query()[key] + if len(v) > 0 { + _ = v[0] + } + } + } + }) + } +} diff -Nru golang-github-gorilla-mux-1.7.3/route.go golang-github-gorilla-mux-1.7.4/route.go --- golang-github-gorilla-mux-1.7.3/route.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/route.go 2020-01-12 19:17:43.000000000 +0000 @@ -74,7 +74,7 @@ return false } - if match.MatchErr == ErrMethodMismatch { + if match.MatchErr == ErrMethodMismatch && r.handler != nil { // We found a route which matches request method, clear MatchErr match.MatchErr = nil // Then override the mis-matched handler @@ -412,11 +412,30 @@ type schemeMatcher []string func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { - return matchInArray(m, r.URL.Scheme) + scheme := r.URL.Scheme + // https://golang.org/pkg/net/http/#Request + // "For [most] server requests, fields other than Path and RawQuery will be + // empty." + // Since we're an http muxer, the scheme is either going to be http or https + // though, so we can just set it based on the tls termination state. + if scheme == "" { + if r.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + } + return matchInArray(m, scheme) } // Schemes adds a matcher for URL schemes. // It accepts a sequence of schemes to be matched, e.g.: "http", "https". +// If the request's URL has a scheme set, it will be matched against. +// Generally, the URL scheme will only be set if a previous handler set it, +// such as the ProxyHeaders handler from gorilla/handlers. +// If unset, the scheme will be determined based on the request's TLS +// termination state. +// The first argument to Schemes will be used when constructing a route URL. func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) @@ -493,8 +512,8 @@ // This also works for host variables: // // r := mux.NewRouter() -// r.Host("{subdomain}.domain.com"). -// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). // Name("article") // // // url.String() will be "http://news.domain.com/articles/technology/42" @@ -502,6 +521,13 @@ // "category", "technology", // "id", "42") // +// The scheme of the resulting url will be the first argument that was passed to Schemes: +// +// // url.String() will be "https://example.com" +// r := mux.NewRouter() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() +// // All variables defined in the route are required, and their values must // conform to the corresponding patterns. func (r *Route) URL(pairs ...string) (*url.URL, error) { @@ -635,7 +661,7 @@ if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.regexp.String()) } @@ -654,7 +680,7 @@ if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.template) } diff -Nru golang-github-gorilla-mux-1.7.3/test_helpers.go golang-github-gorilla-mux-1.7.4/test_helpers.go --- golang-github-gorilla-mux-1.7.3/test_helpers.go 2019-06-30 04:17:52.000000000 +0000 +++ golang-github-gorilla-mux-1.7.4/test_helpers.go 2020-01-12 19:17:43.000000000 +0000 @@ -15,5 +15,5 @@ // can be set by making a route that captures the required variables, // starting a server and sending the request to that server. func SetURLVars(r *http.Request, val map[string]string) *http.Request { - return setVars(r, val) + return requestWithVars(r, val) }