MNP и появление API 1.0 — Добровольное поедание кактусов

от автора


Предисловие

За время многолетнего опыта в IT у многих из Вас были мысли или даже опыт создания своего API, в которое было бы заключено все то самое полезное и ценное, а в определенный момент пазл складывается и начинается этап выращивания и поедания собственного кактуса.
После года обкатки, перед дальнейшей модификацией и развитием, хочу поделиться получившимся функционалом сервиса определения мобильного оператора по номеру телефона (только РФ), подвергнуть само API и сервис Вашей критике 🙂
В статье есть примеры на PHP для обкатки на своей стороне для тех, кому он будет полезен в обмен на ценные замечания 😉

Волшебный пендель в лице отмены мобильного рабства

Еще во времена 2003-2004 года, как сейчас помню, клиент пришел заполнять бумажный бланк для пополнения счета телефона. В графе оператора пользователь указал Билайн, а до боли знакомый код 916 явно мне подсказывал, что это МТС и платеж не будет благословлен, на что клиент меня успокоил: «Этот номер перенесен к другому оператору» — сказал он.
Спустя 10 лет эта процедура была уже законодательно утверждена и, наконец, заработала, только IT-специалисты получили небольшое увеличение нагрузки в виде подготовки систем к распознаванию операторов по номеру с учетом этой переносимости номеров от оператора к оператору — MNP.

Появление данных

Как водится, у всего централизованного есть единый реестр. В данном случае их было два: один — с редко изменяемыми данными — они определяли принадлежность номеров операторам большими блоками, например, 916 ХХХ-ХХ-ХХ. Второй — с динамическими, теми самыми перенесенными номерами — то есть каждый номер был отдельной записью.
Опустим технические детали. Просто был большой пласт, поверх которого «тусили» отдельные номера — вначале лукапили оператора диапазона, затем смотрели, есть ли записи о переносе номера к другому оператору. Пласт обновлялся раз в месяц, отдельные номера — каждый час (по мере переноса номеров).

Хранение и обновление

Осознав, что подход нужен более капитальный, на коленке решено было не делать… Да и какая тут уже коленка — полноценные договора между юридическими лицами для получения доступа к динамической части базы, регламентированная синхронизация — это уже не пришить к каждому отдельному проекту, который использовал эти данные — громоздко. Явно просматривался центр — место хранения данных.

Оффтопик: немного о значимости оператора

Представьте колл-центр. Он совершает множество вызовов и применяет различные схемы выбора оператора для звонка — для звонков на номера МГТС задействуется свой стык, с мобильными — свой. Становилось все грустнее и грустнее, когда все больше и больше звонков совершалось не по тем каналам по мере миграции абонентов — ошибочно выбранный канал — это потеря вполне конкретных рублей, которые безжалостно поедали премиальный фонд сотрудников.

Появление центра — API

Набросать первые пару скриптиков, которые бы отдавали данные — дело то не хитрое. Но одно дело делать для себя, другое — для продуктов, которые находились на стороне заказчика. Тем более, в воздухе витала идея полезного приложения для Android, а это уже пахло доступом для внешнего мира, возможные атаки с целью «сломать» приложение, да и просто школота, порой, из любознательности и для самоутверждения любит что-то поломать. Поэтому, внезапно появляется явный FRONT-END, AAA, BACK-END и обработчики запросов.

Про обработчик…

Очень не хотелось, чтобы запросы клиентов API обрабатывались на лету где-то на BACK-END’е — гораздо интереснее представлялось сложить запросы на диск и обрабатывать множеством отдельных серверов, которые можно легко масштабировать. На том и сошлись.

«Привет, Мир!» — самое сложное на самом простом примере

Вот здесь самое интересное. Обращение к API начинается с авторизации — получения токена для дальнейшего взаимодействия с системой. В качестве входных данных используются аж три идентифицирующих значения — имя пользователя, телефон, e-mail + пароль.

Шозабред!?

В нашей жизни много переменных:

  • корпоративный e-mail не дали унести с прежнего места работы
  • телефон бесконтрольно ушел в минус во время командировки и номер отнял оператор
  • пароль скомпрометирован

вот и получается, что неизменные только имя пользователя, да и то «пока», мне кажется. А все остальное очень даже непостоянно… А поменять один реквизит при наличии двух других — не проблема и вполне эквивалентно авторизации в несколько этапов (двухфакторная).

На одну учетную запись выдается только один токен в единицу времени. Его можно сменить, что вызовет «отзыв» полномочий у скриптов и приложений, которые под ним работали. Это и хорошо и плохо, но главное — контролируемо.

Заметка про токены

Один из сервисов предназначен для регистрации аккаунтов.
Представьте, что посетитель сайта хочет воспользоваться каким-то функционалом сервиса на стороннем сайте — сам сайт запросит для него новую учетку, авторизует (=получит токен), сообщит в параметрах сессий веб-браузера токен для взаимодействия с сервисом. Средствами браузера пользователь сам сможет совершать запросы, но у вас останется «руль» сессий не только внутри вашего портала, но и доступ к внешнему API вы легко закроете сменив токен, если сочтете, что доступ к информации пользователь более не должен получать.

Но прежде, мы сходим и стянем публичный ключик для взаимодействия поверх небезопасных сетей без SSL и следом подготовим данные.

Пример из практики: `SSL под запретом`

Кто знает, может у нас нет доступа по 443 портам — до сих пор помню один случай-паранойю Службы Безопасности одной большой конторы, желавшей все «прослушивать» и не пускавшей никуда по SSL, даже сервера :-/ Другого пути просто не было.

$rsa_key = file_get_contents ('http://core.api.netresult.ru/rsa_public.pem'); $client_secret = md5(time());  $data = json_encode( 	array ('request_type' => 'auth', 		'username' =>'habr_demo_20160426', 		'email' => 'support@media24-corp.ru', 		'phone' => '74995799366', 		'password' => md5('megapass'), 		'client_secret' => $client_secret) ); $pk  = openssl_get_publickey($rsa_key); openssl_public_encrypt($data, $encrypted, $pk); $data = array('body' => chunk_split(base64_encode($encrypted))); 

Вот такой кошмар ради простого запроса — эта куча манипуляций призвана предостеречь от изменения данных при передаче по каналам связи и для легкой валидации на FRONT-END’e.

function api_build_url ( 	$scheme='https', // протокол - http|https 	$domain='core.api.netresult.ru', // домен API, общий публичный - core.api.netresult.ru 	$validation_string_prefix='s',  // строка без слешей, может быть произвольной (в соответствии с настройками) 	$validation_secret='earth', // ключик для проверка запроса, прилетающего на FRONT-END - проверяется FRONT-END`ом в составе всего URI (строка добавленная к URI) 	$data='', // POST данные. Данные ожидаются в переменной $data['body'] 	$api_version='3', // версия API, к которй будет совершаться запрос 	$sla=1, // уровень SLA. Грубо - приоритет. Чем выше, тем приоритетнее. Уже сейчас можно использовать. Если при авторизации не возвращен - принять за единицу, в ином случае использовать приоритет, возвращенный при авторизации. 	$uid=0, // идентификатор пользователя по базе API (AAA). 0 при авторизации, далее использовать возвращенный UID 	$srv_name='AUTHENTICATE', // сервис, к которому происходит обращение со всеми параметрами этого сервиса 	$client_secret // Секретный ключик клиента, сгенерированный на этапе авторизации. Передается серверу один раз, далее используется API для проверки подленност запроса. 	) { 	$url_template = $scheme . '://' . $domain . '/' . $validation_string_prefix . '/__NGINX_SIGN__/__QUERY_STRING__'; 	$request_string = 'v' . $api_version . '-sla' . $sla . '-uid' . $uid . '-sign__SOMESIGN__-req' . $srv_name; 	$request_string = str_replace ('__SOMESIGN__', $client_secret, $request_string); 	$request_string .= '=ts:' . time(); 	echo "request_string before md5: " . $request_string . PHP_EOL; 	if (isset($data['body']) === true) { 		$request_string .= '=postmd5:' . md5($data['body']); 	} 	$request_string = str_replace($client_secret, md5($request_string), $request_string); 	$url = str_replace ('__QUERY_STRING__', $request_string, $url_template); 	$request_string .= $validation_secret; 	$query_sign = md5($request_string); 	$url = str_replace ('__NGINX_SIGN__', $query_sign, $url); 	return $url; } 

Наконец, добрались до авторизации — получаем токен и свой UID. Эту операцию можно сделать единожды за все-все время существования учетной записи, если нет нужды менять секрет/токен.

$url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', $data, '3', 1, 0, 'AUTHENTICATE', $client_secret); $options = array(     'http' => array(         'header'  => "Content-type: application/x-www-form-urlencoded\r\n",         'method'  => 'POST',         'content' => http_build_query($data),     ), ); $context  = stream_context_create($options); $result = file_get_contents($url, false, $context); print_r ($result); /* secret: 50c927316191f8678cec3b5247d1b34f request_string before md5: v3-sla1-uid0-sign50c927316191f8678cec3b5247d1b34f-reqAUTHENTICATE=ts:1461624725 {"response":{"code":"200","data":{"uid":"36","sla":"1"},"msg":"authenticated"}} */ 

Далее учетные данные не используются — только $client_secret для подписания. Кстати, сразу отвечу на вопрос — да, можно в параметрах функции указать https для взаимодействия с использованием SSL, если запросы редкие и нет цели экономит ресурсы (или если они расходуются на стороне браузера клиента. Но понятно, что мы не отдаем клиенту функцию по авторизации, а вот запросы с токеном клиент может сам от себя выполнять по SSL при необходимости.).

Итак, авторизовались. Теперь присваиваем себе правильные UID и SLA, выполняем тестовый запрос.

$uid = $result['response']['data']['uid']; $sla = $result['response']['data']['sla'];  echo PHP_EOL . PHP_EOL . "TEST SERVICE:" . PHP_EOL;  $url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', '', '3', $sla, $uid, 'TEST', $client_secret); $options = array(     'http' => array(         'header'  => "Content-type: application/x-www-form-urlencoded\r\n",         'method'  => 'GET'     ), ); $context  = stream_context_create($options); $result = file_get_contents($url, false, $context); print_r ($result); /* TEST SERVICE: request_string before md5: v3-sla1-uid36-sign50c927316191f8678cec3b5247d1b34f-reqTEST=ts:1461624725 {"response":{"code":"200","data":{"id":"21967","sla":"1","uid":"36","sign":"9cf6cba26113c52abea3af928f6232b6","srv":"test","req_head":"TEST=ts:1461624725","req_body":"","api_version":"3"},"msg":"ok"}} */ 

Ага, «ок»!

Извлекаем пользу

По аналогии обратимся к более полезному сервису LOOKUP_BY_NUMBER, для которого потребуются дополнительные параметры:
number - Номер телефона в международном формате (со знаком "+")
source - выбор источника (all - передавать по умолчанию),

и могут быть установлены дополнительные поля при необходимости (в мобильном приложении, например, для идентификации самих устройств и правильного формирования ответа)
v - версия клиента
did - идентификатор устройства
loc - локализация
format - формат возвращаемых значений

echo PHP_EOL . PHP_EOL . "LOOKUP_BY_NUMBER SERVICE:" . PHP_EOL;  $url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', '', '3', $sla, $uid, 'LOOKUP_BY_NUMBER'.'=number:+79671286464=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all', $client_secret); $options = array(     'http' => array(         'header'  => "Content-type: application/x-www-form-urlencoded\r\n",         'method'  => 'GET'     ), ); $context  = stream_context_create($options); $result = file_get_contents($url, false, $context); print_r ($result); /* LOOKUP_BY_NUMBER SERVICE: request_string before md5: v3-sla1-uid36-sign0e70a22605e5a178b572f0bfe426d210-reqLOOKUP_BY_NUMBER=number:+79671286464=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all=ts:1461625693 {"response":{"code":200,"data":{"input":{"number":{"original":"+79671286464","parsed":"79671286464"}},"route":{"default":{"ServiceProvider_id":"3425721","ServiceProvider_name":"ОАО \"Вымпел-Коммуникации\"","ServiceProvider_region":"г. Москва и Московская область"},"mnp":{"ServiceProvider_id":2,"ServiceProvider_name":"\"Мобильные ТелеСистемы\" ПАО","ServiceProvider_region":"г. Москва и Московская область"}},"active_route":{"type":"mnp"}},"msg":""}} */ 
Возвращаемые значения

Результатом выполнения запроса будет являться таблица маршрутизации в порядке приоритета. Первая запись источника — активная. Остальные — просто есть в базе, но скорее всего устаревшие. Вывод зависит от соответствующей настройки «format».
ВАЖНО: ID Сервис провайдера возвращаются внутренние для каждого источника данных! Можно безопасно консолидировать по ID внутри одного источника.

Пример вывода:

{   "response": {     "code": 200,     "data": {       "input": {         "number": {           "original": "+79781234567",           "parsed": "79781234567"         }       },       "route": {         "default": {           "ServiceProvider_id": "274959",           "ServiceProvider_name": "Мобильные ТелеСистемы",           "ServiceProvider_region": "Краснодарский край"         }       },       "active_route": "default"     },     "msg": ""   } } 
Методы отправки данных

Можно использовать простейший метод GET, пример:
http://core.api.netresult.ru/s/f62bb062e30b01e7a689c8cdfdf10577/v3-sla1-uid0-signbb52d15783f7ed30df5d0e7a618cc048-reqLOOKUP_BY_NUMBER=number:+79781234567=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all

а можно отправлять значения переменных и в POST-данных с шифрованием по примеру авторизации выше.

Пример работающего на базе API мобильного приложения для Android с минимальным функционалом самого приложения.
image

ссылка на оригинал статьи https://habrahabr.ru/post/282443/