pax_global_header00006660000000000000000000000064135155027560014523gustar00rootroot0000000000000052 comment=8a3748addc242fc560bd6d4ff28b0374c010b1b4 handlers-1.4.2/000077500000000000000000000000001351550275600133275ustar00rootroot00000000000000handlers-1.4.2/.circleci/000077500000000000000000000000001351550275600151625ustar00rootroot00000000000000handlers-1.4.2/.circleci/config.yml000066400000000000000000000023731351550275600171570ustar00rootroot00000000000000version: 2.0 jobs: # Base test configuration for Go library tests Each distinct version should # inherit this base, and override (at least) the container image used. "test": &test docker: - image: circleci/golang:latest working_directory: /go/src/github.com/gorilla/handlers steps: &steps - checkout - run: go version - run: go get -t -v ./... - run: > if [[ "$LATEST" = true ]]; then diff -u <(echo -n) <(gofmt -d .) fi - run: > if [[ "$LATEST" = true ]]; then go vet -v . fi - run: go test -v -race ./... "latest": <<: *test environment: LATEST: true "1.12": <<: *test docker: - image: circleci/golang:1.12 "1.11": <<: *test docker: - image: circleci/golang:1.11 "1.10": <<: *test docker: - image: circleci/golang:1.10 "1.9": <<: *test docker: - image: circleci/golang:1.9 "1.8": <<: *test docker: - image: circleci/golang:1.8 "1.7": <<: *test docker: - image: circleci/golang:1.7 workflows: version: 2 build: jobs: - "latest" - "1.12" - "1.11" - "1.10" - "1.9" - "1.8" - "1.7" handlers-1.4.2/.github/000077500000000000000000000000001351550275600146675ustar00rootroot00000000000000handlers-1.4.2/.github/release-drafter.yml000066400000000000000000000001761351550275600204630ustar00rootroot00000000000000# Config for https://github.com/apps/release-drafter template: | ## CHANGELOG $CHANGES handlers-1.4.2/.github/stale.yml000066400000000000000000000005271351550275600165260ustar00rootroot00000000000000daysUntilStale: 60 daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - v2 - needs-review - work-required staleLabel: stale markComment: > This issue has been automatically marked as stale because it hasn't seen a recent update. It'll be automatically closed in a few days. closeComment: false handlers-1.4.2/LICENSE000066400000000000000000000024371351550275600143420ustar00rootroot00000000000000Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. handlers-1.4.2/README.md000066400000000000000000000053211351550275600146070ustar00rootroot00000000000000gorilla/handlers ================ [![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) [![CircleCI](https://circleci.com/gh/gorilla/handlers.svg?style=svg)](https://circleci.com/gh/gorilla/handlers) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/handlers/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/handlers?badge) Package handlers is a collection of handlers (aka "HTTP middleware") for use with Go's `net/http` package (or any framework supporting `http.Handler`), including: * [**LoggingHandler**](https://godoc.org/github.com/gorilla/handlers#LoggingHandler) for logging HTTP requests in the Apache [Common Log Format](http://httpd.apache.org/docs/2.2/logs.html#common). * [**CombinedLoggingHandler**](https://godoc.org/github.com/gorilla/handlers#CombinedLoggingHandler) for logging HTTP requests in the Apache [Combined Log Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by both Apache and nginx. * [**CompressHandler**](https://godoc.org/github.com/gorilla/handlers#CompressHandler) for gzipping responses. * [**ContentTypeHandler**](https://godoc.org/github.com/gorilla/handlers#ContentTypeHandler) for validating requests against a list of accepted content types. * [**MethodHandler**](https://godoc.org/github.com/gorilla/handlers#MethodHandler) for matching HTTP methods against handlers in a `map[string]http.Handler` * [**ProxyHeaders**](https://godoc.org/github.com/gorilla/handlers#ProxyHeaders) for populating `r.RemoteAddr` and `r.URL.Scheme` based on the `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded` headers when running a Go server behind a HTTP reverse proxy. * [**CanonicalHost**](https://godoc.org/github.com/gorilla/handlers#CanonicalHost) for re-directing to the preferred host when handling multiple domains (i.e. multiple CNAME aliases). * [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics. Other handlers are documented [on the Gorilla website](https://www.gorillatoolkit.org/pkg/handlers). ## Example A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`: ```go import ( "net/http" "github.com/gorilla/handlers" ) func main() { r := http.NewServeMux() // Only log requests to our admin dashboard to stdout r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard))) r.HandleFunc("/", ShowIndex) // Wrap our server with our gzip handler to gzip compress all responses. http.ListenAndServe(":8000", handlers.CompressHandler(r)) } ``` ## License BSD licensed. See the included LICENSE file for details. handlers-1.4.2/canonical.go000066400000000000000000000036531351550275600156140ustar00rootroot00000000000000package handlers import ( "net/http" "net/url" "strings" ) type canonical struct { h http.Handler domain string code int } // CanonicalHost is HTTP middleware that re-directs requests to the canonical // domain. It accepts a domain and a status code (e.g. 301 or 302) and // re-directs clients to this domain. The existing request path is maintained. // // Note: If the provided domain is considered invalid by url.Parse or otherwise // returns an empty scheme or host, clients are not re-directed. // // Example: // // r := mux.NewRouter() // canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302) // r.HandleFunc("/route", YourHandler) // // log.Fatal(http.ListenAndServe(":7000", canonical(r))) // func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler { fn := func(h http.Handler) http.Handler { return canonical{h, domain, code} } return fn } func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) { dest, err := url.Parse(c.domain) if err != nil { // Call the next handler if the provided domain fails to parse. c.h.ServeHTTP(w, r) return } if dest.Scheme == "" || dest.Host == "" { // Call the next handler if the scheme or host are empty. // Note that url.Parse won't fail on in this case. c.h.ServeHTTP(w, r) return } if !strings.EqualFold(cleanHost(r.Host), dest.Host) { // Re-build the destination URL dest := dest.Scheme + "://" + dest.Host + r.URL.Path if r.URL.RawQuery != "" { dest += "?" + r.URL.RawQuery } http.Redirect(w, r, dest, c.code) return } c.h.ServeHTTP(w, r) } // cleanHost cleans invalid Host headers by stripping anything after '/' or ' '. // This is backported from Go 1.5 (in response to issue #11206) and attempts to // mitigate malformed Host headers that do not match the format in RFC7230. func cleanHost(in string) string { if i := strings.IndexAny(in, " /"); i != -1 { return in[:i] } return in } handlers-1.4.2/canonical_test.go000066400000000000000000000065271351550275600166560ustar00rootroot00000000000000package handlers import ( "bufio" "bytes" "log" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestCleanHost(t *testing.T) { tests := []struct { in, want string }{ {"www.google.com", "www.google.com"}, {"www.google.com foo", "www.google.com"}, {"www.google.com/foo", "www.google.com"}, {" first character is a space", ""}, } for _, tt := range tests { got := cleanHost(tt.in) if tt.want != got { t.Errorf("cleanHost(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestCanonicalHost(t *testing.T) { gorilla := "http://www.gorillatoolkit.org" rr := httptest.NewRecorder() r := newRequest("GET", "http://www.example.com/") testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) // Test a re-direct: should return a 302 Found. CanonicalHost(gorilla, http.StatusFound)(testHandler).ServeHTTP(rr, r) if rr.Code != http.StatusFound { t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusFound) } if rr.Header().Get("Location") != gorilla+r.URL.Path { t.Fatalf("bad re-direct: got %q want %q", rr.Header().Get("Location"), gorilla+r.URL.Path) } } func TestKeepsQueryString(t *testing.T) { google := "https://www.google.com" rr := httptest.NewRecorder() querystring := url.Values{"q": {"golang"}, "format": {"json"}}.Encode() r := newRequest("GET", "http://www.example.com/search?"+querystring) testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CanonicalHost(google, http.StatusFound)(testHandler).ServeHTTP(rr, r) want := google + r.URL.Path + "?" + querystring if rr.Header().Get("Location") != want { t.Fatalf("bad re-direct: got %q want %q", rr.Header().Get("Location"), want) } } func TestBadDomain(t *testing.T) { rr := httptest.NewRecorder() r := newRequest("GET", "http://www.example.com/") testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) // Test a bad domain - should return 200 OK. CanonicalHost("%", http.StatusFound)(testHandler).ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusOK) } } func TestEmptyHost(t *testing.T) { rr := httptest.NewRecorder() r := newRequest("GET", "http://www.example.com/") testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) // Test a domain that returns an empty url.Host from url.Parse. CanonicalHost("hello.com", http.StatusFound)(testHandler).ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusOK) } } func TestHeaderWrites(t *testing.T) { gorilla := "http://www.gorillatoolkit.org" testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) // Catch the log output to ensure we don't write multiple headers. var b bytes.Buffer buf := bufio.NewWriter(&b) tl := log.New(buf, "test: ", log.Lshortfile) srv := httptest.NewServer( CanonicalHost(gorilla, http.StatusFound)(testHandler)) defer srv.Close() srv.Config.ErrorLog = tl _, err := http.Get(srv.URL) if err != nil { t.Fatal(err) } err = buf.Flush() if err != nil { t.Fatal(err) } // We rely on the error not changing: net/http does not export it. if strings.Contains(b.String(), "multiple response.WriteHeader calls") { t.Fatalf("re-direct did not return early: multiple header writes") } } handlers-1.4.2/compress.go000066400000000000000000000067401351550275600155200ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "compress/flate" "compress/gzip" "io" "net/http" "strings" ) type compressResponseWriter struct { io.Writer http.ResponseWriter http.Hijacker http.Flusher http.CloseNotifier } func (w *compressResponseWriter) WriteHeader(c int) { w.ResponseWriter.Header().Del("Content-Length") w.ResponseWriter.WriteHeader(c) } func (w *compressResponseWriter) Header() http.Header { return w.ResponseWriter.Header() } func (w *compressResponseWriter) Write(b []byte) (int, error) { h := w.ResponseWriter.Header() if h.Get("Content-Type") == "" { h.Set("Content-Type", http.DetectContentType(b)) } h.Del("Content-Length") return w.Writer.Write(b) } type flusher interface { Flush() error } func (w *compressResponseWriter) Flush() { // Flush compressed data if compressor supports it. if f, ok := w.Writer.(flusher); ok { f.Flush() } // Flush HTTP response. if w.Flusher != nil { w.Flusher.Flush() } } // CompressHandler gzip compresses HTTP responses for clients that support it // via the 'Accept-Encoding' header. // // Compressing TLS traffic may leak the page contents to an attacker if the // page contains user input: http://security.stackexchange.com/a/102015/12208 func CompressHandler(h http.Handler) http.Handler { return CompressHandlerLevel(h, gzip.DefaultCompression) } // CompressHandlerLevel gzip compresses HTTP responses with specified compression level // for clients that support it via the 'Accept-Encoding' header. // // The compression level should be gzip.DefaultCompression, gzip.NoCompression, // or any integer value between gzip.BestSpeed and gzip.BestCompression inclusive. // gzip.DefaultCompression is used in case of invalid compression level. func CompressHandlerLevel(h http.Handler, level int) http.Handler { if level < gzip.DefaultCompression || level > gzip.BestCompression { level = gzip.DefaultCompression } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { L: for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { switch strings.TrimSpace(enc) { case "gzip": w.Header().Set("Content-Encoding", "gzip") r.Header.Del("Accept-Encoding") w.Header().Add("Vary", "Accept-Encoding") gw, _ := gzip.NewWriterLevel(w, level) defer gw.Close() h, hok := w.(http.Hijacker) if !hok { /* w is not Hijacker... oh well... */ h = nil } f, fok := w.(http.Flusher) if !fok { f = nil } cn, cnok := w.(http.CloseNotifier) if !cnok { cn = nil } w = &compressResponseWriter{ Writer: gw, ResponseWriter: w, Hijacker: h, Flusher: f, CloseNotifier: cn, } break L case "deflate": w.Header().Set("Content-Encoding", "deflate") r.Header.Del("Accept-Encoding") w.Header().Add("Vary", "Accept-Encoding") fw, _ := flate.NewWriter(w, level) defer fw.Close() h, hok := w.(http.Hijacker) if !hok { /* w is not Hijacker... oh well... */ h = nil } f, fok := w.(http.Flusher) if !fok { f = nil } cn, cnok := w.(http.CloseNotifier) if !cnok { cn = nil } w = &compressResponseWriter{ Writer: fw, ResponseWriter: w, Hijacker: h, Flusher: f, CloseNotifier: cn, } break L } } h.ServeHTTP(w, r) }) } handlers-1.4.2/compress_test.go000066400000000000000000000144011351550275600165500ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "bufio" "io" "net" "net/http" "net/http/httptest" "strconv" "testing" ) var contentType = "text/plain; charset=utf-8" func compressedRequest(w *httptest.ResponseRecorder, compression string) { CompressHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", strconv.Itoa(9*1024)) w.Header().Set("Content-Type", contentType) for i := 0; i < 1024; i++ { io.WriteString(w, "Gorilla!\n") } })).ServeHTTP(w, &http.Request{ Method: "GET", Header: http.Header{ "Accept-Encoding": []string{compression}, }, }) } func TestCompressHandlerNoCompression(t *testing.T) { w := httptest.NewRecorder() compressedRequest(w, "") if enc := w.HeaderMap.Get("Content-Encoding"); enc != "" { t.Errorf("wrong content encoding, got %q want %q", enc, "") } if ct := w.HeaderMap.Get("Content-Type"); ct != contentType { t.Errorf("wrong content type, got %q want %q", ct, contentType) } if w.Body.Len() != 1024*9 { t.Errorf("wrong len, got %d want %d", w.Body.Len(), 1024*9) } if l := w.HeaderMap.Get("Content-Length"); l != "9216" { t.Errorf("wrong content-length. got %q expected %d", l, 1024*9) } } func TestAcceptEncodingIsDropped(t *testing.T) { tCases := []struct { name, compression, expect string isPresent bool }{ { "accept-encoding-gzip", "gzip", "", false, }, { "accept-encoding-deflate", "deflate", "", false, }, { "accept-encoding-gzip,deflate", "gzip,deflate", "", false, }, { "accept-encoding-gzip,deflate,something", "gzip,deflate,something", "", false, }, { "accept-encoding-unknown", "unknown", "unknown", true, }, } for _, tCase := range tCases { ch := CompressHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { acceptEnc := r.Header.Get("Accept-Encoding") if acceptEnc == "" && tCase.isPresent { t.Fatalf("%s: expected 'Accept-Encoding' header to be present but was not", tCase.name) } if acceptEnc != "" { if !tCase.isPresent { t.Fatalf("%s: expected 'Accept-Encoding' header to be dropped but was still present having value %q", tCase.name, acceptEnc) } if acceptEnc != tCase.expect { t.Fatalf("%s: expected 'Accept-Encoding' to be %q but was %q", tCase.name, tCase.expect, acceptEnc) } } })) w := httptest.NewRecorder() ch.ServeHTTP(w, &http.Request{ Method: "GET", Header: http.Header{ "Accept-Encoding": []string{tCase.compression}, }, }) } } func TestCompressHandlerGzip(t *testing.T) { w := httptest.NewRecorder() compressedRequest(w, "gzip") if w.HeaderMap.Get("Content-Encoding") != "gzip" { t.Errorf("wrong content encoding, got %q want %q", w.HeaderMap.Get("Content-Encoding"), "gzip") } if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { t.Errorf("wrong content type, got %s want %s", w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } if w.Body.Len() != 72 { t.Errorf("wrong len, got %d want %d", w.Body.Len(), 72) } if l := w.HeaderMap.Get("Content-Length"); l != "" { t.Errorf("wrong content-length. got %q expected %q", l, "") } } func TestCompressHandlerDeflate(t *testing.T) { w := httptest.NewRecorder() compressedRequest(w, "deflate") if w.HeaderMap.Get("Content-Encoding") != "deflate" { t.Fatalf("wrong content encoding, got %q want %q", w.HeaderMap.Get("Content-Encoding"), "deflate") } if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { t.Fatalf("wrong content type, got %s want %s", w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } if w.Body.Len() != 54 { t.Fatalf("wrong len, got %d want %d", w.Body.Len(), 54) } } func TestCompressHandlerGzipDeflate(t *testing.T) { w := httptest.NewRecorder() compressedRequest(w, "gzip, deflate ") if w.HeaderMap.Get("Content-Encoding") != "gzip" { t.Fatalf("wrong content encoding, got %q want %q", w.HeaderMap.Get("Content-Encoding"), "gzip") } if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { t.Fatalf("wrong content type, got %s want %s", w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } } type fullyFeaturedResponseWriter struct{} // Header/Write/WriteHeader implement the http.ResponseWriter interface. func (fullyFeaturedResponseWriter) Header() http.Header { return http.Header{} } func (fullyFeaturedResponseWriter) Write([]byte) (int, error) { return 0, nil } func (fullyFeaturedResponseWriter) WriteHeader(int) {} // Flush implements the http.Flusher interface. func (fullyFeaturedResponseWriter) Flush() {} // Hijack implements the http.Hijacker interface. func (fullyFeaturedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } // CloseNotify implements the http.CloseNotifier interface. func (fullyFeaturedResponseWriter) CloseNotify() <-chan bool { return nil } func TestCompressHandlerPreserveInterfaces(t *testing.T) { // Compile time validation fullyFeaturedResponseWriter implements all the // interfaces we're asserting in the test case below. var ( _ http.Flusher = fullyFeaturedResponseWriter{} _ http.CloseNotifier = fullyFeaturedResponseWriter{} _ http.Hijacker = fullyFeaturedResponseWriter{} ) var h http.Handler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { comp := r.Header.Get("Accept-Encoding") if _, ok := rw.(*compressResponseWriter); !ok { t.Fatalf("ResponseWriter wasn't wrapped by compressResponseWriter, got %T type", rw) } if _, ok := rw.(http.Flusher); !ok { t.Errorf("ResponseWriter lost http.Flusher interface for %q", comp) } if _, ok := rw.(http.CloseNotifier); !ok { t.Errorf("ResponseWriter lost http.CloseNotifier interface for %q", comp) } if _, ok := rw.(http.Hijacker); !ok { t.Errorf("ResponseWriter lost http.Hijacker interface for %q", comp) } }) h = CompressHandler(h) var ( rw fullyFeaturedResponseWriter ) r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create test request: %v", err) } r.Header.Set("Accept-Encoding", "gzip") h.ServeHTTP(rw, r) r.Header.Set("Accept-Encoding", "deflate") h.ServeHTTP(rw, r) } handlers-1.4.2/cors.go000066400000000000000000000225661351550275600146370ustar00rootroot00000000000000package handlers import ( "net/http" "strconv" "strings" ) // CORSOption represents a functional option for configuring the CORS middleware. type CORSOption func(*cors) error type cors struct { h http.Handler allowedHeaders []string allowedMethods []string allowedOrigins []string allowedOriginValidator OriginValidator exposedHeaders []string maxAge int ignoreOptions bool allowCredentials bool optionStatusCode int } // OriginValidator takes an origin string and returns whether or not that origin is allowed. type OriginValidator func(string) bool var ( defaultCorsOptionStatusCode = 200 defaultCorsMethods = []string{"GET", "HEAD", "POST"} defaultCorsHeaders = []string{"Accept", "Accept-Language", "Content-Language", "Origin"} // (WebKit/Safari v9 sends the Origin header by default in AJAX requests) ) const ( corsOptionMethod string = "OPTIONS" corsAllowOriginHeader string = "Access-Control-Allow-Origin" corsExposeHeadersHeader string = "Access-Control-Expose-Headers" corsMaxAgeHeader string = "Access-Control-Max-Age" corsAllowMethodsHeader string = "Access-Control-Allow-Methods" corsAllowHeadersHeader string = "Access-Control-Allow-Headers" corsAllowCredentialsHeader string = "Access-Control-Allow-Credentials" corsRequestMethodHeader string = "Access-Control-Request-Method" corsRequestHeadersHeader string = "Access-Control-Request-Headers" corsOriginHeader string = "Origin" corsVaryHeader string = "Vary" corsOriginMatchAll string = "*" ) func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get(corsOriginHeader) if !ch.isOriginAllowed(origin) { if r.Method != corsOptionMethod || ch.ignoreOptions { ch.h.ServeHTTP(w, r) } return } if r.Method == corsOptionMethod { if ch.ignoreOptions { ch.h.ServeHTTP(w, r) return } if _, ok := r.Header[corsRequestMethodHeader]; !ok { w.WriteHeader(http.StatusBadRequest) return } method := r.Header.Get(corsRequestMethodHeader) if !ch.isMatch(method, ch.allowedMethods) { w.WriteHeader(http.StatusMethodNotAllowed) return } requestHeaders := strings.Split(r.Header.Get(corsRequestHeadersHeader), ",") allowedHeaders := []string{} for _, v := range requestHeaders { canonicalHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) if canonicalHeader == "" || ch.isMatch(canonicalHeader, defaultCorsHeaders) { continue } if !ch.isMatch(canonicalHeader, ch.allowedHeaders) { w.WriteHeader(http.StatusForbidden) return } allowedHeaders = append(allowedHeaders, canonicalHeader) } if len(allowedHeaders) > 0 { w.Header().Set(corsAllowHeadersHeader, strings.Join(allowedHeaders, ",")) } if ch.maxAge > 0 { w.Header().Set(corsMaxAgeHeader, strconv.Itoa(ch.maxAge)) } if !ch.isMatch(method, defaultCorsMethods) { w.Header().Set(corsAllowMethodsHeader, method) } } else { if len(ch.exposedHeaders) > 0 { w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ",")) } } if ch.allowCredentials { w.Header().Set(corsAllowCredentialsHeader, "true") } if len(ch.allowedOrigins) > 1 { w.Header().Set(corsVaryHeader, corsOriginHeader) } returnOrigin := origin if ch.allowedOriginValidator == nil && len(ch.allowedOrigins) == 0 { returnOrigin = "*" } else { for _, o := range ch.allowedOrigins { // A configuration of * is different than explicitly setting an allowed // origin. Returning arbitrary origin headers in an access control allow // origin header is unsafe and is not required by any use case. if o == corsOriginMatchAll { returnOrigin = "*" break } } } w.Header().Set(corsAllowOriginHeader, returnOrigin) if r.Method == corsOptionMethod { w.WriteHeader(ch.optionStatusCode) return } ch.h.ServeHTTP(w, r) } // CORS provides Cross-Origin Resource Sharing middleware. // Example: // // import ( // "net/http" // // "github.com/gorilla/handlers" // "github.com/gorilla/mux" // ) // // func main() { // r := mux.NewRouter() // r.HandleFunc("/users", UserEndpoint) // r.HandleFunc("/projects", ProjectEndpoint) // // // Apply the CORS middleware to our top-level router, with the defaults. // http.ListenAndServe(":8000", handlers.CORS()(r)) // } // func CORS(opts ...CORSOption) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { ch := parseCORSOptions(opts...) ch.h = h return ch } } func parseCORSOptions(opts ...CORSOption) *cors { ch := &cors{ allowedMethods: defaultCorsMethods, allowedHeaders: defaultCorsHeaders, allowedOrigins: []string{}, optionStatusCode: defaultCorsOptionStatusCode, } for _, option := range opts { option(ch) } return ch } // // Functional options for configuring CORS. // // AllowedHeaders adds the provided headers to the list of allowed headers in a // CORS request. // This is an append operation so the headers Accept, Accept-Language, // and Content-Language are always allowed. // Content-Type must be explicitly declared if accepting Content-Types other than // application/x-www-form-urlencoded, multipart/form-data, or text/plain. func AllowedHeaders(headers []string) CORSOption { return func(ch *cors) error { for _, v := range headers { normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) if normalizedHeader == "" { continue } if !ch.isMatch(normalizedHeader, ch.allowedHeaders) { ch.allowedHeaders = append(ch.allowedHeaders, normalizedHeader) } } return nil } } // AllowedMethods can be used to explicitly allow methods in the // Access-Control-Allow-Methods header. // This is a replacement operation so you must also // pass GET, HEAD, and POST if you wish to support those methods. func AllowedMethods(methods []string) CORSOption { return func(ch *cors) error { ch.allowedMethods = []string{} for _, v := range methods { normalizedMethod := strings.ToUpper(strings.TrimSpace(v)) if normalizedMethod == "" { continue } if !ch.isMatch(normalizedMethod, ch.allowedMethods) { ch.allowedMethods = append(ch.allowedMethods, normalizedMethod) } } return nil } } // AllowedOrigins sets the allowed origins for CORS requests, as used in the // 'Allow-Access-Control-Origin' HTTP header. // Note: Passing in a []string{"*"} will allow any domain. func AllowedOrigins(origins []string) CORSOption { return func(ch *cors) error { for _, v := range origins { if v == corsOriginMatchAll { ch.allowedOrigins = []string{corsOriginMatchAll} return nil } } ch.allowedOrigins = origins return nil } } // AllowedOriginValidator sets a function for evaluating allowed origins in CORS requests, represented by the // 'Allow-Access-Control-Origin' HTTP header. func AllowedOriginValidator(fn OriginValidator) CORSOption { return func(ch *cors) error { ch.allowedOriginValidator = fn return nil } } // OptionStatusCode sets a custom status code on the OPTIONS requests. // Default behaviour sets it to 200 to reflect best practices. This is option is not mandatory // and can be used if you need a custom status code (i.e 204). // // More informations on the spec: // https://fetch.spec.whatwg.org/#cors-preflight-fetch func OptionStatusCode(code int) CORSOption { return func(ch *cors) error { ch.optionStatusCode = code return nil } } // ExposedHeaders can be used to specify headers that are available // and will not be stripped out by the user-agent. func ExposedHeaders(headers []string) CORSOption { return func(ch *cors) error { ch.exposedHeaders = []string{} for _, v := range headers { normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v)) if normalizedHeader == "" { continue } if !ch.isMatch(normalizedHeader, ch.exposedHeaders) { ch.exposedHeaders = append(ch.exposedHeaders, normalizedHeader) } } return nil } } // MaxAge determines the maximum age (in seconds) between preflight requests. A // maximum of 10 minutes is allowed. An age above this value will default to 10 // minutes. func MaxAge(age int) CORSOption { return func(ch *cors) error { // Maximum of 10 minutes. if age > 600 { age = 600 } ch.maxAge = age return nil } } // IgnoreOptions causes the CORS middleware to ignore OPTIONS requests, instead // passing them through to the next handler. This is useful when your application // or framework has a pre-existing mechanism for responding to OPTIONS requests. func IgnoreOptions() CORSOption { return func(ch *cors) error { ch.ignoreOptions = true return nil } } // AllowCredentials can be used to specify that the user agent may pass // authentication details along with the request. func AllowCredentials() CORSOption { return func(ch *cors) error { ch.allowCredentials = true return nil } } func (ch *cors) isOriginAllowed(origin string) bool { if origin == "" { return false } if ch.allowedOriginValidator != nil { return ch.allowedOriginValidator(origin) } if len(ch.allowedOrigins) == 0 { return true } for _, allowedOrigin := range ch.allowedOrigins { if allowedOrigin == origin || allowedOrigin == corsOriginMatchAll { return true } } return false } func (ch *cors) isMatch(needle string, haystack []string) bool { for _, v := range haystack { if v == needle { return true } } return false } handlers-1.4.2/cors_test.go000066400000000000000000000305641351550275600156730ustar00rootroot00000000000000package handlers import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestDefaultCORSHandlerReturnsOk(t *testing.T) { r := newRequest("GET", "http://www.example.com/") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusFound) } } func TestDefaultCORSHandlerReturnsOkWithOrigin(t *testing.T) { r := newRequest("GET", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusFound) } } func TestCORSHandlerIgnoreOptionsFallsThrough(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }) CORS(IgnoreOptions())(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusTeapot { t.Fatalf("bad status: got %v want %v", status, http.StatusTeapot) } } func TestCORSHandlerSetsExposedHeaders(t *testing.T) { // Test default configuration. r := newRequest("GET", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(ExposedHeaders([]string{"X-CORS-TEST"}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsExposeHeadersHeader) if header != "X-Cors-Test" { t.Fatal("bad header: expected X-Cors-Test header, got empty header for method.") } } func TestCORSHandlerUnsetRequestMethodForPreflightBadRequest(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(AllowedMethods([]string{"DELETE"}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusBadRequest { t.Fatalf("bad status: got %v want %v", status, http.StatusBadRequest) } } func TestCORSHandlerInvalidRequestMethodForPreflightMethodNotAllowed(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "DELETE") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusMethodNotAllowed { t.Fatalf("bad status: got %v want %v", status, http.StatusMethodNotAllowed) } } func TestCORSHandlerOptionsRequestMustNotBePassedToNextHandler(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "GET") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("Options request must not be passed to next handler") }) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } } func TestCORSHandlerOptionsRequestMustNotBePassedToNextHandlerWithCustomStatusCode(t *testing.T) { statusCode := 204 r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "GET") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("Options request must not be passed to next handler") }) CORS(OptionStatusCode(statusCode))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != statusCode { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } } func TestCORSHandlerOptionsRequestMustNotBePassedToNextHandlerWhenOriginNotAllowed(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "GET") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("Options request must not be passed to next handler") }) CORS(AllowedOrigins([]string{}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } } func TestCORSHandlerAllowedMethodForPreflight(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "DELETE") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(AllowedMethods([]string{"DELETE"}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsAllowMethodsHeader) if header != "DELETE" { t.Fatalf("bad header: expected DELETE method header, got empty header.") } } func TestCORSHandlerAllowMethodsNotSetForSimpleRequestPreflight(t *testing.T) { for _, method := range defaultCorsMethods { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, method) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsAllowMethodsHeader) if header != "" { t.Fatalf("bad header: expected empty method header, got %s.", header) } } } func TestCORSHandlerAllowedHeaderNotSetForSimpleRequestPreflight(t *testing.T) { for _, simpleHeader := range defaultCorsHeaders { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "GET") r.Header.Set(corsRequestHeadersHeader, simpleHeader) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsAllowHeadersHeader) if header != "" { t.Fatalf("bad header: expected empty header, got %s.", header) } } } func TestCORSHandlerAllowedHeaderForPreflight(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "POST") r.Header.Set(corsRequestHeadersHeader, "Content-Type") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(AllowedHeaders([]string{"Content-Type"}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsAllowHeadersHeader) if header != "Content-Type" { t.Fatalf("bad header: expected Content-Type header, got empty header.") } } func TestCORSHandlerInvalidHeaderForPreflightForbidden(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "POST") r.Header.Set(corsRequestHeadersHeader, "Content-Type") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusForbidden { t.Fatalf("bad status: got %v want %v", status, http.StatusForbidden) } } func TestCORSHandlerMaxAgeForPreflight(t *testing.T) { r := newRequest("OPTIONS", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) r.Header.Set(corsRequestMethodHeader, "POST") rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(MaxAge(3500))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsMaxAgeHeader) if header != "600" { t.Fatalf("bad header: expected %s to be %s, got %s.", corsMaxAgeHeader, "600", header) } } func TestCORSHandlerAllowedCredentials(t *testing.T) { r := newRequest("GET", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(AllowCredentials())(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsAllowCredentialsHeader) if header != "true" { t.Fatalf("bad header: expected %s to be %s, got %s.", corsAllowCredentialsHeader, "true", header) } } func TestCORSHandlerMultipleAllowOriginsSetsVaryHeader(t *testing.T) { r := newRequest("GET", "http://www.example.com/") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS(AllowedOrigins([]string{r.URL.String(), "http://google.com"}))(testHandler).ServeHTTP(rr, r) if status := rr.Code; status != http.StatusOK { t.Fatalf("bad status: got %v want %v", status, http.StatusOK) } header := rr.HeaderMap.Get(corsVaryHeader) if header != corsOriginHeader { t.Fatalf("bad header: expected %s to be %s, got %s.", corsVaryHeader, corsOriginHeader, header) } } func TestCORSWithMultipleHandlers(t *testing.T) { var lastHandledBy string corsMiddleware := CORS() testHandler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lastHandledBy = "testHandler1" }) testHandler2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lastHandledBy = "testHandler2" }) r1 := newRequest("GET", "http://www.example.com/") rr1 := httptest.NewRecorder() handler1 := corsMiddleware(testHandler1) corsMiddleware(testHandler2) handler1.ServeHTTP(rr1, r1) if lastHandledBy != "testHandler1" { t.Fatalf("bad CORS() registration: Handler served should be Handler registered") } } func TestCORSOriginValidatorWithImplicitStar(t *testing.T) { r := newRequest("GET", "http://a.example.com") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) originValidator := func(origin string) bool { if strings.HasSuffix(origin, ".example.com") { return true } return false } CORS(AllowedOriginValidator(originValidator))(testHandler).ServeHTTP(rr, r) header := rr.HeaderMap.Get(corsAllowOriginHeader) if header != r.URL.String() { t.Fatalf("bad header: expected %s to be %s, got %s.", corsAllowOriginHeader, r.URL.String(), header) } } func TestCORSOriginValidatorWithExplicitStar(t *testing.T) { r := newRequest("GET", "http://a.example.com") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) originValidator := func(origin string) bool { if strings.HasSuffix(origin, ".example.com") { return true } return false } CORS( AllowedOriginValidator(originValidator), AllowedOrigins([]string{"*"}), )(testHandler).ServeHTTP(rr, r) header := rr.HeaderMap.Get(corsAllowOriginHeader) if header != "*" { t.Fatalf("bad header: expected %s to be %s, got %s.", corsAllowOriginHeader, "*", header) } } func TestCORSAllowStar(t *testing.T) { r := newRequest("GET", "http://a.example.com") r.Header.Set("Origin", r.URL.String()) rr := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(rr, r) header := rr.HeaderMap.Get(corsAllowOriginHeader) if header != "*" { t.Fatalf("bad header: expected %s to be %s, got %s.", corsAllowOriginHeader, "*", header) } } handlers-1.4.2/doc.go000066400000000000000000000005441351550275600144260ustar00rootroot00000000000000/* Package handlers is a collection of handlers (aka "HTTP middleware") for use with Go's net/http package (or any framework supporting http.Handler). The package includes handlers for logging in standardised formats, compressing HTTP responses, validating content types and other useful tools for manipulating requests and responses. */ package handlers handlers-1.4.2/go.mod000066400000000000000000000000431351550275600144320ustar00rootroot00000000000000module github.com/gorilla/handlers handlers-1.4.2/handlers.go000066400000000000000000000114351351550275600154620ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "bufio" "fmt" "net" "net/http" "sort" "strings" ) // MethodHandler is an http.Handler that dispatches to a handler whose key in the // MethodHandler's map matches the name of the HTTP request's method, eg: GET // // If the request's method is OPTIONS and OPTIONS is not a key in the map then // the handler responds with a status of 200 and sets the Allow header to a // comma-separated list of available methods. // // If the request's method doesn't match any of its keys the handler responds // with a status of HTTP 405 "Method Not Allowed" and sets the Allow header to a // comma-separated list of available methods. type MethodHandler map[string]http.Handler func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if handler, ok := h[req.Method]; ok { handler.ServeHTTP(w, req) } else { allow := []string{} for k := range h { allow = append(allow, k) } sort.Strings(allow) w.Header().Set("Allow", strings.Join(allow, ", ")) if req.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } } // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP // status code and body size type responseLogger struct { w http.ResponseWriter status int size int } func (l *responseLogger) Header() http.Header { return l.w.Header() } func (l *responseLogger) Write(b []byte) (int, error) { size, err := l.w.Write(b) l.size += size return size, err } func (l *responseLogger) WriteHeader(s int) { l.w.WriteHeader(s) l.status = s } func (l *responseLogger) Status() int { return l.status } func (l *responseLogger) Size() int { return l.size } func (l *responseLogger) Flush() { f, ok := l.w.(http.Flusher) if ok { f.Flush() } } type hijackLogger struct { responseLogger } func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { h := l.responseLogger.w.(http.Hijacker) conn, rw, err := h.Hijack() if err == nil && l.responseLogger.status == 0 { // The status will be StatusSwitchingProtocols if there was no error and // WriteHeader has not been called yet l.responseLogger.status = http.StatusSwitchingProtocols } return conn, rw, err } type closeNotifyWriter struct { loggingResponseWriter http.CloseNotifier } type hijackCloseNotifier struct { loggingResponseWriter http.Hijacker http.CloseNotifier } // isContentType validates the Content-Type header matches the supplied // contentType. That is, its type and subtype match. func isContentType(h http.Header, contentType string) bool { ct := h.Get("Content-Type") if i := strings.IndexRune(ct, ';'); i != -1 { ct = ct[0:i] } return ct == contentType } // ContentTypeHandler wraps and returns a http.Handler, validating the request // content type is compatible with the contentTypes list. It writes a HTTP 415 // error if that fails. // // Only PUT, POST, and PATCH requests are considered. func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") { h.ServeHTTP(w, r) return } for _, ct := range contentTypes { if isContentType(r.Header, ct) { h.ServeHTTP(w, r) return } } http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType) }) } const ( // HTTPMethodOverrideHeader is a commonly used // http header to override a request method. HTTPMethodOverrideHeader = "X-HTTP-Method-Override" // HTTPMethodOverrideFormKey is a commonly used // HTML form key to override a request method. HTTPMethodOverrideFormKey = "_method" ) // HTTPMethodOverrideHandler wraps and returns a http.Handler which checks for // the X-HTTP-Method-Override header or the _method form key, and overrides (if // valid) request.Method with its value. // // This is especially useful for HTTP clients that don't support many http verbs. // It isn't secure to override e.g a GET to a POST, so only POST requests are // considered. Likewise, the override method can only be a "write" method: PUT, // PATCH or DELETE. // // Form method takes precedence over header method. func HTTPMethodOverrideHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { om := r.FormValue(HTTPMethodOverrideFormKey) if om == "" { om = r.Header.Get(HTTPMethodOverrideHeader) } if om == "PUT" || om == "PATCH" || om == "DELETE" { r.Method = om } } h.ServeHTTP(w, r) }) } handlers-1.4.2/handlers_go18.go000066400000000000000000000011501351550275600163110ustar00rootroot00000000000000// +build go1.8 package handlers import ( "fmt" "net/http" ) type loggingResponseWriter interface { commonLoggingResponseWriter http.Pusher } func (l *responseLogger) Push(target string, opts *http.PushOptions) error { p, ok := l.w.(http.Pusher) if !ok { return fmt.Errorf("responseLogger does not implement http.Pusher") } return p.Push(target, opts) } func (c *compressResponseWriter) Push(target string, opts *http.PushOptions) error { p, ok := c.ResponseWriter.(http.Pusher) if !ok { return fmt.Errorf("compressResponseWriter does not implement http.Pusher") } return p.Push(target, opts) } handlers-1.4.2/handlers_go18_test.go000066400000000000000000000017031351550275600173540ustar00rootroot00000000000000// +build go1.8 package handlers import ( "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestLoggingHandlerWithPush(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if _, ok := w.(http.Pusher); !ok { t.Fatalf("%T from LoggingHandler does not satisfy http.Pusher interface when built with Go >=1.8", w) } w.WriteHeader(200) }) logger := LoggingHandler(ioutil.Discard, handler) logger.ServeHTTP(httptest.NewRecorder(), newRequest("GET", "/")) } func TestCombinedLoggingHandlerWithPush(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if _, ok := w.(http.Pusher); !ok { t.Fatalf("%T from CombinedLoggingHandler does not satisfy http.Pusher interface when built with Go >=1.8", w) } w.WriteHeader(200) }) logger := CombinedLoggingHandler(ioutil.Discard, handler) logger.ServeHTTP(httptest.NewRecorder(), newRequest("GET", "/")) } handlers-1.4.2/handlers_pre18.go000066400000000000000000000001521351550275600164730ustar00rootroot00000000000000// +build !go1.8 package handlers type loggingResponseWriter interface { commonLoggingResponseWriter } handlers-1.4.2/handlers_test.go000066400000000000000000000107301351550275600165160ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) const ( ok = "ok\n" notAllowed = "Method not allowed\n" ) var okHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(ok)) }) func newRequest(method, url string) *http.Request { req, err := http.NewRequest(method, url, nil) if err != nil { panic(err) } return req } func TestMethodHandler(t *testing.T) { tests := []struct { req *http.Request handler http.Handler code int allow string // Contents of the Allow header body string }{ // No handlers {newRequest("GET", "/foo"), MethodHandler{}, http.StatusMethodNotAllowed, "", notAllowed}, {newRequest("OPTIONS", "/foo"), MethodHandler{}, http.StatusOK, "", ""}, // A single handler {newRequest("GET", "/foo"), MethodHandler{"GET": okHandler}, http.StatusOK, "", ok}, {newRequest("POST", "/foo"), MethodHandler{"GET": okHandler}, http.StatusMethodNotAllowed, "GET", notAllowed}, // Multiple handlers {newRequest("GET", "/foo"), MethodHandler{"GET": okHandler, "POST": okHandler}, http.StatusOK, "", ok}, {newRequest("POST", "/foo"), MethodHandler{"GET": okHandler, "POST": okHandler}, http.StatusOK, "", ok}, {newRequest("DELETE", "/foo"), MethodHandler{"GET": okHandler, "POST": okHandler}, http.StatusMethodNotAllowed, "GET, POST", notAllowed}, {newRequest("OPTIONS", "/foo"), MethodHandler{"GET": okHandler, "POST": okHandler}, http.StatusOK, "GET, POST", ""}, // Override OPTIONS {newRequest("OPTIONS", "/foo"), MethodHandler{"OPTIONS": okHandler}, http.StatusOK, "", ok}, } for i, test := range tests { rec := httptest.NewRecorder() test.handler.ServeHTTP(rec, test.req) if rec.Code != test.code { t.Fatalf("%d: wrong code, got %d want %d", i, rec.Code, test.code) } if allow := rec.HeaderMap.Get("Allow"); allow != test.allow { t.Fatalf("%d: wrong Allow, got %s want %s", i, allow, test.allow) } if body := rec.Body.String(); body != test.body { t.Fatalf("%d: wrong body, got %q want %q", i, body, test.body) } } } func TestContentTypeHandler(t *testing.T) { tests := []struct { Method string AllowContentTypes []string ContentType string Code int }{ {"POST", []string{"application/json"}, "application/json", http.StatusOK}, {"POST", []string{"application/json", "application/xml"}, "application/json", http.StatusOK}, {"POST", []string{"application/json"}, "application/json; charset=utf-8", http.StatusOK}, {"POST", []string{"application/json"}, "application/json+xxx", http.StatusUnsupportedMediaType}, {"POST", []string{"application/json"}, "text/plain", http.StatusUnsupportedMediaType}, {"GET", []string{"application/json"}, "", http.StatusOK}, {"GET", []string{}, "", http.StatusOK}, } for _, test := range tests { r, err := http.NewRequest(test.Method, "/", nil) if err != nil { t.Error(err) continue } h := ContentTypeHandler(okHandler, test.AllowContentTypes...) r.Header.Set("Content-Type", test.ContentType) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != test.Code { t.Errorf("expected %d, got %d", test.Code, w.Code) } } } func TestHTTPMethodOverride(t *testing.T) { var tests = []struct { Method string OverrideMethod string ExpectedMethod string }{ {"POST", "PUT", "PUT"}, {"POST", "PATCH", "PATCH"}, {"POST", "DELETE", "DELETE"}, {"PUT", "DELETE", "PUT"}, {"GET", "GET", "GET"}, {"HEAD", "HEAD", "HEAD"}, {"GET", "PUT", "GET"}, {"HEAD", "DELETE", "HEAD"}, } for _, test := range tests { h := HTTPMethodOverrideHandler(okHandler) reqs := make([]*http.Request, 0, 2) rHeader, err := http.NewRequest(test.Method, "/", nil) if err != nil { t.Error(err) } rHeader.Header.Set(HTTPMethodOverrideHeader, test.OverrideMethod) reqs = append(reqs, rHeader) f := url.Values{HTTPMethodOverrideFormKey: []string{test.OverrideMethod}} rForm, err := http.NewRequest(test.Method, "/", strings.NewReader(f.Encode())) if err != nil { t.Error(err) } rForm.Header.Set("Content-Type", "application/x-www-form-urlencoded") reqs = append(reqs, rForm) for _, r := range reqs { w := httptest.NewRecorder() h.ServeHTTP(w, r) if r.Method != test.ExpectedMethod { t.Errorf("Expected %s, got %s", test.ExpectedMethod, r.Method) } } } } handlers-1.4.2/logging.go000066400000000000000000000160051351550275600153060ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "io" "net" "net/http" "net/url" "strconv" "time" "unicode/utf8" ) // Logging // LogFormatterParams is the structure any formatter will be handed when time to log comes type LogFormatterParams struct { Request *http.Request URL url.URL TimeStamp time.Time StatusCode int Size int } // LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler type LogFormatter func(writer io.Writer, params LogFormatterParams) // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its // friends type loggingHandler struct { writer io.Writer handler http.Handler formatter LogFormatter } func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { t := time.Now() logger := makeLogger(w) url := *req.URL h.handler.ServeHTTP(logger, req) if req.MultipartForm != nil { req.MultipartForm.RemoveAll() } params := LogFormatterParams{ Request: req, URL: url, TimeStamp: t, StatusCode: logger.Status(), Size: logger.Size(), } h.formatter(h.writer, params) } func makeLogger(w http.ResponseWriter) loggingResponseWriter { var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK} if _, ok := w.(http.Hijacker); ok { logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}} } h, ok1 := logger.(http.Hijacker) c, ok2 := w.(http.CloseNotifier) if ok1 && ok2 { return hijackCloseNotifier{logger, h, c} } if ok2 { return &closeNotifyWriter{logger, c} } return logger } type commonLoggingResponseWriter interface { http.ResponseWriter http.Flusher Status() int Size() int } const lowerhex = "0123456789abcdef" func appendQuoted(buf []byte, s string) []byte { var runeTmp [utf8.UTFMax]byte for width := 0; len(s) > 0; s = s[width:] { r := rune(s[0]) width = 1 if r >= utf8.RuneSelf { r, width = utf8.DecodeRuneInString(s) } if width == 1 && r == utf8.RuneError { buf = append(buf, `\x`...) buf = append(buf, lowerhex[s[0]>>4]) buf = append(buf, lowerhex[s[0]&0xF]) continue } if r == rune('"') || r == '\\' { // always backslashed buf = append(buf, '\\') buf = append(buf, byte(r)) continue } if strconv.IsPrint(r) { n := utf8.EncodeRune(runeTmp[:], r) buf = append(buf, runeTmp[:n]...) continue } switch r { case '\a': buf = append(buf, `\a`...) case '\b': buf = append(buf, `\b`...) case '\f': buf = append(buf, `\f`...) case '\n': buf = append(buf, `\n`...) case '\r': buf = append(buf, `\r`...) case '\t': buf = append(buf, `\t`...) case '\v': buf = append(buf, `\v`...) default: switch { case r < ' ': buf = append(buf, `\x`...) buf = append(buf, lowerhex[s[0]>>4]) buf = append(buf, lowerhex[s[0]&0xF]) case r > utf8.MaxRune: r = 0xFFFD fallthrough case r < 0x10000: buf = append(buf, `\u`...) for s := 12; s >= 0; s -= 4 { buf = append(buf, lowerhex[r>>uint(s)&0xF]) } default: buf = append(buf, `\U`...) for s := 28; s >= 0; s -= 4 { buf = append(buf, lowerhex[r>>uint(s)&0xF]) } } } } return buf } // buildCommonLogLine builds a log entry for req in Apache Common Log Format. // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte { username := "-" if url.User != nil { if name := url.User.Username(); name != "" { username = name } } host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { host = req.RemoteAddr } uri := req.RequestURI // Requests using the CONNECT method over HTTP/2.0 must use // the authority field (aka r.Host) to identify the target. // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT if req.ProtoMajor == 2 && req.Method == "CONNECT" { uri = req.Host } if uri == "" { uri = url.RequestURI() } buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) buf = append(buf, host...) buf = append(buf, " - "...) buf = append(buf, username...) buf = append(buf, " ["...) buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...) buf = append(buf, `] "`...) buf = append(buf, req.Method...) buf = append(buf, " "...) buf = appendQuoted(buf, uri) buf = append(buf, " "...) buf = append(buf, req.Proto...) buf = append(buf, `" `...) buf = append(buf, strconv.Itoa(status)...) buf = append(buf, " "...) buf = append(buf, strconv.Itoa(size)...) return buf } // writeLog writes a log entry for req to w in Apache Common Log Format. // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. func writeLog(writer io.Writer, params LogFormatterParams) { buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) buf = append(buf, '\n') writer.Write(buf) } // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format. // ts is the timestamp with which the entry should be logged. // status and size are used to provide the response HTTP status and size. func writeCombinedLog(writer io.Writer, params LogFormatterParams) { buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) buf = append(buf, ` "`...) buf = appendQuoted(buf, params.Request.Referer()) buf = append(buf, `" "`...) buf = appendQuoted(buf, params.Request.UserAgent()) buf = append(buf, '"', '\n') writer.Write(buf) } // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in // Apache Combined Log Format. // // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. // // LoggingHandler always sets the ident field of the log to - func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { return loggingHandler{out, h, writeCombinedLog} } // LoggingHandler return a http.Handler that wraps h and logs requests to out in // Apache Common Log Format (CLF). // // See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format. // // LoggingHandler always sets the ident field of the log to - // // Example: // // r := mux.NewRouter() // r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // w.Write([]byte("This is a catch-all route")) // }) // loggedRouter := handlers.LoggingHandler(os.Stdout, r) // http.ListenAndServe(":1123", loggedRouter) // func LoggingHandler(out io.Writer, h http.Handler) http.Handler { return loggingHandler{out, h, writeLog} } // CustomLoggingHandler provides a way to supply a custom log formatter // while taking advantage of the mechanisms in this package func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler { return loggingHandler{out, h, f} } handlers-1.4.2/logging_test.go000066400000000000000000000242721351550275600163520ustar00rootroot00000000000000// Copyright 2013 The Gorilla Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package handlers import ( "bytes" "encoding/base64" "fmt" "io/ioutil" "math/rand" "mime/multipart" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" ) func TestMakeLogger(t *testing.T) { rec := httptest.NewRecorder() logger := makeLogger(rec) // initial status if logger.Status() != http.StatusOK { t.Fatalf("wrong status, got %d want %d", logger.Status(), http.StatusOK) } // WriteHeader logger.WriteHeader(http.StatusInternalServerError) if logger.Status() != http.StatusInternalServerError { t.Fatalf("wrong status, got %d want %d", logger.Status(), http.StatusInternalServerError) } // Write logger.Write([]byte(ok)) if logger.Size() != len(ok) { t.Fatalf("wrong size, got %d want %d", logger.Size(), len(ok)) } // Header logger.Header().Set("key", "value") if val := logger.Header().Get("key"); val != "value" { t.Fatalf("wrong header, got %s want %s", val, "value") } } func TestLoggerCleanup(t *testing.T) { rand.Seed(time.Now().UnixNano()) rbuf := make([]byte, 128) if _, err := rand.Read(rbuf); err != nil { t.Fatalf("Failed to generate random content: %v", err) } contents := base64.StdEncoding.EncodeToString(rbuf) var body bytes.Buffer body.WriteString(fmt.Sprintf(` --boundary Content-Disposition: form-data; name="buzz"; filename="example.txt" %s --boundary-- `, contents)) r := multipart.NewReader(&body, "boundary") form, err := r.ReadForm(0) // small max memory to force flush to disk if err != nil { t.Fatalf("Failed to read multipart form: %v", err) } tmpFiles, err := ioutil.ReadDir(os.TempDir()) if err != nil { t.Fatalf("Failed to list %s: %v", os.TempDir(), err) } var tmpFile string for _, f := range tmpFiles { if !strings.HasPrefix(f.Name(), "multipart-") { continue } path := filepath.Join(os.TempDir(), f.Name()) switch b, err := ioutil.ReadFile(path); { case err != nil: t.Fatalf("Failed to read %s: %v", path, err) case string(b) != contents: continue default: tmpFile = path break } } if tmpFile == "" { t.Fatal("Could not find multipart form tmp file") } req := newRequest("GET", "/subdir/asdf") req.MultipartForm = form var buf bytes.Buffer handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.URL.Path = "/" // simulate http.StripPrefix and friends w.WriteHeader(200) }) logger := LoggingHandler(&buf, handler) logger.ServeHTTP(httptest.NewRecorder(), req) if _, err := os.Stat(tmpFile); err == nil || !os.IsNotExist(err) { t.Fatalf("Expected %s to not exist, got %v", tmpFile, err) } } func TestLogPathRewrites(t *testing.T) { var buf bytes.Buffer handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.URL.Path = "/" // simulate http.StripPrefix and friends w.WriteHeader(200) }) logger := LoggingHandler(&buf, handler) logger.ServeHTTP(httptest.NewRecorder(), newRequest("GET", "/subdir/asdf")) if !strings.Contains(buf.String(), "GET /subdir/asdf HTTP") { t.Fatalf("Got log %#v, wanted substring %#v", buf.String(), "GET /subdir/asdf HTTP") } } func BenchmarkWriteLog(b *testing.B) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { b.Fatalf(err.Error()) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) req := newRequest("GET", "http://example.com") req.RemoteAddr = "192.168.100.5" b.ResetTimer() params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusUnauthorized, Size: 500, } buf := &bytes.Buffer{} for i := 0; i < b.N; i++ { buf.Reset() writeLog(buf, params) } } func TestLogFormatterWriteLog_Scenario1(t *testing.T) { formatter := writeLog expected := "192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 200 100\n" LoggingScenario1(t, formatter, expected) } func TestLogFormatterCombinedLog_Scenario1(t *testing.T) { formatter := writeCombinedLog expected := "192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 200 100 \"http://example.com\" " + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" LoggingScenario1(t, formatter, expected) } func TestLogFormatterWriteLog_Scenario2(t *testing.T) { formatter := writeLog expected := "192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"CONNECT www.example.com:443 HTTP/2.0\" 200 100\n" LoggingScenario2(t, formatter, expected) } func TestLogFormatterCombinedLog_Scenario2(t *testing.T) { formatter := writeCombinedLog expected := "192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"CONNECT www.example.com:443 HTTP/2.0\" 200 100 \"http://example.com\" " + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" LoggingScenario2(t, formatter, expected) } func TestLogFormatterWriteLog_Scenario3(t *testing.T) { formatter := writeLog expected := "192.168.100.5 - kamil [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 401 500\n" LoggingScenario3(t, formatter, expected) } func TestLogFormatterCombinedLog_Scenario3(t *testing.T) { formatter := writeCombinedLog expected := "192.168.100.5 - kamil [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 401 500 \"http://example.com\" " + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" LoggingScenario3(t, formatter, expected) } func TestLogFormatterWriteLog_Scenario4(t *testing.T) { formatter := writeLog expected := "192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"GET /test?abc=hello%20world&a=b%3F HTTP/1.1\" 200 100\n" LoggingScenario4(t, formatter, expected) } func TestLogFormatterCombinedLog_Scenario5(t *testing.T) { formatter := writeCombinedLog expected := "::1 - kamil [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 200 100 \"http://example.com\" " + "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " + "AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n" LoggingScenario5(t, formatter, expected) } func LoggingScenario1(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { panic(err) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) // A typical request with an OK response req := constructTypicalRequestOk() buf := new(bytes.Buffer) params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusOK, Size: 100, } formatter(buf, params) log := buf.String() if log != expected { t.Fatalf("wrong log, got %q want %q", log, expected) } } func LoggingScenario2(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { panic(err) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) // CONNECT request over http/2.0 req := constructConnectRequest() buf := new(bytes.Buffer) params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusOK, Size: 100, } formatter(buf, params) log := buf.String() if log != expected { t.Fatalf("wrong log, got %q want %q", log, expected) } } func LoggingScenario3(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { panic(err) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) // Request with an unauthorized user req := constructTypicalRequestOk() req.URL.User = url.User("kamil") buf := new(bytes.Buffer) params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusUnauthorized, Size: 500, } formatter(buf, params) log := buf.String() if log != expected { t.Fatalf("wrong log, got %q want %q", log, expected) } } func LoggingScenario4(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { panic(err) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) // Request with url encoded parameters req := constructEncodedRequest() buf := new(bytes.Buffer) params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusOK, Size: 100, } formatter(buf, params) log := buf.String() if log != expected { t.Fatalf("wrong log, got %q want %q", log, expected) } } func LoggingScenario5(t *testing.T, formatter LogFormatter, expected string) { loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { panic(err) } ts := time.Date(1983, 05, 26, 3, 30, 45, 0, loc) req := constructTypicalRequestOk() req.URL.User = url.User("kamil") req.RemoteAddr = "::1" buf := new(bytes.Buffer) params := LogFormatterParams{ Request: req, URL: *req.URL, TimeStamp: ts, StatusCode: http.StatusOK, Size: 100, } formatter(buf, params) log := buf.String() if log != expected { t.Fatalf("wrong log, got %q want %q", log, expected) } } // A typical request with an OK response func constructTypicalRequestOk() *http.Request { req := newRequest("GET", "http://example.com") req.RemoteAddr = "192.168.100.5" req.Header.Set("Referer", "http://example.com") req.Header.Set( "User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.33 "+ "(KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33", ) return req } // CONNECT request over http/2.0 func constructConnectRequest() *http.Request { req := &http.Request{ Method: "CONNECT", Host: "www.example.com:443", Proto: "HTTP/2.0", ProtoMajor: 2, ProtoMinor: 0, RemoteAddr: "192.168.100.5", Header: http.Header{}, URL: &url.URL{Host: "www.example.com:443"}, } req.Header.Set("Referer", "http://example.com") req.Header.Set( "User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.33 "+ "(KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33", ) return req } func constructEncodedRequest() *http.Request { req := constructTypicalRequestOk() req.URL, _ = url.Parse("http://example.com/test?abc=hello%20world&a=b%3F") return req } handlers-1.4.2/proxy_headers.go000066400000000000000000000110051351550275600165270ustar00rootroot00000000000000package handlers import ( "net/http" "regexp" "strings" ) var ( // De-facto standard header keys. xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host") xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme") xRealIP = http.CanonicalHeaderKey("X-Real-IP") ) var ( // RFC7239 defines a new "Forwarded: " header designed to replace the // existing use of X-Forwarded-* headers. // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 forwarded = http.CanonicalHeaderKey("Forwarded") // Allows for a sub-match of the first value after 'for=' to the next // comma, semi-colon or space. The match is case-insensitive. forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`) // Allows for a sub-match for the first instance of scheme (http|https) // prefixed by 'proto='. The match is case-insensitive. protoRegex = regexp.MustCompile(`(?i)(?:proto=)(https|http)`) ) // ProxyHeaders inspects common reverse proxy headers and sets the corresponding // fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP // for the remote (client) IP address, X-Forwarded-Proto or X-Forwarded-Scheme // for the scheme (http|https), X-Forwarded-Host for the host and the RFC7239 // Forwarded header, which may include both client IPs and schemes. // // NOTE: This middleware should only be used when behind a reverse // proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are // configured not to) strip these headers from client requests, or where these // headers are accepted "as is" from a remote client (e.g. when Go is not behind // a proxy), can manifest as a vulnerability if your application uses these // headers for validating the 'trustworthiness' of a request. func ProxyHeaders(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // Set the remote IP with the value passed from the proxy. if fwd := getIP(r); fwd != "" { r.RemoteAddr = fwd } // Set the scheme (proto) with the value passed from the proxy. if scheme := getScheme(r); scheme != "" { r.URL.Scheme = scheme } // Set the host with the value passed by the proxy if r.Header.Get(xForwardedHost) != "" { r.Host = r.Header.Get(xForwardedHost) } // Call the next handler in the chain. h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } // getIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239 // Forwarded headers (in that order). func getIP(r *http.Request) string { var addr string if fwd := r.Header.Get(xForwardedFor); fwd != "" { // Only grab the first (client) address. Note that '192.168.0.1, // 10.1.1.1' is a valid key for X-Forwarded-For where addresses after // the first may represent forwarding proxies earlier in the chain. s := strings.Index(fwd, ", ") if s == -1 { s = len(fwd) } addr = fwd[:s] } else if fwd := r.Header.Get(xRealIP); fwd != "" { // X-Real-IP should only contain one IP address (the client making the // request). addr = fwd } else if fwd := r.Header.Get(forwarded); fwd != "" { // match should contain at least two elements if the protocol was // specified in the Forwarded header. The first element will always be // the 'for=' capture, which we ignore. In the case of multiple IP // addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only // extract the first, which should be the client IP. if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 { // IPv6 addresses in Forwarded headers are quoted-strings. We strip // these quotes. addr = strings.Trim(match[1], `"`) } } return addr } // getScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239 // Forwarded headers (in that order). func getScheme(r *http.Request) string { var scheme string // Retrieve the scheme from X-Forwarded-Proto. if proto := r.Header.Get(xForwardedProto); proto != "" { scheme = strings.ToLower(proto) } else if proto = r.Header.Get(xForwardedScheme); proto != "" { scheme = strings.ToLower(proto) } else if proto = r.Header.Get(forwarded); proto != "" { // match should contain at least two elements if the protocol was // specified in the Forwarded header. The first element will always be // the 'proto=' capture, which we ignore. In the case of multiple proto // parameters (invalid) we only extract the first. if match := protoRegex.FindStringSubmatch(proto); len(match) > 1 { scheme = strings.ToLower(match[1]) } } return scheme } handlers-1.4.2/proxy_headers_test.go000066400000000000000000000067071351550275600176030ustar00rootroot00000000000000package handlers import ( "net/http" "net/http/httptest" "testing" ) type headerTable struct { key string // header key val string // header val expected string // expected result } func TestGetIP(t *testing.T) { headers := []headerTable{ {xForwardedFor, "8.8.8.8", "8.8.8.8"}, // Single address {xForwardedFor, "8.8.8.8, 8.8.4.4", "8.8.8.8"}, // Multiple {xForwardedFor, "[2001:db8:cafe::17]:4711", "[2001:db8:cafe::17]:4711"}, // IPv6 address {xForwardedFor, "", ""}, // None {xRealIP, "8.8.8.8", "8.8.8.8"}, // Single address {xRealIP, "8.8.8.8, 8.8.4.4", "8.8.8.8, 8.8.4.4"}, // Multiple {xRealIP, "[2001:db8:cafe::17]:4711", "[2001:db8:cafe::17]:4711"}, // IPv6 address {xRealIP, "", ""}, // None {forwarded, `for="_gazonk"`, "_gazonk"}, // Hostname {forwarded, `For="[2001:db8:cafe::17]:4711`, `[2001:db8:cafe::17]:4711`}, // IPv6 address {forwarded, `for=192.0.2.60;proto=http;by=203.0.113.43`, `192.0.2.60`}, // Multiple params {forwarded, `for=192.0.2.43, for=198.51.100.17`, "192.0.2.43"}, // Multiple params {forwarded, `for="workstation.local",for=198.51.100.17`, "workstation.local"}, // Hostname } for _, v := range headers { req := &http.Request{ Header: http.Header{ v.key: []string{v.val}, }} res := getIP(req) if res != v.expected { t.Fatalf("wrong header for %s: got %s want %s", v.key, res, v.expected) } } } func TestGetScheme(t *testing.T) { headers := []headerTable{ {xForwardedProto, "https", "https"}, {xForwardedProto, "http", "http"}, {xForwardedProto, "HTTP", "http"}, {xForwardedScheme, "https", "https"}, {xForwardedScheme, "http", "http"}, {xForwardedScheme, "HTTP", "http"}, {forwarded, `For="[2001:db8:cafe::17]:4711`, ""}, // No proto {forwarded, `for=192.0.2.43, for=198.51.100.17;proto=https`, "https"}, // Multiple params before proto {forwarded, `for=172.32.10.15; proto=https;by=127.0.0.1`, "https"}, // Space before proto {forwarded, `for=192.0.2.60;proto=http;by=203.0.113.43`, "http"}, // Multiple params } for _, v := range headers { req := &http.Request{ Header: http.Header{ v.key: []string{v.val}, }, } res := getScheme(req) if res != v.expected { t.Fatalf("wrong header for %s: got %s want %s", v.key, res, v.expected) } } } // Test the middleware end-to-end func TestProxyHeaders(t *testing.T) { rr := httptest.NewRecorder() r := newRequest("GET", "/") r.Header.Set(xForwardedFor, "8.8.8.8") r.Header.Set(xForwardedProto, "https") r.Header.Set(xForwardedHost, "google.com") var ( addr string proto string host string ) ProxyHeaders(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { addr = r.RemoteAddr proto = r.URL.Scheme host = r.Host })).ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("bad status: got %d want %d", rr.Code, http.StatusOK) } if addr != r.Header.Get(xForwardedFor) { t.Fatalf("wrong address: got %s want %s", addr, r.Header.Get(xForwardedFor)) } if proto != r.Header.Get(xForwardedProto) { t.Fatalf("wrong address: got %s want %s", proto, r.Header.Get(xForwardedProto)) } if host != r.Header.Get(xForwardedHost) { t.Fatalf("wrong address: got %s want %s", host, r.Header.Get(xForwardedHost)) } } handlers-1.4.2/recovery.go000066400000000000000000000041451351550275600155200ustar00rootroot00000000000000package handlers import ( "log" "net/http" "runtime/debug" ) // RecoveryHandlerLogger is an interface used by the recovering handler to print logs. type RecoveryHandlerLogger interface { Println(...interface{}) } type recoveryHandler struct { handler http.Handler logger RecoveryHandlerLogger printStack bool } // RecoveryOption provides a functional approach to define // configuration for a handler; such as setting the logging // whether or not to print strack traces on panic. type RecoveryOption func(http.Handler) func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler { for _, option := range opts { option(h) } return h } // RecoveryHandler is HTTP middleware that recovers from a panic, // logs the panic, writes http.StatusInternalServerError, and // continues to the next handler. // // Example: // // r := mux.NewRouter() // r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // panic("Unexpected error!") // }) // // http.ListenAndServe(":1123", handlers.RecoveryHandler()(r)) func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler { r := &recoveryHandler{handler: h} return parseRecoveryOptions(r, opts...) } } // RecoveryLogger is a functional option to override // the default logger func RecoveryLogger(logger RecoveryHandlerLogger) RecoveryOption { return func(h http.Handler) { r := h.(*recoveryHandler) r.logger = logger } } // PrintRecoveryStack is a functional option to enable // or disable printing stack traces on panic. func PrintRecoveryStack(print bool) RecoveryOption { return func(h http.Handler) { r := h.(*recoveryHandler) r.printStack = print } } func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { defer func() { if err := recover(); err != nil { w.WriteHeader(http.StatusInternalServerError) h.log(err) } }() h.handler.ServeHTTP(w, req) } func (h recoveryHandler) log(v ...interface{}) { if h.logger != nil { h.logger.Println(v...) } else { log.Println(v...) } if h.printStack { debug.PrintStack() } } handlers-1.4.2/recovery_test.go000066400000000000000000000022511351550275600165530ustar00rootroot00000000000000package handlers import ( "bytes" "log" "net/http" "net/http/httptest" "strings" "testing" ) func TestRecoveryLoggerWithDefaultOptions(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) handler := RecoveryHandler() handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { panic("Unexpected error!") }) recovery := handler(handlerFunc) recovery.ServeHTTP(httptest.NewRecorder(), newRequest("GET", "/subdir/asdf")) if !strings.Contains(buf.String(), "Unexpected error!") { t.Fatalf("Got log %#v, wanted substring %#v", buf.String(), "Unexpected error!") } } func TestRecoveryLoggerWithCustomLogger(t *testing.T) { var buf bytes.Buffer var logger = log.New(&buf, "", log.LstdFlags) handler := RecoveryHandler(RecoveryLogger(logger), PrintRecoveryStack(false)) handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { panic("Unexpected error!") }) recovery := handler(handlerFunc) recovery.ServeHTTP(httptest.NewRecorder(), newRequest("GET", "/subdir/asdf")) if !strings.Contains(buf.String(), "Unexpected error!") { t.Fatalf("Got log %#v, wanted substring %#v", buf.String(), "Unexpected error!") } }