package cors import ( "fmt" "log" "net/http" "net/url" "strconv" "strings" macaron "gitea.com/macaron/macaron" ) const version = "0.1.1" const anyDomain = "!*" // Version returns the version of this module func Version() string { return version } /* Options to configure the CORS middleware read from the [cors] section of the ini configuration file. SCHEME may be http or https as accepted schemes or the '*' wildcard to accept any scheme. ALLOW_DOMAIN may be a comma separated list of domains that are allowed to run CORS requests Special values are the a single '*' wildcard that will allow any domain to send requests without credentials and the special '!*' wildcard which will reply with requesting domain in the 'access-control-allow-origin' header and hence allow requess from any domain *with* credentials. ALLOW_SUBDOMAIN set to true accepts requests from any subdomain of ALLOW_DOMAIN. METHODS may be a comma separated list of HTTP-methods to be accepted. MAX_AGE_SECONDS may be the duration in secs for which the response is cached (default 600). ref: https://stackoverflow.com/questions/54300997/is-it-possible-to-cache-http-options-response?noredirect=1#comment95790277_54300997 ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age ALLOW_CREDENTIALS set to false rejects any request with credentials. */ type Options struct { Section string Scheme string AllowDomain []string AllowSubdomain bool Methods []string MaxAgeSeconds int AllowCredentials bool } func prepareOptions(options []Options) Options { var opt Options if len(options) > 0 { opt = options[0] } if len(opt.Section) == 0 { opt.Section = "cors" } sec := macaron.Config().Section(opt.Section) if len(opt.Scheme) == 0 { opt.Scheme = sec.Key("SCHEME").MustString("http") } if len(opt.AllowDomain) == 0 { opt.AllowDomain = sec.Key("ALLOW_DOMAIN").Strings(",") if len(opt.AllowDomain) == 0 { opt.AllowDomain = []string{"*"} } } if !opt.AllowSubdomain { opt.AllowSubdomain = sec.Key("ALLOW_SUBDOMAIN").MustBool(false) } if len(opt.Methods) == 0 { opt.Methods = sec.Key("METHODS").Strings(",") if len(opt.Methods) == 0 { opt.Methods = []string{ http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions, } } } if opt.MaxAgeSeconds <= 0 { opt.MaxAgeSeconds = sec.Key("MAX_AGE_SECONDS").MustInt(600) } if !opt.AllowCredentials { opt.AllowCredentials = sec.Key("ALLOW_CREDENTIALS").MustBool(true) } return opt } // CORS responds to preflight requests with adequat access-control-* respond headers // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin // https://fetch.spec.whatwg.org/#cors-protocol-and-credentials func CORS(options ...Options) macaron.Handler { opt := prepareOptions(options) return func(ctx *macaron.Context, log *log.Logger) { reqOptions := ctx.Req.Method == http.MethodOptions headers := map[string]string{ "access-control-allow-methods": strings.Join(opt.Methods, ","), "access-control-allow-headers": ctx.Req.Header.Get("access-control-request-headers"), "access-control-max-age": strconv.Itoa(opt.MaxAgeSeconds), } if opt.AllowDomain[0] == "*" { headers["access-control-allow-origin"] = "*" } else { origin := ctx.Req.Header.Get("Origin") if reqOptions && origin == "" { respErrorf(ctx, log, http.StatusBadRequest, "missing origin header in CORS request") return } u, err := url.Parse(origin) if err != nil { respErrorf(ctx, log, http.StatusBadRequest, "Failed to parse CORS origin header. Reason: %v", err) return } ok := false for _, d := range opt.AllowDomain { if u.Hostname() == d || (opt.AllowSubdomain && strings.HasSuffix(u.Hostname(), "."+d)) || d == anyDomain { ok = true break } } if ok { if opt.Scheme != "*" { u.Scheme = opt.Scheme } headers["access-control-allow-origin"] = u.String() headers["access-control-allow-credentials"] = strconv.FormatBool(opt.AllowCredentials) headers["vary"] = "Origin" } if reqOptions && !ok { respErrorf(ctx, log, http.StatusBadRequest, "CORS request from prohibited domain %v", origin) return } } ctx.Resp.Before(func(w macaron.ResponseWriter) { for k, v := range headers { w.Header().Set(k, v) } }) if reqOptions { ctx.Resp.WriteHeader(200) // return response return } } } func respErrorf(ctx *macaron.Context, log *log.Logger, statusCode int, format string, a ...interface{}) { msg := fmt.Sprintf(format, a...) log.Println(msg) ctx.WriteHeader(statusCode) _, err := ctx.Write([]byte(msg)) if err != nil { panic(err) } return }