Определение IP и местоположения пользователя посетителя сайта 5.

Пришедшие вчера за помощью студенты натолкнули на мысль окончательно завершить данную тему.
Итак, чего не так в нашем скрипте для определения IP и местоположения пользователя?
А не так то, что мы анализируем лишь один содержащий IP параметр: REMOTE_ADDR. Т.е. на самом деле это правильно, как сказано в замечательной статье. Но всей информации мы можем не увидеть, и даже у пользователя из какой-нибудь Сызрани, сидящего через не анонимный прокси, вместо Сызрани будет гордо высвечиваться какой-нибудь Вашингтон. Исправим это, поступив точно так, как рекомендуют поступить в вышеозначенной статье. Поле REMOTE_ADDR будем анализировать в качестве первичного и основного источника информации, а потом пробежимся по всем заголовкам HTTP_ (VIA, X_FORWARDED_FOR, X_CLIENT_IP и т.д., сколько найдем), достанем из них все, что соответствует шаблону IP, скормим определялке географического положения и выдадим в качестве дополнительной информации.
Пользователь может сидеть не через единственный прокси, а через каскад (тоже не анонимный, хехе). В таком случае, в одном или нескольких заголовках HTTP_ могут быть перечислены несколько прокси, причем тут нет никаких стандартов. Вполне возможна ситуация «кто в лес, кто по дрова»: прокси будут перечислены через запятую, пробел, через знак |, двоеточие. Это тоже нужно учесть.

Наведем в скрипте порядок

Для начала небольшой: заведем условные области «подключаемых скриптов» и «глобальных переменных» перед условной «областью функций». Понятно, что в php нет никаких «областей» в скрипте, и писать можно что и где угодно, но мне так гораздо приятнее глазу.
Перенесем из условного «тела» скрипта в область внешних скриптов строчку:
include("SxGeo.php");
А в область глобальных переменных перенесем из функции isip() переменную
$ip_pattern="#(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)#"; и команду, создающую объект SxGeo:
$SxGeo = new SxGeo('SxGeoCity.dat');
Переменная с регулярным выражением IP у нас будет использоваться несколько раз в разных функциях, так что пусть лежит в одном месте (в начало функции isip() надо не забыть дописать код global $ip_pattern;), а объект SxGeo в процессе работы может несколько раз создаваться, так что пусть и создастся один раз в самом начале работы скрипта, потом просто будем к нему обращаться.
Представим структуру данных
На самом деле скрипт и ранее выдавал структуру данных, просто внимание на это я не обращал, а для улучшения скрипта лучше ее представить, унифицировать и дополнить для разных случаев, обрабатываемых скриптом.
Условная структура данных предыдущего скрипта была такова:
"IP=IP-адрес пользователя";
"ISO_CODE=Код страны (ISO)";
"COUNTRY_NAME=Страна";
"CTNR_LAT=Широта страны";
"CTNR_LON=Долгота страны";
"REGION_ISO=Код региона (ISO)";
"REGION_NAME=Регион";
"CITY_NAME=Город";
"CTY_LAT=Широта города";
"CTY_LON=Долгота города";
Дополню ее двумя полями:
"FIELD=Поле с данными об IP";
"MESSAGE=Сообщение об ошибке";

Т.е. скрипт будет также показывать информацию, из какого поля получена информация об IP (имя HTTP_-заголовка, REMOTE_ADDR или вручную, если IP был передан через GET-запрос в параметре ip), а также отображать статус своего исполнения (ОК или ошибка, и какая).

Создаем отдельную функцию определения местоположения IP

Создаем новую функцию get_info_ip($field, $ip) и переносим туда почти весь код из «тела» скрипта, немного его модифицируем:

function get_info_ip($field, $ip)
{
global $SxGeo;
$retv=""; //возвращаемое значение
// проверка на соответствие формату
if (!isip($ip))
{
//не IP - записали в поле MESSAGE сообщение об ошибке и прекратили работу
$retv=$field."|0.0.0.0|ERROR_NOT_IP|0|0|0|0|0|0|0|0|0|";
return $retv;
}
//проверяем, не попал ли IP в особый диапазон
$check_diap = get_spec_diap($ip);
if ($check_diap!=1)
{
$retv=$field."|".$ip."|".$check_diap."|0|0|0|0|0|0|0|0|0|";
return $retv;
}
$add_info = $SxGeo->getCityFull($ip); // Вся информация о городе
$main_info = $SxGeo->get($ip);         // Краткая информация о городе или код страны (если используется база SxGeo Country)
//"FIELD|IP|MESSAGE|ISO_CODE|COUNTRY_NAME|CTNR_LAT|CTNR_LON|REGION_ISO|REGION_NAME|CITY_NAME|CTY_LAT|CTY_LON|n";
$retv=$field."|".$ip."|OK|".$main_info['country']['iso']."|".$add_info['country']['name_en']."|".
$add_info['country']['lat']."|".$add_info['country']['lon']."|".
$add_info['region']['iso']."|".$add_info['region']['name_en']."|".
$main_info['city']['name_en'].'|'.$main_info['city']['lat']."|".$main_info['city']['lon'].'|';
return $retv;
}


Основные отличия от предыдущей версии скрипта в том, что:
1. Немного поменяна последовательность получения данных.
2. При ошибках и предупреждениях работа скрипта не прерывается командой die();, а формируется структура с заполненным сообщением об ошибке и пустыми данными
3. Функция сама не занимается выводом сообщений. Результат работы аккумулируется в переменной и возвращается вызвавшей функции.
4. Добавлен аргумент $field, куда вызвавшая функция должна записать источник IP-адреса.

Поиск IP в заголовках HTTP_
Алгоритм таков:
1. В цикле обойдем массив $_SERVER и если в его элементах HTTP_ будет найдено совпадение с регулярным выражением для ip, то вызовем функцию
preg_match_all($ip_pattern,$v,$matches);
в таком виде, где:
$ip_pattern — регулярное выражение для IP
$v — значение элемента массива $_SERVER
$matches — многомерный массив, в который preg_match_all запишет все найденные IP.
2. С помощью двух вложенных циклов обойдем массив $matches и передадим каждый IP функции get_info_ip

function get_all_info_ip() //получаем информацию о всех IP, из всех переменных HTTP_* сервера
{
global $ip_pattern;
$ret="";
foreach ($_SERVER as $k => $v)
{
//если нашли в поле HTTP_* (HTTP_VIA, HTTP_X_FORWARDED_FOR и т.д.)
//что-то похожее на IP
if ((substr($k,0,5)=="HTTP_") AND (preg_match($ip_pattern,$v)))
{
preg_match_all($ip_pattern,$v,$matches); //вытаскиваем из строки все совпадения с шаблоном IP
foreach ($matches as $tmp) //preg_match_all выдает многомерный массив
{
foreach($tmp as $ip) //вытаскиваем каждый отдельный IP
{
$ret.=get_info_ip($k,$ip)."n"; //получаем информацию для каждого IP
}
}
}
}
return $ret;
}


Модифицируем тело скрипта

Выведем структуру данных и сообщение о начале анализа основного IP:
echo "FIELD|IP|MESSAGE|ISO_CODE|COUNTRY_NAME|CTNR_LAT|CTNR_LON|REGION_ISO|REGION_NAME|CITY_NAME|CTY_LAT|CTY_LON|n";
echo '---START-MAIN-DATA---'."n";
Если IP был передан через переменную GET-запроса ip, то анализируем только его, выводим информационную строку о завершении анализа основного IP и прерываем работу скрипта:

if (isset($_GET['ip']))
{
$ip=$_GET['ip'];
echo get_info_ip("MANUAL",$ip)."n";
echo '---END-MAIN-DATA---'."n";
die();
}


Если IP поступил от пользователя, анализируем REMOTE_ADDR, потом заголовки HTTP_, выводим данные:
$ip = $_SERVER['REMOTE_ADDR'];
echo get_info_ip("REMOTE_ADDR",$ip)."n";
echo '---END-MAIN-DATA---'."n";
echo '---START-ADD-DATA---'."n";
$ip=get_all_info_ip();
echo $ip;
echo '---END-ADD-DATA---'."n";

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


Каскад прокси

Анонимный прокси, заполняющий несколько заголовков HTTP_

Скачать. Посмотреть код на PasteBin Посмотреть в работе
Впрочем, совсем не составляет труда сделать ему вид, более радующий глаз человека:
1. В строке header('Content-type: text/plain; charset=utf8'); изменим text/plain на text/html
2. Модифицируем сообщения скрипта.
3. Добавим код, выводящий оформление HTML (2 и 3 см. в самом скрипте ниже)

Каскад прокси
Анонимный прокси, заполняющий несколько заголовков HTTP_

Скачать. Посмотреть код на PasteBin Посмотреть в работе
Предыдущая серия

Это перепост заметки из моего блога на LJ.ROSSIA.ORG
Оригинал находится здесь: http://lj.rossia.org/users/hex_laden/330304.html
Прокомментировать заметку можно по ссылке выше.