-
Star
(120)
You must be signed in to star a gist -
Fork
(41)
You must be signed in to fork a gist
-
-
Save algal/5480916 to your computer and use it in GitHub Desktop.
| # | |
| # A CORS (Cross-Origin Resouce Sharing) config for nginx | |
| # | |
| # == Purpose | |
| # | |
| # This nginx configuration enables CORS requests in the following way: | |
| # - enables CORS just for origins on a whitelist specified by a regular expression | |
| # - CORS preflight request (OPTIONS) are responded immediately | |
| # - Access-Control-Allow-Credentials=true for GET and POST requests | |
| # - Access-Control-Max-Age=20days, to minimize repetitive OPTIONS requests | |
| # - various superluous settings to accommodate nonconformant browsers | |
| # | |
| # == Comment on echoing Access-Control-Allow-Origin | |
| # | |
| # How do you allow CORS requests only from certain domains? The last | |
| # published W3C candidate recommendation states that the | |
| # Access-Control-Allow-Origin header can include a list of origins. | |
| # (See: http://www.w3.org/TR/2013/CR-cors-20130129/#access-control-allow-origin-response-header ) | |
| # However, browsers do not support this well and it likely will be | |
| # dropped from the spec (see, http://www.rfc-editor.org/errata_search.php?rfc=6454&eid=3249 ). | |
| # | |
| # The usual workaround is for the server to keep a whitelist of | |
| # acceptable origins on the server (as a regular expression), match | |
| # the request's Origin header against the list, and echo it back | |
| # | |
| # (Yes you can use '*' to accept all origins but this is too open and | |
| # prevents using 'Access-Control-Allow-Credentials: true', which is | |
| # needed for HTTP Basic Access authentication.) | |
| # | |
| # == Comment on spec | |
| # | |
| # Comments below are all based on my reading of the CORS spec as of | |
| # 2013-Jan-29 ( http://www.w3.org/TR/2013/CR-cors-20130129/ ), the | |
| # XMLHttpRequest spec ( | |
| # http://www.w3.org/TR/2012/WD-XMLHttpRequest-20121206/ ), and | |
| # experimentation with latest versions of Firefox, Chrome, Safari at | |
| # that point in time. | |
| # | |
| # == Changelog | |
| # | |
| # based on https://gist.github.com/alexjs/4165271 | |
| # | |
| location / { | |
| # if the request included an Origin: header with an origin on the whitelist, | |
| # then it is some kind of CORS request. | |
| # specifically, this example allow CORS requests from | |
| # scheme : http or https | |
| # authority : any authority ending in "mckinsey.com" | |
| # port : nothing, or :<any_number> | |
| if ($http_origin ~* (https?://.*\.mckinsey\.com(:[0-9]+)?)) { | |
| set $cors "true"; | |
| } | |
| # Nginx doesn't support nested If statements, so we use string | |
| # concatenation to create a flag for compound conditions | |
| # OPTIONS indicates a CORS pre-flight request | |
| if ($request_method = 'OPTIONS') { | |
| set $cors "${cors}options"; | |
| } | |
| # non-OPTIONS indicates a normal CORS request | |
| if ($request_method = 'GET') { | |
| set $cors "${cors}get"; | |
| } | |
| if ($request_method = 'POST') { | |
| set $cors "${cors}post"; | |
| } | |
| # if it's a GET or POST, set the standard CORS responses header | |
| if ($cors = "trueget") { | |
| # Tells the browser this origin may make cross-origin requests | |
| # (Here, we echo the requesting origin, which matched the whitelist.) | |
| add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
| # Tells the browser it may show the response, when XmlHttpRequest.withCredentials=true. | |
| add_header 'Access-Control-Allow-Credentials' 'true'; | |
| # # Tell the browser which response headers the JS can see, besides the "simple response headers" | |
| # add_header 'Access-Control-Expose-Headers' 'myresponseheader'; | |
| } | |
| if ($cors = "truepost") { | |
| # Tells the browser this origin may make cross-origin requests | |
| # (Here, we echo the requesting origin, which matched the whitelist.) | |
| add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
| # Tells the browser it may show the response, when XmlHttpRequest.withCredentials=true. | |
| add_header 'Access-Control-Allow-Credentials' 'true'; | |
| # # Tell the browser which response headers the JS can see | |
| # add_header 'Access-Control-Expose-Headers' 'myresponseheader'; | |
| } | |
| # if it's OPTIONS, for a CORS preflight request, then respond immediately with no response body | |
| if ($cors = "trueoptions") { | |
| # Tells the browser this origin may make cross-origin requests | |
| # (Here, we echo the requesting origin, which matched the whitelist.) | |
| add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
| # in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies) | |
| add_header 'Access-Control-Allow-Credentials' 'true'; | |
| # | |
| # Return special preflight info | |
| # | |
| # Tell browser to cache this pre-flight info for 20 days | |
| add_header 'Access-Control-Max-Age' 1728000; | |
| # Tell browser we respond to GET,POST,OPTIONS in normal CORS requests. | |
| # | |
| # Not officially needed but still included to help non-conforming browsers. | |
| # | |
| # OPTIONS should not be needed here, since the field is used | |
| # to indicate methods allowed for "actual request" not the | |
| # preflight request. | |
| # | |
| # GET,POST also should not be needed, since the "simple | |
| # methods" GET,POST,HEAD are included by default. | |
| # | |
| # We should only need this header for non-simple requests | |
| # methods (e.g., DELETE), or custom request methods (e.g., XMODIFY) | |
| add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; | |
| # Tell browser we accept these headers in the actual request | |
| # | |
| # A dynamic, wide-open config would just echo back all the headers | |
| # listed in the preflight request's | |
| # Access-Control-Request-Headers. | |
| # | |
| # A dynamic, restrictive config, would just echo back the | |
| # subset of Access-Control-Request-Headers headers which are | |
| # allowed for this resource. | |
| # | |
| # This static, fairly open config just returns a hardcoded set of | |
| # headers that covers many cases, including some headers that | |
| # are officially unnecessary but actually needed to support | |
| # non-conforming browsers | |
| # | |
| # Comment on some particular headers below: | |
| # | |
| # Authorization -- practically and officially needed to support | |
| # requests using HTTP Basic Access authentication. Browser JS | |
| # can use HTTP BA authentication with an XmlHttpRequest object | |
| # req by calling | |
| # | |
| # req.withCredentials=true, and | |
| # req.setRequestHeader('Authorization','Basic ' + window.btoa(theusername + ':' + thepassword)) | |
| # | |
| # Counterintuitively, the username and password fields on | |
| # XmlHttpRequest#open cannot be used to set the authorization | |
| # field automatically for CORS requests. | |
| # | |
| # Content-Type -- this is a "simple header" only when it's | |
| # value is either application/x-www-form-urlencoded, | |
| # multipart/form-data, or text/plain; and in that case it does | |
| # not officially need to be included. But, if your browser | |
| # code sets the content type as application/json, for example, | |
| # then that makes the header non-simple, and then your server | |
| # must declare that it allows the Content-Type header. | |
| # | |
| # Accept,Accept-Language,Content-Language -- these are the | |
| # "simple headers" and they are officially never | |
| # required. Practically, possibly required. | |
| # | |
| # Origin -- logically, should not need to be explicitly | |
| # required, since it's implicitly required by all of | |
| # CORS. officially, it is unclear if it is required or | |
| # forbidden! practically, probably required by existing | |
| # browsers (Gecko does not request it but WebKit does, so | |
| # WebKit might choke if it's not returned back). | |
| # | |
| # User-Agent,DNT -- officially, should not be required, as | |
| # they cannot be set as "author request headers". practically, | |
| # may be required. | |
| # | |
| # My Comment: | |
| # | |
| # The specs are contradictory, or else just confusing to me, | |
| # in how they describe certain headers as required by CORS but | |
| # forbidden by XmlHttpRequest. The CORS spec says the browser | |
| # is supposed to set Access-Control-Request-Headers to include | |
| # only "author request headers" (section 7.1.5). And then the | |
| # server is supposed to use Access-Control-Allow-Headers to | |
| # echo back the subset of those which is allowed, telling the | |
| # browser that it should not continue and perform the actual | |
| # request if it includes additional headers (section 7.1.5, | |
| # step 8). So this implies the browser client code must take | |
| # care to include all necessary headers as author request | |
| # headers. | |
| # | |
| # However, the spec for XmlHttpRequest#setRequestHeader | |
| # (section 4.6.2) provides a long list of headers which the | |
| # the browser client code is forbidden to set, including for | |
| # instance Origin, DNT (do not track), User-Agent, etc.. This | |
| # is understandable: these are all headers that we want the | |
| # browser itself to control, so that malicious browser client | |
| # code cannot spoof them and for instance pretend to be from a | |
| # different origin, etc.. | |
| # | |
| # But if XmlHttpRequest forbids the browser client code from | |
| # setting these (as per the XmlHttpRequest spec), then they | |
| # are not author request headers. And if they are not author | |
| # request headers, then the browser should not include them in | |
| # the preflight request's Access-Control-Request-Headers. And | |
| # if they are not included in Access-Control-Request-Headers, | |
| # then they should not be echoed by | |
| # Access-Control-Allow-Headers. And if they are not echoed by | |
| # Access-Control-Allow-Headers, then the browser should not | |
| # continue and execute actual request. So this seems to imply | |
| # that the CORS and XmlHttpRequest specs forbid certain | |
| # widely-used fields in CORS requests, including the Origin | |
| # field, which they also require for CORS requests. | |
| # | |
| # The bottom line: it seems there are headers needed for the | |
| # web and CORS to work, which at the moment you should | |
| # hard-code into Access-Control-Allow-Headers, although | |
| # official specs imply this should not be necessary. | |
| # | |
| add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since'; | |
| # build entire response to the preflight request | |
| # no body in this response | |
| add_header 'Content-Length' 0; | |
| # (should not be necessary, but included for non-conforming browsers) | |
| add_header 'Content-Type' 'text/plain charset=UTF-8'; | |
| # indicate successful return with no content | |
| return 204; | |
| } | |
| } |
Thanks for the comment. I can't check it right now, but that sounds 100% correct.
Use map instead http://serverfault.com/questions/674900/nginx-if-statement-inside-location-returns-404
If is evil. Lots of configuration, such as those using try_files will not work properly if you use if like this. See: http://wiki.nginx.org/IfIsEvil
@thoughtless, do you have a gist that uses map?
@thoughtless, can you elaborate how you would avoid including blank headers when using map? Your comment doesn't provide a whole lot of insight or guidance. For example, I don't see this working very well:
# ...
map "${request_method}" $cors_method {
'~*^(get|post)$' 'getpost';
'~*^OPTIONS$' 'options';
}
map "${http_origin}" $cors_enable {
default '';
'~*^https?://[^/]+\.(example)\.com(:[0-9]+)?$' 'o';
'~*^null' '*'; # for file:// URLs
}
map "${cors_enable}-${cors_method}" $allow_origin_header {
default '';
'~^o-(getpost|options)$' "$http_origin";
'~^\*-(getpost|options)$' '*';
}
# ...
map "${cors_enable}-${cors_method}" $content_length_header {
# This should only get set with the OPTIONS method
default '';
'~^[*o]-options$' '0';
}
add_header 'Access-Control-Allow-Origin' "${allow_origin_header}"; # probably okay
# ...
add_header 'Content-Length' "${content_length_header}"; # huh?
if ($content_length_header != '') {
return 204;
}
# ...
@posita I use maps in this gist https://gist.github.com/slbmeh/6e2dbc1218a0be0d7ae2
@algal - I reworked your conf to remove as many ifs as possible by using redirects instead. The main if check on $http_origin is still required. Instead of setting variables and checking them, I rewrite to /cors/$request_method$uri, I then create 3 location blocks, /cors/GET/, /cors/POST/, /cors/OPTIONS/ to set the appropriate headers and then rewrite once more to remove the cors prefix so Nginx will fall back to whatever locations you have set up. I had to rework it as it was causing me issues with v.1.6.2 and using it with proxy_pass to a uri end point.
it seems like https://gist.github.com/slbmeh/6e2dbc1218a0be0d7ae2 not work,
but https://gist.github.com/sbuzonas/6e2dbc1218a0be0d7ae2 dose.
I noted this on the original but you probably want a
$at the end of the regex on line 54. Without ithttps://anything.mckinsey.com.evil.comgets through.