package cors_fasthttp import ( "context" "fmt" "net/http" "strconv" "strings" "github.com/valyala/fasthttp" "go.uber.org/zap" "git.loafle.net/commons_go/logging" ) // Cors http handler type Cors interface { Handler(h fasthttp.RequestHandler) fasthttp.RequestHandler } // Cors http handler type cors struct { ctx context.Context logger *zap.Logger co CorsOptions // Set to true when allowed origins contains a "*" allowedOriginsAll bool // Normalized list of plain allowed origins allowedOrigins []string // List of allowed origins containing wildcards allowedWOrigins []wildcard // 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 } // New creates a new Cors handler with the provided options. func New(ctx context.Context, co CorsOptions) Cors { c := &cors{ ctx: ctx, logger: logging.WithContext(ctx), co: co, exposedHeaders: convert(co.ExposedHeaders, http.CanonicalHeaderKey), } // 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 // Allowed Origins if len(co.AllowedOrigins) == 0 { if co.AllowOriginFunc == nil { // Default is all origins c.allowedOriginsAll = true } } else { c.allowedOrigins = []string{} c.allowedWOrigins = []wildcard{} for _, origin := range co.AllowedOrigins { // Normalize origin = strings.ToLower(origin) if origin == "*" { // If "*" is present in the list, turn the whole list into a match all c.allowedOriginsAll = true c.allowedOrigins = nil c.allowedWOrigins = nil break } else if i := strings.IndexByte(origin, '*'); i >= 0 { // Split the origin in two: start and end string without the * w := wildcard{origin[0:i], origin[i+1 : len(origin)]} c.allowedWOrigins = append(c.allowedWOrigins, w) } else { c.allowedOrigins = append(c.allowedOrigins, origin) } } } // Allowed Headers if len(co.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(co.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) for _, h := range co.AllowedHeaders { if h == "*" { c.allowedHeadersAll = true c.allowedHeaders = nil break } } } // Allowed Methods if len(co.AllowedMethods) == 0 { // Default is spec's "simple" methods c.allowedMethods = []string{"GET", "POST", "HEAD"} } else { c.allowedMethods = convert(co.AllowedMethods, strings.ToUpper) } return c } // Default creates a new Cors handler with default options. func Default(ctx context.Context) Cors { return New(ctx, CorsOptions{}) } // AllowAll create a new Cors handler with permissive configuration allowing all // origins with all standard methods with any header and credentials. func AllowAll(ctx context.Context) Cors { return New(ctx, CorsOptions{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"}, AllowedHeaders: []string{"*"}, AllowCredentials: true, }) } // Handler apply the CORS specification on the request, and add relevant CORS headers // as necessary. func (c *cors) Handler(h fasthttp.RequestHandler) fasthttp.RequestHandler { return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { if string(ctx.Method()) == "OPTIONS" && ctx.Request.Header.Peek("Access-Control-Request-Method") != nil { c.logger.Info("Handler: Preflight request") c.handlePreflight(ctx) // 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) if c.co.OptionsPassthrough { h(ctx) } else { ctx.SetStatusCode(fasthttp.StatusOK) } } else { c.logger.Info("Handler: Actual request") c.handleActualRequest(ctx) h(ctx) } }) } // handlePreflight handles pre-flight CORS requests func (c *cors) handlePreflight(ctx *fasthttp.RequestCtx) { origin := string(ctx.Request.Header.Peek("Origin")) if string(ctx.Method()) != "OPTIONS" { c.logger.Info(fmt.Sprintf(" Preflight aborted: %s!=OPTIONS", string(ctx.Method()))) return } // Always set Vary headers // see https://github.com/rs/cors/issues/10, // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 ctx.Response.Header.Add("Vary", "Origin") ctx.Response.Header.Add("Vary", "Access-Control-Request-Method") ctx.Response.Header.Add("Vary", "Access-Control-Request-Headers") if origin == "" { c.logger.Info(" Preflight aborted: empty origin") return } if !c.isOriginAllowed(origin) { c.logger.Info(fmt.Sprintf(" Preflight aborted: origin '%s' not allowed", origin)) return } reqMethod := string(ctx.Request.Header.Peek("Access-Control-Request-Method")) if !c.isMethodAllowed(reqMethod) { c.logger.Info(fmt.Sprintf(" Preflight aborted: method '%s' not allowed", reqMethod)) return } reqHeaders := parseHeaderList(string(ctx.Request.Header.Peek("Access-Control-Request-Headers"))) if !c.areHeadersAllowed(reqHeaders) { c.logger.Info(fmt.Sprintf(" Preflight aborted: headers '%v' not allowed", reqHeaders)) return } if c.allowedOriginsAll && !c.co.AllowCredentials { ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") } else { ctx.Response.Header.Set("Access-Control-Allow-Origin", 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 ctx.Response.Header.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 ctx.Response.Header.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) } if c.co.AllowCredentials { ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true") } if c.co.MaxAge > 0 { ctx.Response.Header.Set("Access-Control-Max-Age", strconv.Itoa(c.co.MaxAge)) } // c.logger.Info(fmt.Sprintf(" Preflight response headers: %v", ctx.Response.Header.)) } // handleActualRequest handles simple cross-origin requests, actual request or redirects func (c *cors) handleActualRequest(ctx *fasthttp.RequestCtx) { origin := string(ctx.Request.Header.Peek("Origin")) method := string(ctx.Method()) if method == "OPTIONS" { c.logger.Info(fmt.Sprintf(" Actual request no headers added: method == %s", method)) return } // Always set Vary, see https://github.com/rs/cors/issues/10 ctx.Response.Header.Add("Vary", "Origin") if origin == "" { c.logger.Info(" Actual request no headers added: missing origin") return } if !c.isOriginAllowed(origin) { c.logger.Info(fmt.Sprintf(" 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(method) { c.logger.Info(fmt.Sprintf(" Actual request no headers added: method '%s' not allowed", method)) return } if c.allowedOriginsAll && !c.co.AllowCredentials { ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") } else { ctx.Response.Header.Set("Access-Control-Allow-Origin", origin) } if len(c.exposedHeaders) > 0 { ctx.Response.Header.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) } if c.co.AllowCredentials { ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true") } // c.logf(" Actual response added headers: %v", headers) } // 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.co.AllowOriginFunc != nil { return c.co.AllowOriginFunc(origin) } if c.allowedOriginsAll { return true } origin = strings.ToLower(origin) for _, o := range c.allowedOrigins { if o == origin { return true } } for _, w := range c.allowedWOrigins { if w.match(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 }