diff options
Diffstat (limited to 'Godeps/_workspace/src/github.com/rs/cors/cors.go')
-rw-r--r-- | Godeps/_workspace/src/github.com/rs/cors/cors.go | 308 |
1 files changed, 308 insertions, 0 deletions
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 +} |