cors-anywhere на чистом конфиге nginx

0987db9078fda83503072c9004abcfb4

Если вы сталкивались с CORS, то знаете всю ту боль, которую испытывает разработчик, когда нужно сходить к API на другом домене. Если конфигурация сервера не доступна для настройки, то использовали какое-нибудь решение на основе не менее популярного решения cors-anywhere.

Пятница вечер делать нечего

Не многим изестно, что директива proxy_pass поддерживает не только локальные домены и потоки (aka upstream), но и внешние источники, например:

proxy_pass https://api.github.com/$request_uri

Так зародилась идея написать универсальный (с некоторыми оговорками) конфиг для nginx, который поддерживает любой переданный домен.

Чем мы можем управлять

Мы можем объявлять новые переменные на основе глобальных c поддержой регулярных выражений с помощью map:

map $request_url $my_request_path {
  ~*/(.*)$ $1;
  default  "";
}

Так, при запросе к http://example.com/api в переменной $my_request_path будет лежать api.

Мы можем отправлять клиенту дополнительные заголовки с помощью add_header:

add_header X-Request-Path $my_request_path always;

Теперь у нас добавился заголовок X-Request-Path с значением api.

С помощью директивы proxy_set_header добавлять заголовки к запросу, который отправляется proxy_pass. А с помощью proxy_hide_headerскрыватьзаголвки, которые мы получили от proxy_pass.

С помощью директивы if обрабатывать выражения, например, при запросе методом OPTIONS отдавать сразу нужный код ответа:

if ($request_method = OPTIONS) {
  return 204;
}

Собираем все вместе

Для начала объявим $proxy_uri который мы будем извлекать из $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               "";
}

Если коротко это работает так: при запросе http://example.com/example.ru, в переменной $proxy_uri будет лежать https://example.ru

Из полученного $proxy_uri извлечем часть, которая будет соответствовать заголовку Origin:

map $proxy_uri $proxy_origin {
  ~*(.*)/.*$ $1;
  default    "";
}

Для заголовка Forwarded нам понадобится обработать сразу 2 переменные:

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";
}

Обработка заголовока X-Forwarded-For уже встроена в nginx

Теперь мы можем перейти к объявлению нашего проксирующего сервера:

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

Мы получили минимально рабочий проксирующий сервер, у которого обрабатывается CORS Preflight Request и добавляются соответствующие заголовки.

Делаем красиво

Все бы хорошо, но если у сервера, к которому мы проксируем, будет настроена обработка CORS, то его заголовки будут передаваться клиенту. Давайте скроем все возможные:

# 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;

Хорошо бы еще передавать IP клиента, чтобы хоть как-то обходить rate limit, который может возникнуть, если несколько пользователей будут обращаться к одному ресурсу:

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;

Мы же не говорим про анонимность, верно?)

И, напоследок, немного улучшим производительность выключив кэш/буферизацию/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;

Конфиг полностью

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

© Habrahabr.ru