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.ruin 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;
  }
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *