summaryrefslogtreecommitdiffstats
path: root/vendor/gitea.com/macaron/cors/cors.go
blob: 2d0613f2bc28396daeef2ae1f6d30e88feb5dfdf (plain)
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
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
}