- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

add_header X-Request-Path $my_request_path always;

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

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

С помощью директивы if [7] обрабатывать выражения, например, при запросе методом 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 [8]:

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

Для заголовка Forwarded [9] нам понадобится обработать сразу 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 [10] уже встроена в nginx [11]

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

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 [12] и добавляются соответствующие заголовки.

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

Все бы хорошо, но если у сервера, к которому мы проксируем, будет настроена обработка 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;
  }
}

Автор: Антон Петров

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/nginx/372092

Ссылки в тексте:

[1] cors-anywhere: https://github.com/Rob--W/cors-anywhere

[2] proxy_pass: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

[3] map: https://nginx.org/en/docs/http/ngx_http_map_module.html#map

[4] add_header: https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header

[5] proxy_set_header: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header

[6] proxy_hide_header: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_hide_header

[7] if: https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#if

[8] Origin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin

[9] Forwarded: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded

[10] X-Forwarded-For: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For

[11] nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#variables

[12] CORS Preflight Request: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

[13] Источник: https://habr.com/ru/post/651253/?utm_source=habrahabr&utm_medium=rss&utm_campaign=651253