diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2d5fd5d0053be1ede119e3462d8024c660e7064e..bc84f57a44c6b73bbdc9dd3a1c6acb7981a87996 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -233,6 +233,10 @@ "ImportPath": "github.com/mtchavez/jenkins", "Rev": "5a816af6ef21ef401bff5e4b7dd255d63400f497" }, + { + "ImportPath": "github.com/rs/cors", + "Rev": "5e4ce6bc0ecd3472f6f943666d84876691be2ced" + }, { "ImportPath": "github.com/steakknife/hamming", "Comment": "0.0.10", diff --git a/Godeps/_workspace/src/github.com/rs/cors/.travis.yml b/Godeps/_workspace/src/github.com/rs/cors/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..bbb5185a2edc87ed46f15fd234ea3243e3aebfad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/.travis.yml @@ -0,0 +1,4 @@ +language: go +go: +- 1.3 +- 1.4 diff --git a/Godeps/_workspace/src/github.com/rs/cors/LICENSE b/Godeps/_workspace/src/github.com/rs/cors/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d8e2df5a479a47e6afe311efdda70e2b541ad7a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Olivier Poitrey <rs@dailymotion.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/rs/cors/README.md b/Godeps/_workspace/src/github.com/rs/cors/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4fe320cf3429354fe3df8beeab81610ee24d602d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/README.md @@ -0,0 +1,96 @@ +# Go CORS handler [](https://godoc.org/github.com/rs/cors) [](https://raw.githubusercontent.com/rs/cors/master/LICENSE) [](https://travis-ci.org/rs/cors) + +CORS is a `net/http` handler implementing [Cross Origin Resource Sharing W3 specification](http://www.w3.org/TR/cors/) in Golang. + +## Getting Started + +After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first `.go` file. We'll call it `server.go`. + +```go +package main + +import ( + "net/http" + + "github.com/rs/cors" +) + +func main() { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + // cors.Default() setup the middleware with default options being + // all origins accepted with simple methods (GET, POST). See + // documentation below for more options. + handler := cors.Default().Handler(h) + http.ListenAndServe(":8080", handler) +} +``` + +Install `cors`: + + go get github.com/rs/cors + +Then run your server: + + go run server.go + +The server now runs on `localhost:8080`: + + $ curl -D - -H 'Origin: http://foo.com' http://localhost:8080/ + HTTP/1.1 200 OK + Access-Control-Allow-Origin: foo.com + Content-Type: application/json + Date: Sat, 25 Oct 2014 03:43:57 GMT + Content-Length: 18 + + {"hello": "world"} + +### More Examples + +* `net/http`: [examples/nethttp/server.go](https://github.com/rs/cors/blob/master/examples/nethttp/server.go) +* [Goji](https://goji.io): [examples/goji/server.go](https://github.com/rs/cors/blob/master/examples/goji/server.go) +* [Martini](http://martini.codegangsta.io): [examples/martini/server.go](https://github.com/rs/cors/blob/master/examples/martini/server.go) +* [Negroni](https://github.com/codegangsta/negroni): [examples/negroni/server.go](https://github.com/rs/cors/blob/master/examples/negroni/server.go) +* [Alice](https://github.com/justinas/alice): [examples/alice/server.go](https://github.com/rs/cors/blob/master/examples/alice/server.go) + +## Parameters + +Parameters are passed to the middleware thru the `cors.New` method as follow: + +```go +c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + AllowCredentials: true, +}) + +// Insert the middleware +handler = c.Handler(handler) +``` + +* **AllowedOrigins** `[]string`: A list of origins a cross-domain request can be executed from. If the special `*` value is present in the list, all origins will be allowed. The default value is `*`. +* **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It take the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored +* **AllowedMethods** `[]string`: A list of methods the client is allowed to use with cross-domain requests. +* **AllowedHeaders** `[]string`: A list of non simple headers the client is allowed to use with cross-domain requests. Default value is simple methods (`GET` and `POST`) +* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification +* **AllowCredentials** `bool`: Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. The default is `false`. +* **MaxAge** `int`: Indicates how long (in seconds) the results of a preflight request can be cached. The default is `0` which stands for no max age. + +See [API documentation](http://godoc.org/github.com/rs/cors) for more info. + +## Benchmarks + + BenchmarkWithout 20000000 64.6 ns/op 8 B/op 1 allocs/op + BenchmarkDefault 3000000 469 ns/op 114 B/op 2 allocs/op + BenchmarkAllowedOrigin 3000000 608 ns/op 114 B/op 2 allocs/op + BenchmarkPreflight 20000000 73.2 ns/op 0 B/op 0 allocs/op + BenchmarkPreflightHeader 20000000 73.6 ns/op 0 B/op 0 allocs/op + BenchmarkParseHeaderList 2000000 847 ns/op 184 B/op 6 allocs/op + BenchmarkParse…Single 5000000 290 ns/op 32 B/op 3 allocs/op + BenchmarkParse…Normalized 2000000 776 ns/op 160 B/op 6 allocs/op + +## Licenses + +All source code is licensed under the [MIT License](https://raw.github.com/rs/cors/master/LICENSE). diff --git a/Godeps/_workspace/src/github.com/rs/cors/bench_test.go b/Godeps/_workspace/src/github.com/rs/cors/bench_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b6e3721decccc2b4dd74f272f6a663c78c786b93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/bench_test.go @@ -0,0 +1,88 @@ +package cors + +import ( + "net/http" + "testing" +) + +type FakeResponse struct { + header http.Header +} + +func (r FakeResponse) Header() http.Header { + return r.header +} + +func (r FakeResponse) WriteHeader(n int) { +} + +func (r FakeResponse) Write(b []byte) (n int, err error) { + return len(b), nil +} + +func BenchmarkWithout(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + testHandler.ServeHTTP(res, req) + } +} + +func BenchmarkDefault(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "somedomain.com") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkAllowedOrigin(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "somedomain.com") + c := New(Options{ + AllowedOrigins: []string{"somedomain.com"}, + }) + handler := c.Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkPreflight(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Access-Control-Request-Method", "GET") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkPreflightHeader(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "Accept") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/cors.go b/Godeps/_workspace/src/github.com/rs/cors/cors.go new file mode 100644 index 0000000000000000000000000000000000000000..1ec676e3a9addf7e8fb3d17281df35c295499293 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/cors.go @@ -0,0 +1,345 @@ +/* +Package cors is net/http handler to handle CORS related requests +as defined by http://www.w3.org/TR/cors/ + +You can configure it by passing an option struct to cors.New: + + c := cors.New(cors.Options{ + AllowedOrigins: []string{"foo.com"}, + AllowedMethods: []string{"GET", "POST", "DELETE"}, + AllowCredentials: true, + }) + +Then insert the handler in the chain: + + handler = c.Handler(handler) + +See Options documentation for more options. + +The resulting handler is a standard net/http handler. +*/ +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // Default value is ["*"] + AllowedOrigins []string + // AllowOriginFunc is a custom function to validate the origin. It take the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(origin string) bool + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (GET and POST) + AllowedMethods []string + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +type Cors struct { + // Debug logger + log *log.Logger + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + // Normalized list of allowed origins + allowedOrigins []string + // Optional origin validator function + allowOriginFunc func(origin string) bool + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + // Normalized list of allowed headers + allowedHeaders []string + // Normalized list of allowed methods + allowedMethods []string + // Normalized list of exposed headers + exposedHeaders []string + allowCredentials bool + maxAge int +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + } + if options.Debug { + c.log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + // Default is all origins + c.allowedOriginsAll = true + } else { + c.allowedOrigins = convert(options.AllowedOrigins, strings.ToLower) + for _, o := range c.allowedOrigins { + if o == "*" { + c.allowedOriginsAll = true + c.allowedOrigins = nil + break + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{"GET", "POST"} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Default creates a new Cors handler with default options +func Default() *Cors { + return New(Options{}) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + h.ServeHTTP(w, r) + } + }) +} + +// Martini compatible handler +func (c *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + c.logf("HandlerFunc: Preflight request") + c.handlePreflight(w, r) + } else { + c.logf("HandlerFunc: Actual request") + c.handleActualRequest(w, r) + } +} + +// Negroni compatible interface +func (c *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Method == "OPTIONS" { + c.logf("ServeHTTP: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + } else { + c.logf("ServeHTTP: Actual request") + c.handleActualRequest(w, r) + next(w, r) + } +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != "OPTIONS" { + c.logf(" Preflight aborted: %s!=OPTIONS", r.Method) + return + } + if origin == "" { + c.logf(" Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(origin) { + c.logf(" Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf(" Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + headers.Set("Access-Control-Allow-Origin", origin) + headers.Add("Vary", "Origin") + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf(" Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method == "OPTIONS" { + c.logf(" Actual request no headers added: method == %s", r.Method) + return + } + if origin == "" { + c.logf(" Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(origin) { + c.logf(" Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + if c.log != nil { + c.logf(" Actual request no headers added: method '%s' not allowed", + r.Method) + } + + return + } + headers.Set("Access-Control-Allow-Origin", origin) + headers.Add("Vary", "Origin") + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf(" Actual response added headers: %v", headers) +} + +// convenience method. checks if debugging is turned on before printing +func (c *Cors) logf(format string, a ...interface{}) { + if c.log != nil { + c.log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoing +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == "OPTIONS" { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + } + } + if !found { + return false + } + } + return true +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/cors_test.go b/Godeps/_workspace/src/github.com/rs/cors/cors_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1a7f0e4a4b8a18c7a2e257d1530e2dd0aa9d9c3c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/cors_test.go @@ -0,0 +1,315 @@ +package cors + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +var testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("bar")) +}) + +func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) { + for name, value := range reqHeaders { + if resHeaders.Get(name) != value { + t.Errorf("Invalid header `%s', wanted `%s', got `%s'", name, value, resHeaders.Get(name)) + } + } +} + +func TestNoConfig(t *testing.T) { + s := New(Options{ + // Intentionally left blank. + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestWildcardOrigin(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"*"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestAllowedOrigin(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestDisallowedOrigin(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://barbaz.com") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestAllowedOriginFunc(t *testing.T) { + r, _ := regexp.Compile("^http://foo") + s := New(Options{ + AllowOriginFunc: func(o string) bool { + println(r.MatchString(o)) + return r.MatchString(o) + }, + }) + + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + res := httptest.NewRecorder() + req.Header.Set("Origin", "http://foobar.com") + s.Handler(testHandler).ServeHTTP(res, req) + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + }) + + res = httptest.NewRecorder() + req.Header.Set("Origin", "http://barfoo.com") + s.Handler(testHandler).ServeHTTP(res, req) + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + }) +} + +func TestAllowedMethod(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedMethods: []string{"PUT", "DELETE"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "PUT") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "PUT", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestDisallowedMethod(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedMethods: []string{"PUT", "DELETE"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "PATCH") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestAllowedHeader(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"X-Header-1", "x-header-2"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "X-Header-2, X-HEADER-1") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestAllowedWildcardHeader(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"*"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "X-Header-2, X-HEADER-1") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestDisallowedHeader(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"X-Header-1", "x-header-2"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "X-Header-3, X-Header-1") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestOriginHeader(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "origin") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Origin", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestExposedHeader(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + ExposedHeaders: []string{"X-Header-1", "x-header-2"}, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "X-Header-1, X-Header-2", + }) +} + +func TestAllowedCredentials(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowCredentials: true, + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + req.Header.Add("Access-Control-Request-Method", "GET") + + s.Handler(testHandler).ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/alice/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/alice/server.go new file mode 100644 index 0000000000000000000000000000000000000000..00a6a0bf1bdf794c529cec6469f0296798a9cb85 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/alice/server.go @@ -0,0 +1,24 @@ +package main + +import ( + "net/http" + + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + "github.com/justinas/alice" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + chain := alice.New(c.Handler).Then(mux) + http.ListenAndServe(":8080", chain) +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/default/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/default/server.go new file mode 100644 index 0000000000000000000000000000000000000000..94e49bb5b0e9c60cb32250a5be5488e537a7a00b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/default/server.go @@ -0,0 +1,18 @@ +package main + +import ( + "net/http" + + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" +) + +func main() { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + // Use default options + handler := cors.Default().Handler(h) + http.ListenAndServe(":8080", handler) +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/goji/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/goji/server.go new file mode 100644 index 0000000000000000000000000000000000000000..96f63cfd9e8cd539cc27360c9cd9b2ecce350b27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/goji/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" + + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + "github.com/zenazn/goji" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + goji.Use(c.Handler) + + goji.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + goji.Serve() +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/martini/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/martini/server.go new file mode 100644 index 0000000000000000000000000000000000000000..199d989e6b3516b068f97d568a3a806d49094181 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/martini/server.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/go-martini/martini" + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + "github.com/martini-contrib/render" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + m := martini.Classic() + m.Use(render.Renderer()) + m.Use(c.HandlerFunc) + + m.Get("/", func(r render.Render) { + r.JSON(200, map[string]interface{}{"hello": "world"}) + }) + + m.Run() +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/negroni/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/negroni/server.go new file mode 100644 index 0000000000000000000000000000000000000000..b9e6f386a98b645d3d6de0a12fc0dd03698b21a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/negroni/server.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/codegangsta/negroni" + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + n := negroni.Classic() + n.Use(c) + n.UseHandler(mux) + n.Run(":3000") +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/nethttp/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/nethttp/server.go new file mode 100644 index 0000000000000000000000000000000000000000..a11fe064cb7a941e67782e8ac5e6c13466698124 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/nethttp/server.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + http.ListenAndServe(":8080", c.Handler(handler)) +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/examples/openbar/server.go b/Godeps/_workspace/src/github.com/rs/cors/examples/openbar/server.go new file mode 100644 index 0000000000000000000000000000000000000000..746a15a1663d4659723d60ae56fae8a7b8e43370 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/examples/openbar/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" + + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowCredentials: true, + }) + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + http.ListenAndServe(":8080", c.Handler(h)) +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/utils.go b/Godeps/_workspace/src/github.com/rs/cors/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..e8823a639c5016551fefcda16ab2d1b4074ca159 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/utils.go @@ -0,0 +1,59 @@ +package cors + +const toLower = 'a' - 'A' + +type converter func(string) string + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/Godeps/_workspace/src/github.com/rs/cors/utils_test.go b/Godeps/_workspace/src/github.com/rs/cors/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..de562b772e8629da839bcf8c3ffced6c9eaae0b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/utils_test.go @@ -0,0 +1,52 @@ +package cors + +import ( + "strings" + "testing" +) + +func TestConvert(t *testing.T) { + s := convert([]string{"A", "b", "C"}, strings.ToLower) + e := []string{"a", "b", "c"} + if s[0] != e[0] || s[1] != e[1] || s[2] != e[2] { + t.Errorf("%v != %v", s, e) + } +} + +func TestParseHeaderList(t *testing.T) { + h := parseHeaderList("header, second-header, THIRD-HEADER, Numb3r3d-H34d3r") + e := []string{"Header", "Second-Header", "Third-Header", "Numb3r3d-H34d3r"} + if h[0] != e[0] || h[1] != e[1] || h[2] != e[2] { + t.Errorf("%v != %v", h, e) + } +} + +func TestParseHeaderListEmpty(t *testing.T) { + if len(parseHeaderList("")) != 0 { + t.Error("should be empty sclice") + } + if len(parseHeaderList(" , ")) != 0 { + t.Error("should be empty sclice") + } +} + +func BenchmarkParseHeaderList(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("header, second-header, THIRD-HEADER") + } +} + +func BenchmarkParseHeaderListSingle(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("header") + } +} + +func BenchmarkParseHeaderListNormalized(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("Header1, Header2, Third-Header") + } +} diff --git a/commands/http/handler.go b/commands/http/handler.go index 17b782526e3ff867fec79045770c2598ff4b59b1..774c5ca4a6238601cb31e1ba8de2d760584c1f56 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" cmds "github.com/ipfs/go-ipfs/commands" @@ -16,10 +17,17 @@ import ( var log = u.Logger("commands/http") +// the internal handler for the API +type internalHandler struct { + ctx cmds.Context + root *cmds.Command +} + +// The Handler struct is funny because we want to wrap our internal handler +// with CORS while keeping our fields. type Handler struct { - ctx cmds.Context - root *cmds.Command - origin string + internalHandler + corsHandler http.Handler } var ErrNotFound = errors.New("404 page not found") @@ -39,16 +47,31 @@ var mimeTypes = map[string]string{ cmds.Text: "text/plain", } -func NewHandler(ctx cmds.Context, root *cmds.Command, origin string) *Handler { +func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Handler { // allow whitelisted origins (so we can make API requests from the browser) - if len(origin) > 0 { - log.Info("Allowing API requests from origin: " + origin) + if len(allowedOrigin) > 0 { + log.Info("Allowing API requests from origin: " + allowedOrigin) } - return &Handler{ctx, root, origin} + // Create a handler for the API. + internal := internalHandler{ctx, root} + + // Create a CORS object for wrapping the internal handler. + c := cors.New(cors.Options{ + AllowedMethods: []string{"GET", "POST", "PUT"}, + + // use AllowOriginFunc instead of AllowedOrigins because we want to be + // restrictive by default. + AllowOriginFunc: func(origin string) bool { + return (allowedOrigin == "*") || (origin == allowedOrigin) + }, + }) + + // Wrap the internal handler with CORS handling-middleware. + return &Handler{internal, c.Handler(internal)} } -func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Debug("Incoming API request: ", r.URL) // error on external referers (to prevent CSRF attacks) @@ -65,11 +88,6 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if len(i.origin) > 0 { - w.Header().Set("Access-Control-Allow-Origin", i.origin) - } - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - req, err := Parse(r, i.root) if err != nil { if err == ErrNotFound { @@ -168,6 +186,11 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { flushCopy(w, out) } +func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Call the CORS handler which wraps the internal handler. + i.corsHandler.ServeHTTP(w, r) +} + // flushCopy Copies from an io.Reader to a http.ResponseWriter. // Flushes chunks over HTTP stream as they are read (if supported by transport). func flushCopy(w http.ResponseWriter, out io.Reader) error { diff --git a/commands/http/handler_test.go b/commands/http/handler_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1d622e048c42aa7071c991f2ac4fdf2c7fbc502a --- /dev/null +++ b/commands/http/handler_test.go @@ -0,0 +1,71 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/ipfs/go-ipfs/commands" +) + +func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) { + for name, value := range reqHeaders { + if resHeaders.Get(name) != value { + t.Errorf("Invalid header `%s', wanted `%s', got `%s'", name, value, resHeaders.Get(name)) + } + } +} + +func TestDisallowedOrigin(t *testing.T) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://barbaz.com") + + handler := NewHandler(commands.Context{}, nil, "") + handler.ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestWildcardOrigin(t *testing.T) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://foobar.com") + + handler := NewHandler(commands.Context{}, nil, "*") + handler.ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +} + +func TestAllowedMethod(t *testing.T) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://www.foobar.com") + req.Header.Add("Access-Control-Request-Method", "PUT") + + handler := NewHandler(commands.Context{}, nil, "http://www.foobar.com") + handler.ServeHTTP(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Access-Control-Allow-Origin": "http://www.foobar.com", + "Access-Control-Allow-Methods": "PUT", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }) +}