├── 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 | --------------------------------------------------------------------------------