Если вы сталкивались с 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;
}
}