forked from go-macaron/cors
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cors.go
175 lines (154 loc) · 4.89 KB
/
cors.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package cors
import (
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"strings"
macaron "gopkg.in/macaron.v1"
)
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
AllowHeaders []string
ExposeHeaders []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": strings.Join(opt.AllowHeaders, ","),
"access-control-expose-headers": strings.Join(opt.ExposeHeaders, ","),
"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 {
if k == "vary" && w.Header().Get("vary") != "" {
continue
}
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
}