cors-anywhere on a clean nginx config
If you have experienced CORS, then you know all the pain that a developer experiences when they need to go to an API on a different domain. If the server configuration is not available for configuration, then we used some solution based on an equally popular solution cors-anywhere.
Friday night nothing to do
Not many people know that the directive proxy_pass supports not only local domains and threads (aka upstream
), but also external sources, for example:
proxy_pass https://api.github.com/$request_uri
This is how the idea was born to write a universal (with some reservations) nginx config that supports any given domain.
What can we manage
We can declare new variables based on globals with regular expression support using map:
map $request_url $my_request_path {
~*/(.*)$ $1;
default "";
}
So, when asked for http://example.com/api
in a variable $my_request_path
will lie api
.
We can send additional headers to the client with add_header:
add_header X-Request-Path $my_request_path always;
We now have a title X-Request-Path
with meaning api
.
With the directive proxy_set_header add headers to the request that is sent proxy_pass
. And with the help proxy_hide_header hide headlines we got from proxy_pass
.
With the directive if process expressions, for example, when requested by the method OPTIONS
give immediately the desired response code:
if ($request_method = OPTIONS) {
return 204;
}
Putting it all together
To begin, let’s announce $proxy_uri
which we will extract from $request_uri
:
map $request_uri $proxy_uri {
~*/http://(.*)/(.+)$ "http://$1/$2";
~*/https://(.*)/(.+)$ "https://$1/$2";
~*/http://(.*)$ "http://$1/";
~*/https://(.*)$ "https://$1/";
~*/(.*)/(.+)$ "https://$1/$2";
~*/(.*)$ "https://$1/";
default "";
}
In short, it works like this: when requested http://example.com/example.ru
in a variable $proxy_uri
will lie https://example.ru
From received $proxy_uri
extract the part that will match the title origin:
map $proxy_uri $proxy_origin {
~*(.*)/.*$ $1;
default "";
}
For header forwarded we need to process 2 variables at once:
map $remote_addr $proxy_forwarded_addr {
~^[0-9.]+$ "for=$remote_addr";
~^[0-9A-Fa-f:.]+$ "for="[$remote_addr]"";
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
"" "$proxy_forwarded_addr";
default "$http_forwarded, $proxy_forwarded_addr";
}
Header processing X-Forwarded-For already built into nginx
Now we can move on to declaring our proxy server:
server {
listen 443 ssl;
server_name cors.example.com;
proxy_http_version 1.1;
proxy_pass_request_headers on;
proxy_pass_request_body on;
proxy_redirect off;
resolver 77.88.8.8 77.88.8.1 8.8.8.8 8.8.4.4 valid=1d;
location / {
if ($proxy_uri = "") {
# empty uri
return 403;
}
# add proxy cors headers
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Methods "*" always;
add_header Access-Control-Allow-Origin "*" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_set_header Host $proxy_host;
proxy_set_header Origin $proxy_origin;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";
proxy_pass $proxy_uri;
}
}
We got a minimally working proxy server, which processes CORS Preflight Request and the appropriate headers are added.
We do beautiful
Everything would be fine, but if the server to which we are proxiing has CORS processing configured, then its headers will be transmitted to the client. Let’s hide everything possible:
# hide original cors
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Request-Headers;
proxy_hide_header Access-Control-Request-Method;
It would also be nice to pass the client’s IP in order to somehow bypass the rate limit, which can occur if several users access the same resource:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Client-IP $remote_addr;
proxy_set_header CF-Connecting-IP $remote_addr;
proxy_set_header Fastly-Client-IP $remote_addr;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header X-Cluster-Client-IP $remote_addr;
We’re not talking about anonymity, right?)
And finally, we’ll improve performance a bit by turning off cache/buffering/etc:
sendfile on;
tcp_nodelay on;
tcp_nopush on;
etag off;
if_modified_since off;
proxy_buffering off;
proxy_cache off;
proxy_cache_convert_head off;
proxy_max_temp_file_size 0;
client_max_body_size 0;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
reset_timedout_connection on;
gzip off;
gzip_proxied off;
# brotli off;
Full config
map $request_uri $proxy_uri {
~*/http://(.*)/(.+)$ "http://$1/$2";
~*/https://(.*)/(.+)$ "https://$1/$2";
~*/http://(.*)$ "http://$1/";
~*/https://(.*)$ "https://$1/";
~*/(.*)/(.+)$ "https://$1/$2";
~*/(.*)$ "https://$1/";
default "";
}
map $proxy_uri $proxy_origin {
~*(.*)/.*$ $1;
default "";
}
map $remote_addr $proxy_forwarded_addr {
~^[0-9.]+$ "for=$remote_addr";
~^[0-9A-Fa-f:.]+$ "for="[$remote_addr]"";
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
"" "$proxy_forwarded_addr";
default "$http_forwarded, $proxy_forwarded_addr";
}
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/cors.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cors.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/cors.example.com/chain.pem;
server_name cors.example.com;
sendfile on;
tcp_nodelay on;
tcp_nopush on;
etag off;
if_modified_since off;
proxy_buffering off;
proxy_cache off;
proxy_cache_convert_head off;
proxy_max_temp_file_size 0;
client_max_body_size 0;
proxy_http_version 1.1;
proxy_pass_request_headers on;
proxy_pass_request_body on;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
reset_timedout_connection on;
proxy_redirect off;
resolver 77.88.8.8 77.88.8.1 8.8.8.8 8.8.4.4 valid=1d;
gzip off;
gzip_proxied off;
# brotli off;
location / {
if ($proxy_uri = "") {
return 403;
}
# add proxy cors
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Methods "*" always;
add_header Access-Control-Allow-Origin "*" always;
if ($request_method = "OPTIONS") {
return 204;
}
# pass client to proxy
proxy_set_header Host $proxy_host;
proxy_set_header Origin $proxy_origin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Client-IP $remote_addr;
proxy_set_header CF-Connecting-IP $remote_addr;
proxy_set_header Fastly-Client-IP $remote_addr;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header X-Cluster-Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";
# hide original cors
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Request-Headers;
proxy_hide_header Access-Control-Request-Method;
proxy_pass $proxy_uri;
}
}