From 24fc1f073dd5ec0302937fb51729991dd9a40ba3 Mon Sep 17 00:00:00 2001 From: Taylor Gerring Date: Sun, 29 Mar 2015 21:21:14 +0200 Subject: [PATCH 1/4] Add flag to control CORS header #394 * Disabled on CLI * http://localhost on Mist --- cmd/geth/main.go | 1 + cmd/mist/main.go | 7 +++++++ cmd/utils/flags.go | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 05e2e4ae6..62e30ac9a 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -233,6 +233,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso utils.VMDebugFlag, utils.ProtocolVersionFlag, utils.NetworkIdFlag, + utils.RPCCORSDomainFlag, } // missing: diff --git a/cmd/mist/main.go b/cmd/mist/main.go index fab651b22..6780cfb3a 100644 --- a/cmd/mist/main.go +++ b/cmd/mist/main.go @@ -47,12 +47,19 @@ var ( Usage: "absolute path to GUI assets directory", Value: common.DefaultAssetPath(), } + rpcCorsFlag = utils.RPCCORSDomainFlag ) func init() { + // Mist-specific default + if len(rpcCorsFlag.Value) == 0 { + rpcCorsFlag.Value = "http://localhost" + } + app.Action = run app.Flags = []cli.Flag{ assetPathFlag, + rpcCorsFlag, utils.BootnodesFlag, utils.DataDirFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 2a3e2f447..131f8a5c0 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -148,7 +148,11 @@ var ( Usage: "Port on which the JSON-RPC server should listen", Value: 8545, } - + RPCCORSDomainFlag = cli.StringFlag{ + Name: "rpccorsdomain", + Usage: "Domain on which to send Access-Control-Allow-Origin header", + Value: "", + } // Network Settings MaxPeersFlag = cli.IntFlag{ Name: "maxpeers", From 04a7c4ae1e7fe9683854fd759cad0e5e04a2f2c0 Mon Sep 17 00:00:00 2001 From: Taylor Gerring Date: Sun, 29 Mar 2015 21:26:47 +0200 Subject: [PATCH 2/4] Abstract http into rpc package New RpcConfig object to pass growing config --- cmd/geth/admin.go | 16 +++++++++++----- cmd/utils/flags.go | 17 +++++++---------- rpc/http.go | 12 ++++++++++++ rpc/messages.go | 6 ++++++ 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/cmd/geth/admin.go b/cmd/geth/admin.go index 3a58b8881..b217e88b5 100644 --- a/cmd/geth/admin.go +++ b/cmd/geth/admin.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "net" - "net/http" "os" "time" @@ -70,12 +68,20 @@ func (js *jsre) startRPC(call otto.FunctionCall) otto.Value { return otto.FalseValue() } - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port)) + config := rpc.RpcConfig{ + ListenAddress: addr, + ListenPort: uint(port), + // CorsDomain: ctx.GlobalString(RPCCORSDomainFlag.Name), + } + + xeth := xeth.New(js.ethereum, nil) + err = rpc.Start(xeth, config) + if err != nil { - fmt.Printf("Can't listen on %s:%d: %v", addr, port, err) + fmt.Printf(err.Error()) return otto.FalseValue() } - go http.Serve(l, rpc.JSONRPC(xeth.New(js.ethereum, nil))) + return otto.TrueValue() } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 131f8a5c0..e82fd9c28 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2,9 +2,6 @@ package utils import ( "crypto/ecdsa" - "fmt" - "net" - "net/http" "os" "path" "runtime" @@ -259,12 +256,12 @@ func GetAccountManager(ctx *cli.Context) *accounts.Manager { } func StartRPC(eth *eth.Ethereum, ctx *cli.Context) { - addr := ctx.GlobalString(RPCListenAddrFlag.Name) - port := ctx.GlobalInt(RPCPortFlag.Name) - fmt.Println("Starting RPC on port: ", port) - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - Fatalf("Can't listen on %s:%d: %v", addr, port, err) + config := rpc.RpcConfig{ + ListenAddress: ctx.GlobalString(RPCListenAddrFlag.Name), + ListenPort: uint(ctx.GlobalInt(RPCPortFlag.Name)), + CorsDomain: ctx.GlobalString(RPCCORSDomainFlag.Name), } - go http.Serve(l, rpc.JSONRPC(xeth.New(eth, nil))) + + xeth := xeth.New(eth, nil) + _ = rpc.Start(xeth, config) } diff --git a/rpc/http.go b/rpc/http.go index 919c567bd..d146f28a6 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -2,8 +2,10 @@ package rpc import ( "encoding/json" + "fmt" "io" "io/ioutil" + "net" "net/http" "github.com/ethereum/go-ethereum/logger" @@ -17,6 +19,16 @@ const ( maxSizeReqLength = 1024 * 1024 // 1MB ) +func Start(pipe *xeth.XEth, config RpcConfig) error { + l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", config.ListenAddress, config.ListenPort)) + if err != nil { + rpclogger.Errorf("Can't listen on %s:%d: %v", config.ListenAddress, config.ListenPort, err) + return err + } + go http.Serve(l, JSONRPC(pipe)) + return nil +} + // JSONRPC returns a handler that implements the Ethereum JSON-RPC API. func JSONRPC(pipe *xeth.XEth) http.Handler { api := NewEthereumApi(pipe) diff --git a/rpc/messages.go b/rpc/messages.go index 5c498234f..43c4d5e0d 100644 --- a/rpc/messages.go +++ b/rpc/messages.go @@ -21,6 +21,12 @@ import ( "fmt" ) +type RpcConfig struct { + ListenAddress string + ListenPort uint + CorsDomain string +} + type InvalidTypeError struct { method string msg string From b6fde73ef130e80eb834a60e766ce288de800bef Mon Sep 17 00:00:00 2001 From: Taylor Gerring Date: Sun, 29 Mar 2015 21:56:04 +0200 Subject: [PATCH 3/4] Add settable domain to CORS handler #331 --- rpc/http.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/rpc/http.go b/rpc/http.go index d146f28a6..f15d557ad 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/xeth" + "github.com/rs/cors" ) var rpclogger = logger.NewLogger("RPC") @@ -25,7 +26,21 @@ func Start(pipe *xeth.XEth, config RpcConfig) error { rpclogger.Errorf("Can't listen on %s:%d: %v", config.ListenAddress, config.ListenPort, err) return err } - go http.Serve(l, JSONRPC(pipe)) + + var handler http.Handler + if len(config.CorsDomain) > 0 { + var opts cors.Options + opts.AllowedMethods = []string{"POST"} + opts.AllowedOrigins = []string{config.CorsDomain} + + c := cors.New(opts) + handler = c.Handler(JSONRPC(pipe)) + } else { + handler = JSONRPC(pipe) + } + + go http.Serve(l, handler) + return nil } @@ -34,8 +49,7 @@ func JSONRPC(pipe *xeth.XEth) http.Handler { api := NewEthereumApi(pipe) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // TODO this needs to be configurable - w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") // Limit request size to resist DoS if req.ContentLength > maxSizeReqLength { From 35d00e00c52ad7d7c13e53b598e61fc332052f7b Mon Sep 17 00:00:00 2001 From: Taylor Gerring Date: Sun, 29 Mar 2015 22:19:38 +0200 Subject: [PATCH 4/4] Update Godeps --- Godeps/Godeps.json | 4 + .../src/github.com/rs/cors/.travis.yml | 4 + .../_workspace/src/github.com/rs/cors/LICENSE | 19 ++ .../src/github.com/rs/cors/README.md | 84 +++++ .../src/github.com/rs/cors/bench_test.go | 37 +++ .../_workspace/src/github.com/rs/cors/cors.go | 308 ++++++++++++++++++ .../src/github.com/rs/cors/cors_test.go | 288 ++++++++++++++++ .../rs/cors/examples/alice/server.go | 24 ++ .../rs/cors/examples/default/server.go | 18 + .../rs/cors/examples/goji/server.go | 22 ++ .../rs/cors/examples/martini/server.go | 23 ++ .../rs/cors/examples/negroni/server.go | 26 ++ .../rs/cors/examples/nethttp/server.go | 20 ++ .../rs/cors/examples/openbar/server.go | 22 ++ .../src/github.com/rs/cors/utils.go | 27 ++ .../src/github.com/rs/cors/utils_test.go | 28 ++ 16 files changed, 954 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/rs/cors/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/rs/cors/LICENSE create mode 100644 Godeps/_workspace/src/github.com/rs/cors/README.md create mode 100644 Godeps/_workspace/src/github.com/rs/cors/bench_test.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/cors.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/cors_test.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/alice/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/default/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/goji/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/martini/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/negroni/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/nethttp/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/examples/openbar/server.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/utils.go create mode 100644 Godeps/_workspace/src/github.com/rs/cors/utils_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 5ba6bb8cf..4c8c8281e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -90,6 +90,10 @@ "ImportPath": "github.com/robertkrimen/otto/token", "Rev": "dea31a3d392779af358ec41f77a07fcc7e9d04ba" }, + { + "ImportPath": "github.com/rs/cors", + "Rev": "6e0c3cb65fc0fdb064c743d176a620e3ca446dfb" + }, { "ImportPath": "github.com/syndtr/goleveldb/leveldb", "Rev": "832fa7ed4d28545eab80f19e1831fc004305cade" 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 000000000..bbb5185a2 --- /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 000000000..d8e2df5a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Olivier Poitrey + +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 000000000..6f70c30ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/README.md @@ -0,0 +1,84 @@ +# Go CORS handler [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/cors) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/cors/master/LICENSE) [![build](https://img.shields.io/travis/rs/cors.svg?style=flat)](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 `*`. +* **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. + +## 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 000000000..454375d2c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/bench_test.go @@ -0,0 +1,37 @@ +package cors + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func BenchmarkWithout(b *testing.B) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + for i := 0; i < b.N; i++ { + testHandler.ServeHTTP(res, req) + } +} + +func BenchmarkDefault(b *testing.B) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + handler := Default() + + for i := 0; i < b.N; i++ { + handler.Handler(testHandler).ServeHTTP(res, req) + } +} + +func BenchmarkPreflight(b *testing.B) { + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Access-Control-Request-Method", "GET") + handler := Default() + + for i := 0; i < b.N; i++ { + handler.Handler(testHandler).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 000000000..276bc40bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/cors.go @@ -0,0 +1,308 @@ +/* +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 + // 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 + // log object to use when debugging + log *log.Logger +} + +type Cors struct { + // The CORS Options + options Options +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + // 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. + normOptions := Options{ + AllowedOrigins: convert(options.AllowedOrigins, strings.ToLower), + AllowedMethods: convert(options.AllowedMethods, strings.ToUpper), + // Origin is always appended as some browsers will always request + // for this header at preflight + AllowedHeaders: convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey), + ExposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + AllowCredentials: options.AllowCredentials, + MaxAge: options.MaxAge, + Debug: options.Debug, + log: log.New(os.Stdout, "[cors] ", log.LstdFlags), + } + if len(normOptions.AllowedOrigins) == 0 { + // Default is all origins + normOptions.AllowedOrigins = []string{"*"} + } + if len(normOptions.AllowedHeaders) == 1 { + // Add some sensible defaults + normOptions.AllowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } + if len(normOptions.AllowedMethods) == 0 { + // Default is simple methods + normOptions.AllowedMethods = []string{"GET", "POST"} + } + + if normOptions.Debug { + normOptions.log.Printf("Options: %v", normOptions) + } + return &Cors{ + options: normOptions, + } +} + +// 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 (cors *Cors) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + cors.logf("Handler: Preflight request") + cors.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 { + cors.logf("Handler: Actual request") + cors.handleActualRequest(w, r) + h.ServeHTTP(w, r) + } + }) +} + +// Martini compatible handler +func (cors *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + cors.logf("HandlerFunc: Preflight request") + cors.handlePreflight(w, r) + } else { + cors.logf("HandlerFunc: Actual request") + cors.handleActualRequest(w, r) + } +} + +// Negroni compatible interface +func (cors *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Method == "OPTIONS" { + cors.logf("ServeHTTP: Preflight request") + cors.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 { + cors.logf("ServeHTTP: Actual request") + cors.handleActualRequest(w, r) + next(w, r) + } +} + +// handlePreflight handles pre-flight CORS requests +func (cors *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + options := cors.options + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != "OPTIONS" { + cors.logf(" Preflight aborted: %s!=OPTIONS", r.Method) + return + } + if origin == "" { + cors.logf(" Preflight aborted: empty origin") + return + } + if !cors.isOriginAllowed(origin) { + cors.logf(" Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !cors.isMethodAllowed(reqMethod) { + cors.logf(" Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !cors.areHeadersAllowed(reqHeaders) { + cors.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 options.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if options.MaxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(options.MaxAge)) + } + cors.logf(" Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (cors *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + options := cors.options + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method == "OPTIONS" { + cors.logf(" Actual request no headers added: method == %s", r.Method) + return + } + if origin == "" { + cors.logf(" Actual request no headers added: missing origin") + return + } + if !cors.isOriginAllowed(origin) { + cors.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 !cors.isMethodAllowed(r.Method) { + if cors.options.Debug { + cors.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(options.ExposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(options.ExposedHeaders, ", ")) + } + if options.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + cors.logf(" Actual response added headers: %v", headers) +} + +// convenience method. checks if debugging is turned on before printing +func (cors *Cors) logf(format string, a ...interface{}) { + if cors.options.Debug { + cors.options.log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (cors *Cors) isOriginAllowed(origin string) bool { + allowedOrigins := cors.options.AllowedOrigins + origin = strings.ToLower(origin) + for _, allowedOrigin := range allowedOrigins { + switch allowedOrigin { + case "*": + return true + case 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 (cors *Cors) isMethodAllowed(method string) bool { + allowedMethods := cors.options.AllowedMethods + if len(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 _, allowedMethod := range allowedMethods { + if allowedMethod == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (cors *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + found := false + for _, allowedHeader := range cors.options.AllowedHeaders { + if allowedHeader == "*" || allowedHeader == header { + found = true + break + } + } + 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 000000000..f215018c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/cors_test.go @@ -0,0 +1,288 @@ +package cors + +import ( + "net/http" + "net/http/httptest" + "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 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 000000000..0a3e15cb8 --- /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/justinas/alice" + "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\"}")) + }) + + 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 000000000..851ac41d0 --- /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/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 000000000..1fb4073aa --- /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/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 000000000..081af32f9 --- /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/martini-contrib/render" + "github.com/rs/cors" +) + +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 000000000..3cb33bff6 --- /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/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 000000000..eaa775e44 --- /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/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 000000000..094042300 --- /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/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 000000000..429ab1114 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/utils.go @@ -0,0 +1,27 @@ +package cors + +import ( + "net/http" + "strings" +) + +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 +} + +func parseHeaderList(headerList string) (headers []string) { + for _, header := range strings.Split(headerList, ",") { + header = http.CanonicalHeaderKey(strings.TrimSpace(header)) + if header != "" { + headers = append(headers, header) + } + } + 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 000000000..3fc77fc1e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rs/cors/utils_test.go @@ -0,0 +1,28 @@ +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") + e := []string{"Header", "Second-Header", "Third-Header"} + 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") + } +}