168 lines
4.7 KiB
Go
168 lines
4.7 KiB
Go
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.Status(200) // return response
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|