├── caddy ├── test │ ├── target │ │ ├── foo │ │ └── bar │ ├── CaddyFile │ └── origin │ │ └── index.html ├── parse_test.go └── corsPlugin.go ├── LICENSE ├── README.md ├── cors_test.go └── cors.go /caddy/test/target/foo: -------------------------------------------------------------------------------- 1 | Success! -------------------------------------------------------------------------------- /caddy/test/target/bar: -------------------------------------------------------------------------------- 1 | You should not have gotten this! Something is Wrong! -------------------------------------------------------------------------------- /caddy/test/CaddyFile: -------------------------------------------------------------------------------- 1 | #run with caddydev -after="log" -source="." cors -conf=test/Caddyfile 2 | # from caddy directory 3 | 4 | http://localhost:9999 { 5 | root ./test/target 6 | cors /foo 7 | } 8 | 9 | #navigate browser to localhost:9998 10 | #sould see "Success! twice" 11 | http://localhost:9998 { 12 | root ./test/origin 13 | } -------------------------------------------------------------------------------- /caddy/test/origin/index.html: -------------------------------------------------------------------------------- 1 |
2 | Hello World! 3 | 4 | 5 | 6 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Craig Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | cors gives you easy control over Cross Origin Resource Sharing for your site. 3 | 4 | It allows you to whitelist particular domains per route, or to simply allow all domains `*` If desired you may customize nearly every aspect of the specification. 5 | 6 | ### Syntax 7 | 8 | ``` 9 | cors [path] [domains...] { 10 | origin [origin] 11 | origin_regexp [regexp] 12 | methods [methods] 13 | allow_credentials [allowCredentials] 14 | max_age [maxAge] 15 | allowed_headers [allowedHeaders] 16 | exposed_headers [exposedHeaders] 17 | } 18 | ``` 19 | 20 | * **path** is the file or directory this applies to (default is /). 21 | * **domains** is a space-seperated list of domains to allow. If ommitted, all domains will be granted access. 22 | * **origin** is a domain to grant access to. May be specified multiple times or ommitted. 23 | * **origin_regexp** is a regexp that will be matched to the `Origin` header. Access will be granted accordingly. It can be used in conjonction with the `origin` config (executed as a fallback to `origin`). May be specified multiple times or ommitted. 24 | * **methods** is set of http methods to allow. Default is these: POST,GET,OPTIONS,PUT,DELETE. 25 | * **allow_credentials** sets the value of the Access-Control-Allow-Credentials header. Can be true or false. By default, header will not be included. 26 | * **max_age** is the length of time in seconds to cache preflight info. Not set by default. 27 | * **allowed_headers** is a comma-seperated list of request headers a client may send. 28 | * **exposed_headers** is a comma-seperated list of response headers a client may access. 29 | 30 | ### Examples 31 | 32 | Simply allow all domains to request any path: 33 | 34 |cors
35 |
36 | Protect specific paths only, and only allow a few domains:
37 |
38 | cors /foo http://mysite.com http://anothertrustedsite.com
39 |
40 | Full configuration:
41 |
42 | ```
43 | cors / {
44 | origin http://allowedSite.com
45 | origin http://anotherSite.org https://anotherSite.org
46 | origin_regexp .+\.example\.com$
47 | methods POST,PUT
48 | allow_credentials false
49 | max_age 3600
50 | allowed_headers X-Custom-Header,X-Foobar
51 | exposed_headers X-Something-Special,SomethingElse
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/caddy/parse_test.go:
--------------------------------------------------------------------------------
1 | package caddy
2 |
3 | import (
4 | "github.com/caddyserver/caddy"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func TestParse_OneLines(t *testing.T) {
10 | type testCase struct {
11 | desc string
12 | text string
13 | numRules int
14 | path string
15 | allowedOrigins []string
16 | errors bool
17 | }
18 | testCases := []testCase{
19 | {"Plain", "cors", 1, "/", []string{"*"}, false},
20 | {"Single arg path", "cors /foo", 1, "/foo", []string{"*"}, false},
21 | {"Additional arg domain", "cors /foo http://foo.com", 1, "/foo", []string{"http://foo.com"}, false},
22 | {"Multiple domains", "cors /foo http://foo.com,http://bar.com", 1, "/foo", []string{"http://foo.com", "http://bar.com"}, false},
23 | {"Extra args", "cors /foo http://foo.com http://bar.com", 1, "/foo", []string{"http://foo.com", "http://bar.com"}, true},
24 | }
25 | for _, test := range testCases {
26 | c := caddy.NewTestController("http", test.text)
27 | rules, err := parseRules(c)
28 | if err != nil {
29 | if test.errors {
30 | continue
31 | }
32 | t.Fatal(test.desc, err)
33 | }
34 | if len(rules) != test.numRules {
35 | t.Fatalf("%s: Expected %d rules, but found %d.", test.desc, test.numRules, len(rules))
36 | }
37 | if rules[0].Path != test.path {
38 | t.Fatalf("%s: Expected path of %s, but found %s.", test.desc, test.path, rules[0].Path)
39 | }
40 | if !reflect.DeepEqual(rules[0].Conf.AllowedOrigins, test.allowedOrigins) {
41 | t.Fatalf("%s: Allowed origins don't match. Expected: %v. Actual: %v.", test.desc, test.allowedOrigins, rules[0].Conf.AllowedOrigins)
42 | }
43 | }
44 | }
45 |
46 | func TestFull(t *testing.T) {
47 | conf := `cors {
48 | origin http://foo.com
49 | methods POST,PUT
50 | allow_credentials true
51 | max_age 3600
52 | allowed_headers X-Foo,X-bar
53 | exposed_headers X-SECRET
54 | origin http://bar.com
55 | }`
56 | c := caddy.NewTestController("http", conf)
57 | rules, err := parseRules(c)
58 | if err != nil {
59 | t.Fatal(err)
60 | }
61 | if len(rules) != 1 {
62 | t.Fatalf("%d rules is bad", len(rules))
63 | }
64 | config := rules[0].Conf
65 | expectedOrigins := []string{"http://foo.com", "http://bar.com"}
66 | if !reflect.DeepEqual(config.AllowedOrigins, expectedOrigins) {
67 | t.Fatal("Origins don't match", config.AllowedOrigins, expectedOrigins)
68 | }
69 | if config.AllowedMethods != "POST,PUT" {
70 | t.Fatalf("Bad methods '%s'", config.AllowedMethods)
71 | }
72 | if *config.AllowCredentials != true {
73 | t.Fatalf("Wrong AllowCredentials")
74 | }
75 | if config.MaxAge != 3600 {
76 | t.Fatal("Wrong MaxAge")
77 | }
78 | if config.AllowedHeaders != "X-Foo,X-bar" {
79 | t.Fatal("AllowedHeaders")
80 | }
81 | if config.ExposedHeaders != "X-SECRET" {
82 | t.Fatal("ExposedHeaders")
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/cors_test.go:
--------------------------------------------------------------------------------
1 | package cors
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "regexp"
7 | "testing"
8 | )
9 |
10 | func getReq(method string) (*httptest.ResponseRecorder, *http.Request) {
11 | w := httptest.NewRecorder()
12 | r, _ := http.NewRequest(method, "http://foo.com", nil)
13 | r.Header.Set(requestMethodKey, "POST")
14 | return w, r
15 | }
16 |
17 | func TestDefault_NoOrigin(t *testing.T) {
18 | w, r := getReq("OPTIONS")
19 | Default().HandleRequest(w, r)
20 | if w.Header().Get(allowOriginKey) != "" {
21 | t.Fatal("Should not have origin header when no origin passed in")
22 | }
23 | }
24 |
25 | func TestDefault_Origin_Supplied(t *testing.T) {
26 | w, r := getReq("OPTIONS")
27 | r.Header.Set(originKey, "http://bar.com")
28 | Default().HandleRequest(w, r)
29 | if w.Header().Get(allowOriginKey) != "*" {
30 | t.Fatalf("Expect origin of \"*\". Got \"%s\".", w.Header().Get(allowOriginKey))
31 | }
32 | if w.Header().Get(varyKey) != "Origin" {
33 | t.Fatal("Must include Vary:Origin if allow origin header set to specific domain.")
34 | }
35 | }
36 |
37 | func TestDefault_Dissallowed_Origin(t *testing.T) {
38 | w, r := getReq("OPTIONS")
39 | r.Header.Set(originKey, "http://bar.com")
40 | c := Default()
41 | c.AllowedOrigins = []string{"http://blog.bar.com"}
42 | c.HandleRequest(w, r)
43 | if w.Header().Get(allowOriginKey) != "" {
44 | t.Fatal("Should not have origin header when no origin not explicitely allowed")
45 | }
46 | }
47 |
48 | func TestDefault_Allowed_Origin_By_Regexp(t *testing.T) {
49 | c := Default()
50 | c.AllowedOrigins = []string{}
51 | c.OriginRegexps = []*regexp.Regexp{
52 | regexp.MustCompile(".+\\.example\\.com$"),
53 | regexp.MustCompile("^http\\:\\/\\/(.+)\\.other\\.org$"),
54 | }
55 |
56 | allowedOrigins := []string{"http://foo.example.com", "http://bar.other.org"}
57 |
58 | for _, origin := range allowedOrigins {
59 | w, r := getReq("OPTIONS")
60 | r.Header.Set(originKey, origin)
61 | c.HandleRequest(w, r)
62 | if w.Header().Get(allowOriginKey) != origin {
63 | t.Fatal("Should have origin header with the request origin when the regexp matches")
64 | }
65 | }
66 |
67 | }
68 |
69 | func TestDefault_Methods(t *testing.T) {
70 | w, r := getReq("OPTIONS")
71 | r.Header.Set(originKey, "http://bar.com")
72 | c := Default()
73 | c.HandleRequest(w, r)
74 | if w.Header().Get(allowMethodsKey) != "POST, GET, OPTIONS, PUT, DELETE" {
75 | t.Fatal("Allow methods should be set")
76 | }
77 | }
78 |
79 | func TestDefault_AllowAllHeaders(t *testing.T) {
80 | w, r := getReq("OPTIONS")
81 | c := Default()
82 | c.AllowedHeaders = "*"
83 | reqHeaders := "Bar, Foo, X-Yz"
84 | r.Header.Set(originKey, "http://bar.com")
85 | r.Header.Set(requestHeadersKey, reqHeaders)
86 | c.HandleRequest(w, r)
87 | if w.Header().Get(allowHeadersKey) != reqHeaders {
88 | t.Fatal("If AllowedHeaders is *, it should copy the value of requestHeadersKey to allowHeadersKey")
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/cors.go:
--------------------------------------------------------------------------------
1 | package cors
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "regexp"
7 | )
8 |
9 | const (
10 | allowOriginKey string = "Access-Control-Allow-Origin"
11 | allowCredentialsKey = "Access-Control-Allow-Credentials"
12 | allowHeadersKey = "Access-Control-Allow-Headers"
13 | allowMethodsKey = "Access-Control-Allow-Methods"
14 | maxAgeKey = "Access-Control-Max-Age"
15 |
16 | originKey = "Origin"
17 | varyKey = "Vary"
18 | requestMethodKey = "Access-Control-Request-Method"
19 | requestHeadersKey = "Access-Control-Request-Headers"
20 | exposeHeadersKey = "Access-Control-Expose-Headers"
21 | options = "OPTIONS"
22 | )
23 |
24 | type Config struct {
25 | AllowedOrigins []string
26 | OriginRegexps []*regexp.Regexp
27 | AllowedMethods string
28 | AllowedHeaders string
29 | ExposedHeaders string
30 | AllowCredentials *bool
31 | MaxAge int
32 | }
33 |
34 | func Default() *Config {
35 | return &Config{
36 | AllowedOrigins: []string{"*"},
37 | OriginRegexps: []*regexp.Regexp{},
38 | AllowedMethods: "POST, GET, OPTIONS, PUT, DELETE",
39 | AllowedHeaders: "",
40 | ExposedHeaders: "",
41 | MaxAge: 0,
42 | AllowCredentials: nil,
43 | }
44 | }
45 |
46 | // Read the request, setting response headers as appropriate.
47 | // Will NOT write anything to response in any circumstances.
48 | func (c *Config) HandleRequest(w http.ResponseWriter, r *http.Request) {
49 | requestOrigin := r.Header.Get(originKey)
50 | if requestOrigin == "" {
51 | return
52 | }
53 |
54 | //check origin against allowed origins
55 | for _, ao := range c.AllowedOrigins {
56 | if ao == "*" || ao == requestOrigin {
57 | responseOrigin := "*"
58 | if ao != "*" {
59 | responseOrigin = requestOrigin
60 | }
61 | addAllowOriginHeader(w, responseOrigin)
62 | break
63 | }
64 | }
65 |
66 | if w.Header().Get(allowOriginKey) == "" {
67 | if c.anyOriginRegexpMatch(requestOrigin) {
68 | addAllowOriginHeader(w, requestOrigin)
69 | } else {
70 | return //if we didn't set a valid allow-origin, none of the other headers matter
71 | }
72 | }
73 |
74 | if IsPreflight(r) {
75 | w.Header().Set(allowMethodsKey, c.AllowedMethods)
76 | if c.AllowedHeaders != "" {
77 | if c.AllowedHeaders != "*" {
78 | w.Header().Set(allowHeadersKey, c.AllowedHeaders)
79 | } else {
80 | w.Header().Set(allowHeadersKey, r.Header.Get(requestHeadersKey))
81 | }
82 |
83 | }
84 | if c.MaxAge > 0 {
85 | w.Header().Set(maxAgeKey, fmt.Sprint(c.MaxAge))
86 | }
87 | } else {
88 | //regular request
89 | if c.ExposedHeaders != "" {
90 | w.Header().Set(exposeHeadersKey, c.ExposedHeaders)
91 | }
92 | }
93 |
94 | if c.AllowCredentials != nil {
95 | w.Header().Set(allowCredentialsKey, fmt.Sprint(*c.AllowCredentials))
96 | }
97 |
98 | }
99 |
100 | func IsPreflight(r *http.Request) bool {
101 | return r.Method == options && r.Header.Get(requestMethodKey) != ""
102 | }
103 |
104 | func addAllowOriginHeader(w http.ResponseWriter, allowedOrigin string) {
105 | w.Header().Set(allowOriginKey, allowedOrigin)
106 | w.Header().Add(varyKey, originKey)
107 | }
108 |
109 | func (c *Config) anyOriginRegexpMatch(origin string) bool {
110 | for _, r := range c.OriginRegexps {
111 | if r.MatchString(origin) {
112 | return true
113 | }
114 | }
115 |
116 | return false
117 | }
118 |
--------------------------------------------------------------------------------
/caddy/corsPlugin.go:
--------------------------------------------------------------------------------
1 | package caddy
2 |
3 | import (
4 | "net/http"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/captncraig/cors"
10 |
11 | "github.com/caddyserver/caddy"
12 | "github.com/caddyserver/caddy/caddyhttp/httpserver"
13 | )
14 |
15 | type corsRule struct {
16 | Conf *cors.Config
17 | Path string
18 | }
19 |
20 | func init() {
21 | caddy.RegisterPlugin("cors", caddy.Plugin{
22 | ServerType: "http",
23 | Action: setup,
24 | })
25 | }
26 |
27 | func setup(c *caddy.Controller) error {
28 | rules, err := parseRules(c)
29 | if err != nil {
30 | return err
31 | }
32 | siteConfig := httpserver.GetConfig(c)
33 | siteConfig.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
34 | return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
35 | for _, rule := range rules {
36 | if httpserver.Path(r.URL.Path).Matches(rule.Path) {
37 | rule.Conf.HandleRequest(w, r)
38 | if cors.IsPreflight(r) {
39 | return 200, nil
40 | }
41 | break
42 | }
43 | }
44 | return next.ServeHTTP(w, r)
45 | })
46 | })
47 | return nil
48 | }
49 |
50 | func parseRules(c *caddy.Controller) ([]*corsRule, error) {
51 | rules := []*corsRule{}
52 |
53 | for c.Next() {
54 | rule := &corsRule{Path: "/", Conf: cors.Default()}
55 | args := c.RemainingArgs()
56 |
57 | anyOrigins := false
58 | if len(args) > 0 {
59 | rule.Path = args[0]
60 | }
61 | for i := 1; i < len(args); i++ {
62 | if !anyOrigins {
63 | rule.Conf.AllowedOrigins = nil
64 | }
65 | rule.Conf.AllowedOrigins = append(rule.Conf.AllowedOrigins, strings.Split(args[i], ",")...)
66 | anyOrigins = true
67 | }
68 | for c.NextBlock() {
69 | switch c.Val() {
70 | case "origin":
71 | if !anyOrigins {
72 | rule.Conf.AllowedOrigins = nil
73 | }
74 | args := c.RemainingArgs()
75 | for _, domain := range args {
76 | rule.Conf.AllowedOrigins = append(rule.Conf.AllowedOrigins, strings.Split(domain, ",")...)
77 | }
78 | anyOrigins = true
79 | case "origin_regexp":
80 | if arg, err := singleArg(c, "origin_regexp"); err != nil {
81 | return nil, err
82 | } else {
83 | r, err := regexp.Compile(arg)
84 |
85 | if err != nil {
86 | return nil, c.Errf("could no compile regexp: %s", err)
87 | }
88 |
89 | if !anyOrigins {
90 | rule.Conf.AllowedOrigins = nil
91 | anyOrigins = true
92 | }
93 |
94 | rule.Conf.OriginRegexps = append(rule.Conf.OriginRegexps, r)
95 | }
96 | case "methods":
97 | if arg, err := singleArg(c, "methods"); err != nil {
98 | return nil, err
99 | } else {
100 | rule.Conf.AllowedMethods = arg
101 | }
102 | case "allow_credentials":
103 | if arg, err := singleArg(c, "allow_credentials"); err != nil {
104 | return nil, err
105 | } else {
106 | var b bool
107 | if arg == "true" {
108 | b = true
109 | } else if arg != "false" {
110 | return nil, c.Errf("allow_credentials must be true or false.")
111 | }
112 | rule.Conf.AllowCredentials = &b
113 | }
114 | case "max_age":
115 | if arg, err := singleArg(c, "max_age"); err != nil {
116 | return nil, err
117 | } else {
118 | i, err := strconv.Atoi(arg)
119 | if err != nil {
120 | return nil, c.Err("max_age must be valid int")
121 | }
122 | rule.Conf.MaxAge = i
123 | }
124 | case "allowed_headers":
125 | if arg, err := singleArg(c, "allowed_headers"); err != nil {
126 | return nil, err
127 | } else {
128 | rule.Conf.AllowedHeaders = arg
129 | }
130 | case "exposed_headers":
131 | if arg, err := singleArg(c, "exposed_headers"); err != nil {
132 | return nil, err
133 | } else {
134 | rule.Conf.ExposedHeaders = arg
135 | }
136 | default:
137 | return nil, c.Errf("Unknown cors config item: %s", c.Val())
138 | }
139 | }
140 | rules = append(rules, rule)
141 | }
142 | return rules, nil
143 | }
144 |
145 | func singleArg(c *caddy.Controller, desc string) (string, error) {
146 | args := c.RemainingArgs()
147 | if len(args) != 1 {
148 | return "", c.Errf("%s expects exactly one argument", desc)
149 | }
150 | return args[0], nil
151 | }
152 |
--------------------------------------------------------------------------------