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