- PVSM.RU - https://www.pvsm.ru -
Давеча был свидетелем одного интересного спора о том как же действительно нужно определять IP адрес конечного пользователя из скриптов PHP.
Собственно, каждое слово сабжа отображает действительную ситуацию. Это был религиозный спор обострённый весенней замечательной погодой, в котором, я считаю, не оказалось правых и не правых, но который побудил меня к мини-исследованию и к моему счастью поставил точку в понимании этого конфессионального но по факту очень простого вопроса.
Для тех, кто как и я сомневался был уверен, что во всём разобрался, но боялся спросить лень было разбираться в мелочах — под кат.
Занимаясь разработкой VOD сервиса для Samsung SmartTV платформы нам непременно нужно знать страну пользователя, чтобы вдруг нечаянно не показать счастливому пользователю фильм там, где запрещает правообладатель… А ведь за нарушение данного условия договора идут не детские штрафы в тысячах долларов (при чем за каждый факт такой оплошности).
На сервере имеем следующее: php-fpm+nginx
Как определить страну? Ну естественно через IP пользователя и GEO IP базу maxmind [1]
«Пффф....» — подумалось нам всем мне — да проще простого. И дабы не писать свой велосипед, нагуглил на stackoverflow [2], даже вник в каждую строчку, прикрутил и оставил как там и росло код:
public function getUserHostAddress(){
if (!empty($_SERVER['HTTP_X_REAL_IP'])) //check ip from share internet
{
$ip=$_SERVER['HTTP_X_REAL_IP'];
}
elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) //check ip from share internet
{
$ip=$_SERVER['HTTP_CLIENT_IP'];
}
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) //to check ip is pass from proxy
{
$ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
}
else
{
$ip=$_SERVER['REMOTE_ADDR'];
}
return $ip;
}
И всё работало! Почти год… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…
Всё сломалось! А случилось это когда нам пришлось прикручивать одну из платёжных систем и весь этот код рухнул от того, что в HTTP_X_FORWARDED_FOR пришёл не один адрес, а список адресов через запятую (что строго говоря законно, допустимо, и даже не регламентировано в доке по php [3])
И никто бы ничего не заметил, если бы HTTP_X_REAL_IP или HTTP_CLIENT_IP(которые тоже не регламентирован докой) содержали искомый IP, но увы они были пусты :(
«Ну ладно» — подумали мы(теперь я был уже не один) перепишем всё и попросим админов запихивать пользовательский IP в переменную REMOTE_ADDR:
public function getUserHostAddress(){
$ip=$_SERVER['REMOTE_ADDR'];
return $ip;
}
И всё работало! Почти месяц… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…
Всё сломалось! А случилось это потому, что нам нужно было обновить nginx. И мы обратились к профессионалам в этом деле — к нашим админам.
А те в свою очередь решили обновить и конфиг избавившись от нашего «костыля/не костыля» (пока мы этого не поняли) с пробросом в REMOTE_ADDR.
REMOTE_ADDR оставили без изменения т.е. там теперь светилось что-то типа «127.0.0.1»
в HTTP_X_FORWARDED_FOR прокинули IP пользователя (который между делом с лёгкостью удалось переопределить отправкой из браузера заголовка `x-forwarded-for: 999.999.999.999`)
И тут понеслось — Р=Разраб, А=Админ:
А: у вас всё сломалось, и поскольку мы имеем nginx-прокси то нужный вам адрес лежит в HTTP_X_FORWARDED_FOR а в REMOTE_ADDR будет лежать реальный IP сдресс клиента к php-fpm (т.е. 127.0.0.1)
Р: но мы не можем верить HTTP_X_FORWARDED_FOR, ведь это переменная, которую с лёгкостью можно переопределить через заголовок к серверу, ссылаясь на давольно интересную статью [4]
А: нет, мы сделаем так что в ней будет лежать реальный IP конечного пользователя, а в REMOTE_ADDR реальный адрес клиента к php
Р: тогда мы не проследим последовательность проксей, и всё равно для универсализации на другом сервере (скажем без прокси) эти конфиги могут быть не правдивыми пихайте всё в REMOTE_ADDR который в любом случае будет работать.
… это кратко и без матов…
По итогу то конечно всё завелось… и остановились на прозрачном проксировании, когда php думает, что к нему подключаются напрямую клиенты безо всяких проксей и все переменные(точнее одна на которую мы обращаем внимание) в нужном нам состоянии.
Однако не хватает фэншуя в этом деле и по факту у нас ведь есть прокся а может и не одна.
Судить не нам, но никто!
Если мы имеем действительно кучу клиентов напрямую к php, или прозрачное проксирование то всё просто — юзай REMOTE_ADDR на здоровье и наслаждайся.
Но как быть с фэншуем и где что должно лежать, если мы используем нормальное проксирование и хотим чтобы об этом знал PHP?
Казалось бы ну в чем проблема парсить эту переменную и доставать оттуда последний элемент. Но в нашем случае настройки не были до конца корректными и весь HTTP_X_FORWARDED_FOR заменялся заголовком от браузера x-forwarded-for, а должен был приклеивать к нему реальный IP непосредственного пользователя.
Для примера проверил на промышленном
Доверять таким данным тоже страшновато, но если всё правильно сделано в настройках то последним IP будет адресс пользователя, вне зависимости от того что придёт в заголовках.
Казалось бы ну в чем проблема парсить эту переменную и доставать оттуда первый элемент. Но как было показано выше на рисунке, это уж точно не корректные данные и пользователь может нас ввести в заблуждение в два счёта, прислав в x-forwarded-for первым элементом какой захочет IP
Для удобства можно использовать специальный модуль для nginx [6] который нивелирует проблемы определения каскадного и не каскадного проксирования, но он по умолчанию «в стандартных сборках центоса, дебика и федоры nginx идет, почему-то без параметра --with-http_realip_module»(с)Админ, а так же для него должен быть корректно сформированна цепочка в HTTP_X_FORWARDED_FOR и настроены адреса доверенных прокси серверов от которых мы можем брать последний элемент из HTTP_X_FORWARDED_FOR
Однако опять же HTTP_X_REAL_IP это не реальный в общем случае IP конечного пользователя, а лишь первый IP в списке проксей при каскадном проксировании.
Хотя если проксирование не каскадное, то там может лежать и адрес конечного пользователя.
А если проксирование каскадное и корректно настроен модуль http_realip то там должен лежать либо IP конечного пользователя либо корректный IP первой недоверенной прокси если считать от php-сервера, что для нас тоже сгодится
Есть несколько вариантов проксирования для php+nginx
PS
Позже мы наведём фэншуй в настройках и избавимся от прозрачного проксирования, а так же напишем универсальную функцию определения IP для обоих случаев проксирования.
PPS
Ради фана кому интересно: если кто-то в комментариях напишет эту функцию и конфиг nginx за нас и мы её будем использовать, то под честное слово, тот получит 100р на телефон.
Но эта функция и конфиг должны быть во истину православными и учитывать всё :) все зацепки есть в статье.
Главное — дзен: не торопитесь — вдруг первые напишут с ошибками и вы их учтёте, торопитесь — вдруг первый правильный ответ будет до Вас.
Всем спасибо. Хорошей весны! Договаривайтесь с коллегами и любите их! :)
Автор: ZmeeeD
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/32428
Ссылки в тексте:
[1] maxmind: http://www.maxmind.com/
[2] нагуглил на stackoverflow: http://stackoverflow.com/questions/55768/how-do-i-find-a-users-ip-address-with-php
[3] доке по php: http://www.php.net/manual/en/reserved.variables.server.php
[4] статью: http://www.phpfaq.ru/ip
[5] vps: https://www.reg.ru/?rlink=reflink-717
[6] модуль для nginx: http://nginx.org/ru/docs/http/ngx_http_realip_module.html
[7] Источник: http://habrahabr.ru/post/177113/
Нажмите здесь для печати.