JS-программист

Месяца два назад думал, что мне не хватает вывода информации об оплате интернета на рабочий стол. У меня два провайдера: Онлайм и Нетбайнет. И если первый хотя бы смс-ки шлёт, когда баланс заканчивается, то у второго это — платная услуга. А значит она не подключена 😉 Но смс-информирование тоже слабло полезно, хотя бы потому, что когда сидишь «в запаре» на работе, а к тебе приходит смс с содержанием «послезавтра интернет всё, оплатите пожалуйста», это не только не имеет положительного эффекта, но ещё и сильно раздражает. Помнится, когда был абонентом Корбины, у них была утилита, сидящая в трее и выводящая информацию об балансе. Но только для windows, конечно (пользователи не windows там страдали, пытаясь совладать с их реализацией l2tp, им было не до отображения баланса на рабочем столе совсем). И подумалось мне, что не плохо было бы научить Conky отображать текущий баланс, стоимость за месяц и расчётное время отключения.

JS-программист
Эталонный JS-программист

Короче, изначальное ТЗ выглядело так: заходим wget’ом, тащим страницу, потрошим её regex’ами, PROFIT! И если по адресу my.onlime.ru меня встречала страничка с формой, и, теоретически, это было осуществимо, то на my.netbynet.ru была пустая страница с несколькими обильными скриптами на JavaScript. Конечно, тогда я всю эту затею бросил. Мне что, делать было нечего больше? Но тут в пятницу сдал на сертификацию исходники BIOS’ов для очередных наших ПЭВМ, а пока я над ними работал, столько работы по разбору «чёрных ящиков без вскрытия» проделано было, что было решено, пока мозг не остыл, решить и такую мелкую задачу, как разбор работы этих двух сайтов.

И я должен доложить, что в первоначальной негативной оценке подхода Нетбайнета я был не прав. Конечно плохо, что без поддержки JS’а нынче никак нельзя… Но при разборе работы сайта было замечено, что так они полностью вынесли логику работы интерфейса на сторону клиента, а все итерации с личным кабинетом производятся через формализованные JSON-запросы (да, JS-кодеров я считаю мартышками, а JSON очень люблю за простоту и понятность). Для аутентификации нужно отправить JSON-запрос, получить в ответ «200 OK» и cookie. Потом сделать два запроса с полученной cookie, на текущее состояние счёта и дополнительную информацию (стоимость услуг за месяц и расчётное время отключения в RFC 3339). Выглядит так, как будто бы они всё решили сделать по человечески: написали дельное API, а потом интерфейс под него.

Онлайм и глупые шутки
Кретивщики Онлайма попытались так обыграть отсутствие ночью специалистов, ответственных за чат. Плохой вкус и няшные мемасики — отвратительное сочетание.

А вот Онлайм несёт на себе печать родства с «национальным чемпионом»: после заполнения и отправки формы POST’ом на сервер, нас мало того, что делают «302 REDIRECT», так ещё и ничего дельного не возвращается, кроме кучи скриптов. Авторизация делается для PHP-сессии, номер сессии хранится в cookie. Информацию надо тоже забирать через JSON. Но он не доступен даже с корректным значением номера сессии в cookie. Оказалось, что весь JSON отдаётся, только при корректно выставленных значениях дополнительных полей в http-запросах. Значения эти теряются между JS-мусором в теле ответа после попытки аутентификации (в случае её успешного завершения). Но это ещё ничего. Беда начинается, когда хочешь узнать стоимость услуг в данный момент времени: для этого тебе отправят JSON-портянку, в которой будет указано состояние лицевого счёта, список используемых сейчас услуг и список вообще всех возможных услуг. Ну а тебе надо, учитывая используемые услуги, пройтись по списку всех услуг и посчитать… Хорошо, что у меня только «Домашний интернет» используется!

Но зато теперь понятно, почему их «Личный кабинет» так страшно тормозит: ведь там всё, фактически, что есть — слабо вменяемые парсеры для вот этой вот огромной «портянки» и ещё более монструозный чат на JS. Кстати, запросы JSON тоже ужасно долго проходят. Так что не удивительно, что «Личный кабинет» Онлайма мало того, что безумно задумчив, так ещё и «унижает» соседние вкладки на быстродействие. А весь браузер в такие моменты заставляет греться хтоническое чудо от AMD. Зимой это хорошо, но вой вентиляторов пугает мою шиншиллу и людей в соседнем подъезде.

Скрипт для запроса баланса в Нетбайнете:

#!/usr/bin/perl
###########################################################
# Простой скрипт для опроса баланса из личного кабинета
# интернет-провайдера Netbybet (нынче WIFIRE).
# Написан Павлом Алиевым (aliech@aliech.ru).
# Powered by perl!
###########################################################

use strict;
use warnings;
use utf8;

use HTTP::Request;
use HTTP::Cookies;
use LWP::UserAgent;
use JSON::PP;
use DateTime::Format::RFC3339;
use DateTime::TimeZone;


# Данные для аутентфиикации, нужно подправить под себя!
my %auth_data = (
	'accountNumber'	=> "",	# номер счёта
	'password'	=> "",	# пароль
	'captchaCode'	=> ""		# они хотели капчу прикрутить?!
);


# Ну мы будем что-то выводить в utf8 же
binmode(STDOUT, ":utf8");

# Создаём объекты и общие структуры данных
my $cookies = HTTP::Cookies->new();
my $lwp = LWP::UserAgent->new();
$lwp->cookie_jar($cookies);

my $request;
my $response;

# Запрос аутентифкации
$request = HTTP::Request->new('POST', "https://my.netbynet.ru/api/v2/login");
$request->header('Content-Type' => "application/json");
$request->content(encode_json(\%auth_data));
$response = $lwp->request($request);
die("Ошибка запроса аутентификации: " . $response->status_line() . "\n") unless($response->is_success());

# Запрашиваем текущий баланс
$request = HTTP::Request->new('GET', "https://my.netbynet.ru/api/v1/get-balance");
$response = $lwp->request($request);
die("Ошибка запроса текущего баланса: " . $response->status_line() . "\n") unless($response->is_success());
my $current_balance = decode_json($response->content());

# Запрашиваем детали по балансу
$request = HTTP::Request->new('GET', "https://my.netbynet.ru/api/v1/get-balance-details");
$response = $lwp->request($request);
die("Ошибка запроса деталей по балансу: " . $response->status_line() . "\n") unless($response->is_success());
my $balance_details = decode_json($response->content());

# Так как время нам отдали в чём-то типа RFC 3339, в какой-то там временной зоне (UTC?),
# его, время то есть, надо перевести в что-то человекопонятное.
my $rfc3339 = DateTime::Format::RFC3339->new();
my $tz_obj = DateTime::TimeZone->new(name => 'local');
my $time_obj = $rfc3339->parse_datetime($balance_details->{'nextPaymentDate'});
$time_obj->set_time_zone($tz_obj->name());	# меняем пояс

# Вывод информации
printf(
	"Состояние: %s, %sр / %sр (до %s %s)\n",
	$current_balance->{'statusName'},
	$current_balance->{'accountBalance'},
	$balance_details->{'subscriptionFee'},
	$time_obj->dmy('.'),
	$time_obj->hms()
);
 
exit(0);

Пример работы:

aliech@arcturus:~$ ./netbynet_accounts.pl 
Состояние: Активен, 66.2р / 399р (до 23.03.2018 03:00:00)
aliech@arcturus:~$ 

Скрипт для запроса баланса в Онлайме:

#!/usr/bin/perl
###########################################################
# Простой скрипт для опроса баланса из личного кабинета
# интернет-провайдера Onlime.
# Написан Павлом Алиевым (aliech@aliech.ru).
# Powered by perl!
###########################################################

use strict;
use warnings;
use utf8;

use HTTP::Request;
use HTTP::Request::Common;
use HTTP::Cookies;
use LWP::UserAgent;
use JSON::PP;


# Данные для аутентфиикации, нужно подправить под себя!
my %auth_data = (
	'login_credentials[login]'	=> "",	# номер счёта
	'login_credentials[password]'	=> "",	# пароль
);


# Ну мы будем что-то выводить в utf8 же
binmode(STDOUT, ":utf8");

# Создаём объекты и общие структуры данных
my $cookies = HTTP::Cookies->new();
my $lwp = LWP::UserAgent->new();
push(@{$lwp->requests_redirectable}, 'POST');	# после отправки POST'а, они нас перенаправляют! ЗАЧЕМ?!
$lwp->cookie_jar($cookies);

my $request;
my $response;

# Запрос аутентифкации
$request = HTTP::Request::Common::POST("https://my.onlime.ru/session/login", \%auth_data);
$request->header('Referer' => "https://my.onlime.ru/");
$response = $lwp->request($request);
die("Ошибка запроса аутентификации: " . $response->status_line() . "\n") unless($response->is_success());

# Так, короче, чтобы ниже ходить в JSON, нужны значения, присвоенные во время авторизации.
# Но отдали они нам не в куках, а в теле страницы, которая прилетела после перенаправления.
# И нам эту хрень надо регулярным выражением забрать из тела ответа. Бред.
# Кстати, его потом придётся в заголовки подставлять. Как там в анекдоте? "П#$расы, сэр!"
my $json_secret;
if($response->decoded_content() =~ /var\swtf\s=\s'([0-9a-z]+)',/) {
	$json_secret = $1;
}
else {
	die("Не удалось вычленить важную информацию из тела ответа на запрос аутентификации!\n");
}

# Запрос /json/cabinet/ (ну я хз, как это назвать)
$request = HTTP::Request->new('GET', "https://my.onlime.ru/json/cabinet/");
$request->header('X-Requested-With' => "XMLHttpRequest");
$request->header('X-Wtf' => $json_secret);
$request->header('X-Request' => "JSON");
$request->header('Referer' => "https://my.onlime.ru/");
$response = $lwp->request($request);
die("Ошибка запроса /json/cabinet/: " . $response->status_line() . "\n") unless($response->is_success());
my $cabinet = decode_json($response->content());

# Запрос /json/getcontract/ - страшный список всех тарифов и того, что сейчас используется.
$request = HTTP::Request->new('GET', "https://my.onlime.ru/json/getcontract/");
$request->header('X-Requested-With' => "XMLHttpRequest");
$request->header('X-Wtf' => $json_secret);
$request->header('X-Request' => "JSON");
$request->header('Referer' => "https://my.onlime.ru/services/");
$response = $lwp->request($request);
die("Ошибка запроса /json/getcontract/: " . $response->status_line() . "\n") unless($response->is_success());
my $contract = decode_json($response->content());

# Так как они не отдают стоимость услуг за месяц, выдавая полный список доступных,
# придётся всё это обойти и посчитать. Я использую только "Домашний интернет",
# так что обход всего остального сами допишите, если что. Я всё объяснил)
my $month_paid = 0;

my $current_plan = $contract->{'configuration'}->{'internet'}->{'rateplan'}->{'code'};
die("Не удалось получить имя используемого тарифного плана!\n") unless(defined($current_plan));

foreach my $plan (@{$contract->{'catalogue'}->{'rateplans'}->{'INET'}}) {
	if($plan->{'code'} eq $current_plan) {
		$month_paid = $plan->{'price'};
	}
}

# Вывод информации
printf(
	"Состояние: %s, %sр / %sр (дней до блокировки: %s)\n",
	$cabinet->{'bstatus'},
	$cabinet->{'balance'},
	$month_paid,
	$cabinet->{'lock'}
);	

exit(0);

Пример работы:

aliech@arcturus:~$ ./onlime_accounts.pl 
Состояние: Active, 52.54р / 520р (дней до блокировки: 4)
aliech@arcturus:~$ 

PS: пока я пытался разобраться с работой ЛК Онлайма, меня допекал их чатик. Как на странице ЛК он навязчиво лез на глаза, так и в мониторинге сетевых запросов угаживал собой список оных. В какой-то момент времени я написал вопрос, что-то типа «Хочу написать скрипт для запроса баланса из ЛК. Есть ли у вас API для этого?», на что получил ответ «Не можем предоставить данную информацию. Может быть вас интересует ещё что-нибудь?». Через пять минут у меня уже получилось найти, где передаются те самые вожделенные параметры, которые надо подставить в хидеры запроса к JSON’у, и я написал ответ в чат: «Спасибо, я разобрался, как получать JSON из ЛК с информацией о текущем состоянии баланса.» И знаете что? Мне прислали «Рады, что смогли вам помочь! Оцените пожалуйста качество обслуживания:». Мата не хватает…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

4 × 4 =