Pull to refresh

Об использовании regexp в map nginx

Reading time3 min
Views18K

Давно ничего не писал, поэтому разбавим конец пятницы простыми, но не всегда очевидными иcканиями в Nginx.

В этом веб-сервере есть замечательная директива map, которая позволяет существенно упростить и сократить конфиги. Суть директивы в том, что она позволяет создать новую переменную, значение которой зависит от значений одной или нескольких исходных переменных. Ещё большую силу директива приобретает при использовании регулярных выражений, но при этом забывается об одном важном моменте. Выдержка из мануала:

Поскольку переменные вычисляются только в момент использования, само по себе наличие даже большого числа объявлений переменных map не влечёт за собой никаких дополнительных расходов на обработку запросов.

И здесь важным является не только, то что "map не влечёт за собой никаких дополнительных расходов на обработку запросов", а и то, что "переменные вычисляются только в момент использования".

Как известно, конфиг Nginx, в основном, декларативен. Это касается и директивы map, и, не смотря на то, что она расположена в контексте http, её вычисление не происходит до момента обработки запроса. То есть при использовании результирующей переменной в контекстах server, location, if и т.п. мы "подставляем" не готовый результат вычисления, а лишь "формулу" по который этот результат будет вычислен в нужный момент. В этой конфигурационной казуистике не возникает проблем до того момента, пока мы не используем регулярные выражения. А именно регулярные выражения с выделениями. А ещё точнее, регулярные выражения с неименованными выделениями. Проще показать на примере.

Допустим у нас есть домен example.com с множеством поддоменов 3-го уровня, а-ля ru.example.com, en.example.com, de.example.com и т.д., и мы хотим их перенаправить на новые поддомены ru.example.org, en.example.org, de.example.org и т.п. Вместо того чтобы описывать сотни строк редиректов мы поступим вот так:

map $host $redirect_host {
  default "example.org";
  "~^(\S+)\.example\.com$"  $1.example.org;
}
server {
    listen       *:80;
    server_name  .example.com;
  location / {
        rewrite ^(.*)$ https://$redirect_host$1 permanent;
    
}

Здесь мы ошибочно ожидали, что при запросе ru.example.com произойдет вычисление регулярки в map и, соответственно, попав в location переменная $redirect_host будет содержать значение ru.example.org, однако на деле это не так:

$ GET -Sd ru.example.com
GET http://ru.example.com
301 Moved Permanently
GET https://ru.example.orgru

Оказалось, что на момент исполнения запроса наша переменная равна ru.example.orgru. Всё из-за того, что мы пренебрегли предупреждением "переменные вычисляются только в момент использования" и в нашем rewrite оказалась некая регулярка вложенная в другую регулярку.

Самый простой вариант решения - не использовать regexp одновременно и в map и в месте вычисления переменной, например, для данного конкретного случая это может выглядеть так:

map $host $redirect_host {
  default "example.org";
  "~^(\S+)\.example\.com$"  $1.example.org;
}
server {
    listen       *:80;
    server_name  .example.com;
    location / {
        return 301 https://$redirect_host$request_uri;
    }
}

Но что делать, если альтернативного решения без регулярок нет (или просто очень хочется).
Пробуем использовать именованные выделения в map:

map $host $redirect_host {
  default "example.org";
  "~^(?<domainlevel3>\S+)\.example\.com$"  $domainlevel3.example.org;
}
server {
    listen       *:80;
    server_name  .example.com;
    location / {
        rewrite ^(.*)$ https://$redirect_host$1 permanent;
    }
}

Попытка не увенчалась успехом:

$ GET -Sd ru.example.com
GET http://ru.example.com
301 Moved Permanently
GET https://ru.example.orgru

так как наше неименованное выделение $1 получит результат именованного $domainlevel3. То есть необходимо использовать именованные выделения в обеих регулярках:

map $host $redirect_host {
  default "example.org";
  "~^(?<domainlevel3>\S+)\.example\.com$"  $domainlevel3.example.org;
}
server {
    listen       *:80;
    server_name  .example.com;
    location / {
        rewrite ^(?<requri>.*)$ https://$redirect_host$requri permanent;
    }
}

И теперь всё работает как ожидалось:

$ GET -Sd ru.example.com
GET http://ru.example.com
301 Moved Permanently
GET https://ru.example.org/
Tags:
Hubs:
Total votes 22: ↑22 and ↓0+22
Comments2

Articles