Http remote addr php

Гормональный holywar Админа и Разраба PHP или REMOTE_ADDR vs HTTP_X_FORWARDED_FOR

Давеча был свидетелем одного интересного спора о том как же действительно нужно определять IP адрес конечного пользователя из скриптов PHP.
Собственно, каждое слово сабжа отображает действительную ситуацию. Это был религиозный спор, обострённый весенней замечательной погодой, в котором, я считаю, не оказалось правых и не правых, но который побудил меня к мини-исследованию и, к моему счастью, поставил точку в понимании этого конфессионального но по факту очень простого вопроса.
Для тех, кто как и я сомневался был уверен, что во всём разобрался, но боялся спросить лень было разбираться в мелочах — под кат.

Предыстория

Занимаясь разработкой VOD сервиса для Samsung SmartTV платформы нам непременно нужно знать страну пользователя, чтобы вдруг нечаянно не показать счастливому пользователю фильм там, где запрещает правообладатель… А ведь за нарушение данного условия договора идут не детские штрафы в тысячах долларов (при чем за каждый факт такой оплошности).
[Вопрос, как заметили в комментариях, Юридический, и мошенничество возможно, но статья даже не о том как постараться предотвратить такие мошенничества, а о том как правильно подружить php и nginx]

На сервере имеем следующее: php-fpm+nginx

Как определить страну? Ну естественно через IP пользователя и GEO IP базу maxmind
«Пффф. » — подумалось нам всем мне — да проще простого. И дабы не писать свой велосипед, нагуглил на stackoverflow, даже вник в каждую строчку, прикрутил и оставил как там и росло код:

 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; > 

И всё работало! Почти год… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…

Как запутать php или цепочка прокси(всё ещё часть предыстории)

Всё сломалось! А случилось это когда нам пришлось прикручивать одну из платёжных систем и весь этот код рухнул от того, что в HTTP_X_FORWARDED_FOR пришёл не один адрес, а список адресов через запятую (что строго говоря законно, допустимо, и даже не регламентировано в доке по php)
И никто бы ничего не заметил, если бы HTTP_X_REAL_IP или HTTP_CLIENT_IP(которые тоже не регламентирован докой) содержали искомый IP, но увы они были пусты 🙁

Читайте также:  Curl ssl verification disable php

«Ну ладно» — подумали мы(теперь я был уже не один) перепишем всё и попросим админов запихивать пользовательский IP в переменную REMOTE_ADDR:

 public function getUserHostAddress()

И всё работало! Почти месяц… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…

Весенний спор крутых мужиков(это не ирония — они крутые)

Всё сломалось! А случилось это потому, что нам нужно было обновить 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, ведь это переменная, которую с лёгкостью можно переопределить через заголовок к серверу, ссылаясь на давольно интересную статью
А: нет, мы сделаем так что в ней будет лежать реальный IP конечного пользователя, а в REMOTE_ADDR реальный адрес клиента к php
Р: тогда мы не проследим последовательность проксей, и всё равно для универсализации на другом сервере (скажем без прокси) эти конфиги могут быть не правдивыми пихайте всё в REMOTE_ADDR который в любом случае будет работать.

По итогу то конечно всё завелось… и остановились на прозрачном проксировании, когда php думает, что к нему подключаются напрямую клиенты безо всяких проксей и все переменные(точнее одна на которую мы обращаем внимание) в нужном нам состоянии.
Однако не хватает фэншуя в этом деле и по факту у нас ведь есть прокся а может и не одна.

Кто виноват из них кто прав

Если мы имеем действительно кучу клиентов напрямую к php, или прозрачное проксирование то всё просто — юзай REMOTE_ADDR на здоровье и наслаждайся.

Но как быть с фэншуем и где что должно лежать, если мы используем нормальное проксирование и хотим чтобы об этом знал PHP?

Рецепт… но не панацея:

  • REMOTE_ADDR — содержит IP адрес непосредственно обращающегося к нему nginx, в нашем случае 127.0.0.1
  • HTTP_X_FORWARDED_FOR — содержит цепочку прокси адресов и последним идёт IP непосредственного клиента обратившегося к прокси серверу. И тут рассмотрим два частных случая:

    Не каскадное проксирование. В HTTP_X_FORWARDED_FOR последним или единственным IP адресом (в зависимости от того что прислал/не прислал пользователь в заголовке x-forwarded-for) будет реальный, искомый, тот самый адрес пользователя.

Казалось бы ну в чем проблема парсить эту переменную и доставать оттуда последний элемент. Но в нашем случае настройки не были до конца корректными и весь HTTP_X_FORWARDED_FOR заменялся заголовком от браузера x-forwarded-for, а должен был приклеивать к нему реальный IP непосредственного пользователя.

Для примера проверил на промышленном vps хостинге:

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

Для удобства можно использовать специальный модуль для nginx который нивелирует проблемы определения каскадного и не каскадного проксирования, но он по умолчанию «в стандартных сборках центоса, дебика и федоры nginx идет, почему-то без параметра —with-http_realip_module»(с)Админ, а так же для него должен быть корректно сформированна цепочка в HTTP_X_FORWARDED_FOR и настроены адреса доверенных прокси серверов от которых мы можем брать последний элемент из HTTP_X_FORWARDED_FOR

В заключении

  • Прозрачное — характерно неизменное содержание переменных в _SERVER (в том числе и REMOTE_ADDR) как если бы мы работали напрямую с php
  • Не прозрачное не каскадное — характерно то, что Админу и Разрабу нужно договориться, где будет храниться реальные IP адрес пользователя 🙂
  • Не прозрачное каскадное — характерно то же самое что и для не прозрачного не каскадного + правильно настроенный модуль для nginx А также необходимо помнить о возможности каскадного проксирования и о том, что пользователь злой и может присылать в _SERVER[«HTTP_xxxx»] очень неправдивые данные

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

PPS
Ради фана кому интересно: если кто-то в комментариях напишет эту функцию и конфиг nginx за нас и мы её будем использовать, то под честное слово, тот получит 100р на телефон.
Но эта функция и конфиг должны быть во истину православными и учитывать всё 🙂 все зацепки есть в статье.
Главное — дзен: не торопитесь — вдруг первые напишут с ошибками и вы их учтёте, торопитесь — вдруг первый правильный ответ будет до Вас.

Всем спасибо. Хорошей весны! Договаривайтесь с коллегами и любите их! 🙂

 /** * @param null|string $ip_param_name - ключ элемента _SERVER, в котором нужно искать IP адрес * если не задано ищем по индексу REMOTE_ADDR и считаем что проксирование отсутствует или прозрачное, * если задано считаем что IP пробрасывается по заданному индексу, * например по индексу HTTP_X_REAL_IP или любому другому * @param bool $allow_non_trusted - защита, при заданном $ip_param_name но * отсутствующем или не валидном значении _SERVER[$ip_param_name] * если задано будем искать в _SERVER по ключам из аргумента $non_trusted_param_names * @param array $non_trusted_param_names - массив ключей, по которым будем искать IP в массиве _SERVER * @throws Exception * @return string */ public function getUserHostAddress( $ip_param_name = null, $allow_non_trusted = false, array $non_trusted_param_names = array('HTTP_X_REAL_IP','HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','REMOTE_ADDR') )< if(empty($ip_param_name) || !is_string($ip_param_name))< // если не задан или не корректен $ip = $_SERVER['REMOTE_ADDR']; >else< //иначе используем нужную переменную if(!empty($_SERVER[$ip_param_name]) && filter_var($_SERVER[$ip_param_name], FILTER_VALIDATE_IP))< // если переменная подошла как надо $ip = $_SERVER[$ip_param_name]; >else if($allow_non_trusted) < // мы решили пойти на крайний шаг и использовать сырые данные foreach($non_trusted_param_names as $ip_param_name_nt)< if($ip_param_name === $ip_param_name_nt) // мы уже проверяли эту переменную continue; if(!empty($_SERVER[$ip_param_name_nt]) && filter_var($_SERVER[$ip_param_name_nt], FILTER_VALIDATE_IP))< // если переменная подошла как надо $ip = $_SERVER[$ip_param_name_nt]; break; >> > > if(empty($ip)) // так и не нашли подходящих ip, хотя по умолчанию в $_SERVER['REMOTE_ADDR'] что-то должно лежать throw new Exception("Can't detect IP"); return $ip; > 

Источник

Массив $_SERVER

Описание значений глобального массива $_SERVER с примерами.

Параметры сервера

Имя хоста, обычно совпадает с доменом.

Название и версия сервера.

Версия сервера и имя виртуального хоста, обычно пуста.

Имя и версия используемого HTTP протокола.

Значение из директивы конфигурационного файла Apache.
На хостингах указывают контактный e-mail.

Параметры соединения

Имя сервера, как правило, совпадает с доменом.

IP-адрес, с которого пользователь просматривает текущую страницу.

64.246.37.238 fe80:0:0:0:200:f8ff:fe21:67cf

Удаленный хост, с которого пользователь просматривает текущую страницу.

Порт на удаленной машине, который используется для связи с веб-сервером.

Время запроса к серверу в Unix timestamp.

​Время запроса к серверу с точностью до микросекунд.

Пути на сервере

Директория корня сайта, в которой выполняется текущий скрипт.

/home/example.com/public_html

Появился в Apache2, то же самое что и DOCUMENT_ROOT .

Содержит путь, содержащийся после имени скрипта.
Например для адреса http://site.ru/index.php/123 значение будет следующим:

Исходное значение переменной PATH_INFO перед обработкой PHP.

Путь и имя выполняемого скрипта.

​Путь к исполняемому скрипту относительно корня сайта, обычно равен SCRIPT_NAME .

​Абсолютный путь к исполняемому скрипту.

/home/example.com/public_html/index.php

Авторизация на .htpasswd

Метод HTTP аутентификации.

$_SERVER[‘REMOTE_USER’] и $_SERVER[‘PHP_AUTH_USER’]

HTTPS

$_SERVER[‘HTTPS’] , $_SERVER[‘HTTP_X_HTTPS’] , $_SERVER[‘REDIRECT_HTTPS’]

URL

Значения в примерах приведены для адреса http://site.ru/index.php?page=1&sort=2

URI страницы с GET-параметрами, без домена.

Количество элементов массива $_SERVER[‘argv’] .

​Содержит URL страницы без GET-параметров и домена.

Заголовки браузера

Строка, обозначающая браузер и операционную систему, который открыл данную страницу.

Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36

Куки браузера в виде строки: ключ=значение; ключ=значение;.
Данные доступны в переменной $_COOKIE .

_ym_uid=xxx; _ym_d=xxx; PHPSESSID=xxx;

Адрес страницы, с которой браузер пользователя перешёл на текущую страницу.

Содержимое заголовка Accept из текущего запроса.

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

HTTP заголовок переданный клиентом, говорящий о том какие алгоритмы сжатия он может понять.

​Содержимое заголовка Accept-Language .

Предпочтения клиента относительно кодировки.

Значение заголовка Connection .

Браузер отправляет этот заголовок со значением 1 , выражающий предпочтение клиента для зашифрованного ответа.

Дамп переменной $ _SERVER

Для тестирования, значения массива $ _SERVER для разных клиентов можно скидывать в лог-файл:

file_put_contents(__DIR__ . '/server.log', print_r($_SERVER, true) . PHP_EOL, FILE_APPEND);

Источник

Оцените статью