Я изучаю сетевое взаимодействие по протоколу HTTP(S). Мне было интересно попробовать связаться с «Живым Журналом» (он же «LiveJournal», он же «LJ», он же «ЖЖ») из программы‑оболочки «PowerShell» версии 7 (я работаю в операционной системе «Windows 10») и получить от этого веб‑сервиса какие‑нибудь данные.

Протокол взаимодействия с сервером «Живого Журнала»

У «Живого Журнала» есть документация, которая писалась в период 1999–2008 годов (так там сказано). В этой документации есть раздел, посвященный протоколу, с помощью которого можно обратиться к программе‑серверу «Живого Журнала» из своей программы‑клиента. Несмотря на то, что документация старая, она до сих пор актуальна.

Общение с программой‑сервером «Живого Журнала» может происходить через ряд интерфейсов, при этом URI‑адрес входа формируется так:

https://www.livejournal.com/interface/название-интерфейса

Например:

https://www.livejournal.com/interface/flat
https://www.livejournal.com/interface/xmlrpc
https://www.livejournal.com/interface/blogger
https://www.livejournal.com/interface/atom

В документации сказано, что хоть интерфейсы предлагаются разные, в итоге при использовании любого из этих интерфейсов вы вызываете одну и ту же реализацию удалённых процедур. Поэтому, вроде как, можно выбирать работу через любой из предлагаемых интерфейсов, исходя из того, с каким из них вам удобнее будет работать, полагаясь на инструменты, предлагаемые вашим языком программирования и/или платформой. Я подробно изучил работу через интерфейсы «flat» и «XML‑RPC», но в этой статье в примерах буду использовать работу через интерфейс «flat».

Название интерфейса «flat» переводится на русский язык как «плоский». Имеется в виду, что передаваемые программе‑серверу «Живого Журнала» и получаемые в ответ структуры данных имеют «плоское» строение, то есть без вложенностей. При работе через интерфейс «XML‑RPC» структуры данных, наоборот, могут иметь вложенности, то есть, к примеру, хеш‑таблица (ассоциативный массив) может иметь значение в виде массива, который, в свою очередь, может иметь элемент в виде хеш‑таблицы, и так далее. Вложенности создают «глубину» в структуре данных в отличие от данных, передаваемых по «плоскому» интерфейсу «flat».

Работа через интерфейс «flat», командлет «Invoke-WebRequest»

При работе через интерфейс «flat» входные параметры для вызова удалённой процедуры предлагается помещать в тело HTTP(S)‑запроса с методом запроса «POST» в следующем формате:

параметр1=значение1&параметр2=значение2&параметрN=значениеN...

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

Я для формирования и отправки HTTP(S)‑запросов из программы‑оболочки «PowerShell» использую командлет «Invoke‑WebRequest». У него есть недостаток: при использовании этого командлета мне не удалось как‑то получить и рассмотреть до отправки формируемый этим командлетом HTTP(S)‑запрос. В интернетах советуют для этого использовать более гибкие в этом плане классы платформы «.NET» вроде «System.Net.HttpWebRequest» или «System.Net.Http.HttpClient». Но я просто получаю отправленный командлетом «Invoke‑WebRequest» HTTP(S)‑запрос на установленный у меня локальный веб‑сервер, собираю его небольшим скриптом на языке PHP и сохраняю в текстовый файл. Таким образом, в итоге я всё‑таки могу подробно рассмотреть, что там формирует и отправляет командлет «Invoke‑WebRequest».

Для отправки входных параметров для вызова удалённой процедуры с помощью командлета «Invoke‑WebRequest» в описанном выше формате достаточно поместить нужные входные параметры в хеш‑таблицу, а полученную хеш‑таблицу — передать в параметр -Body командлета «Invoke‑WebRequest». В этом случае процентное кодирование командлет выполнит сам, если это будет необходимо.

В отличие от HTTP(S)‑запроса HTTP(S)‑ответ, возвращаемый командлетом «Invoke‑WebRequest», мы можем рассмотреть во всех подробностях. При работе через интерфейс «flat» возвращаемые удалённой процедурой данные передаются в теле HTTP(S)‑ответа в следующем формате:

параметр1
значение1
параметр2
значение2
параметрN
значениеN
...

Значения некоторых возвращаемых параметров могут быть дополнительно закодированы. Например, для дополнительного кодирования текстов постов при их получении через интерфейс «flat» используется процентная кодировка (для раскодирования я использовал метод «UrlDecode» класса «System.Web.HttpUtility» платформы «.NET»), а при их получении через интерфейс «XML‑RPC» используется кодирование «Base64» (для раскодирования я использовал возможности классов «System.Text.Encoding» и «System.Convert» платформы «.NET»). В этой статье я не буду демонстрировать получение текста поста (это выходит за рамки заявленной темы), получу только постоянный URL‑адрес поста.

Подготовка вспомогательных функций

Для извлечения возвращаемых параметров из многострочной строки («multiline string»; это строка, которая содержит символы новой строки) описанного выше формата в хеш‑таблицу я написал следующую функцию:

function toHashTable($str) {
  $arr = $str -split '\r?\n'
  $hash = @{}
  for ($i = 0; $i -lt $arr.Length; $i += 2) {
    $hash[$arr[$i]] = $arr[$i + 1]
  }
  return $hash
}

Командлет «ConvertFrom‑StringData» для этого использовать не получилось, так как этот командлет не может работать с разделителем ключа и значения в паре ключ‑значение в виде символа новой строки.

Нам понадобится вычислять хеш‑сумму для заданной строки по указанному алгоритму. Я взял за основу код из четвертого примера в документации командлета «Get‑FileHash», оформил его в функцию и немного подкорректировал. Вот что у меня получилось:

function getHash($str, $alg) {
  $stringAsStream = [System.IO.MemoryStream]::new()
  $writer = [System.IO.StreamWriter]::new($stringAsStream)
  $writer.write($str)
  $writer.Flush()
  $stringAsStream.Position = 0
  (Get-FileHash -InputStream $stringAsStream -Algorithm $alg).Hash.ToLower()
}

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

Кстати, обратите внимание на применение функции ToLower в последней строке кода в блоке кода выше. Обычно регистр букв (шестнадцатеричных цифр) в хеш‑сумме, обозначающих полубайты хеш‑суммы, не имеет значения, но в преобразовании, применяемом при аутентификации вида «challenge‑response» регистр этих букв имеет значение, так как там на одном из этапов хеш‑сумма интерпретируется не как двоичное число, а как строка символов. А поскольку сервер «Живого Журнала», как я понимаю, работает на какой‑то Unix‑подобной операционной системе (в них хеш‑сумму принято представлять с буквами в нижнем регистре), то для того, чтобы аутентификация вида «challenge‑response» прошла без ошибки, я тоже привожу буквы в хеш‑сумме в нижний регистр. (В моей операционной системе «Windows 10» по умолчанию хеш‑сумма формируется с буквами в верхнем регистре.)

Отношение администрации «Живого Журнала» к скриптам, ботам и так далее

В пользовательском соглашении «Живого Журнала» есть пункт 9.2.6, который запрещает использовать автоматические скрипты для взаимодействия с этим веб‑сервисом без разрешения администрации. Однако, когда я обратился в службу поддержки за таким разрешением, мне там разъяснили, что этот пункт пользовательского соглашения направлен на защиту от повышенных нагрузок на серверы «Живого Журнала». Если робот не нарушает «Правил LiveJournal для роботов», то его использование не запрещено.

Итак, как сказано по указанной в предыдущем абзаце ссылке, робот не должен «тупить», часто совершая повторяющиеся запросы на один и тот же ресурс (URL), следует кэшировать результаты запросов бота (как это делают, к примеру, браузеры). Также там просят не делать бота многопоточным и не устанавливать более 5 соединений в секунду. Если бот будет слишком грузить серверы «Живого Журнала», он может быть заблокирован.

Вдобавок к вышеизложенному отмечу, что скрипт может быть заблокирован, если он будет передавать множество HTTP(S)‑запросов с неправильным паролем. Такое поведение может быть истолковано как попытка взлома учетной записи методом перебора.

Вызов удаленной процедуры без аутентификации

Как минимум одну удалённую процедуру (функцию) программы‑сервера «Живого Журнала» мы можем вызвать без всякой аутентификации. Эта функция называется «getchallenge». Вообще она используется в рамках аутентификации способом «challenge‑response», но ее можно безболезненно использовать для первых экспериментов‑обращений к программе‑серверу «Живого Журнала».

$body = @{
  mode = "getchallenge"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$params["challenge"]
c0:1674766800:1054:60:ZmbOcwbxmdswLmKngEVl:3a50482295a65607685badc39b09d47b

Как можно видеть из кода выше, значение‑отклик (challenge), возвращаемое удалённой функцией «getchallenge», представляет собой некое значение (длинную последовательность символов). При каждом обращении к этой функции возвращается разное значение‑отклик. Каждое значение‑отклик имеет срок действия в 60 секунд.

1.1. Аутентификация с открытой (clear) передачей пароля

При такой аутентификации пароль передается при каждом вызове удалённой функции. Пароль помещается в параметр с именем «password». При этом пароль никак не скрывается и не шифруется на уровне веб‑клиента, поэтому аутентификацию таким способом называют словом «clear» (в данном случае по‑русски это слово означает «явный», «прозрачный»).

В документации отмечено, что аутентификация этим способом не одобряется, так как считается небезопасной, но остается возможной. С другой стороны, в наше время повсюду (в том числе на веб‑сервисе «Живой Журнал») используется шифрование текста HTTP‑запроса, что отражается в появлении аббревиатуры HTTPS в URL‑адресах. Так что злоумышленникам, по идее, придется сначала как‑то взломать этот шифрованный обмен между веб‑сервером и веб‑клиентом, прежде чем они доберутся до пароля. Но я, конечно, всё‑таки не собираюсь использовать этот способ аутентификации, раз он не рекомендуется в документации.

Демонстрировать способы аутентификации я буду на примере задачи получения одного поста из журнала (блога) пользователя с именем (логином) «vbgtut». Вместо настоящего пароля в примерах я буду указывать слово «пароль». Для получения поста я буду вызывать удалённую функцию «getevents».

$body = @{
  mode = "getevents"
  user = "vbgtut"
  password = "пароль"
  selecttype = "one"
  itemid = "148"
  ver = "1"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$params["events_1_url"]
https://vbgtut.livejournal.com/37952.html

В хеш-таблице $params содержится много параметров, я для краткости демонстрирую лишь один из них, постоянный URL-адрес поста. Поскольку URL-адрес поста получен, значит аутентификация (процедура проверки подлинности пользователя) была выполнена с положительным результатом.

1.2. Аутентификация с открытой (clear) передачей хеш-суммы пароля

Вместо передачи пароля через параметр «password» можно передать хеш‑сумму пароля, вычисленную по алгоритму «MD5», через параметр «hpassword». Так как хеш‑сумма пароля никак не скрывается и не шифруется, то этот способ аутентификации считается подвидом способа аутентификации «clear».

Для такого способа аутентификации характерны все те же самые недостатки, которые были описаны для способа аутентификации с открытой передачей пароля. При передаче хеш‑суммы пароля злоумышленник, завладев хеш‑суммой пароля, конечно, не сможет получить сам пароль, но хеш‑суммы пароля уже достаточно для запуска тех же самых удалённых функций, что и с наличием самого пароля. В документации такое использование злоумышленником хеш‑суммы пароля названо термином «replay attack» («атака повторного воспроизведения»).

Этот способ аутентификации тоже не одобряется в документации, но остается возможным.

getHash "пароль" "MD5"
e242f36f4f95f12966da8fa2efd59992
$body = @{
  mode = "getevents"
  user = "vbgtut"
  hpassword = "e242f36f4f95f12966da8fa2efd59992"
  selecttype = "one"
  itemid = "148"
  ver = "1"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$params["events_1_url"]
https://vbgtut.livejournal.com/37952.html

2. Аутентификация способом «challenge-response»

Этот способ аутентификации, как и способ «clear», выполняется с каждым вызовом удалённой функции. Но способ аутентификации «challenge‑response» считается достаточно безопасным для использования. При этом способе аутентификации вместо параметра с именем «password» (или параметра с именем «hpassword») требуется указать три параметра: «auth_method» (способ аутентификации) со значением "challenge", «auth_challenge» со значением‑откликом от программы‑сервера и «auth_response» со значением, вычисленным по особой формуле на базе значения‑отклика и хеш‑суммы пароля.

При этом способе аутентификации по сети не передается ни пароль, ни даже хеш‑сумма пароля. Если даже злоумышленник перехватит HTTP(S)‑запрос, он не сможет их получить. «Атака повторного воспроизведения», упомянутая ранее, думаю, будет ограничена сроком действия отклика, то есть 60 секундами.

При этом способе работа состоит не из одного HTTP(S)‑запроса, как раньше, а из двух: первым HTTP(S)‑запросом получаем отклик (challenge) от сервера, вторым HTTP(S)‑запросом запускаем на выполнение нужную нам удалённую процедуру.

$body = @{
  mode = "getchallenge"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$hPass = getHash "пароль" "MD5"
$hResp = getHash ($params["challenge"] + $hPass) "MD5"
$body = @{
  mode = "getevents"
  user = "vbgtut"
  auth_method = "challenge"
  auth_challenge = $params["challenge"]
  auth_response = $hResp
  selecttype = "one"
  itemid = "148"
  ver = "1"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$params["events_1_url"]
https://vbgtut.livejournal.com/37952.html

3. Аутентификация с «cookie», работа с сессией

«Cookie» по‑русски означает «печенье». Имеются в виду печенья с запеченными в них бумажками с «предсказаниями»; это кондитерское изделие, распространенное в США и в некоторых других странах. В вебе «cookie» — это значения, генерируемые веб‑сервером, вроде описанного в этой статье ранее отклика (challenge). Значения‑«cookie» помещаются в специальный HTTP‑заголовок HTTP(S)‑запросов, как бумажки с «предсказаниями» запекаются в печеньях.

«Cookie» используются для разных задач, в нашем случае «cookie» используется для организации работы в рамках сессии (сеанса). Вообще протокол HTTP изначально спроектирован как протокол без сохранения состояния (stateless). Но людям понадобилось организовать работу в рамках «сессии»: при первом HTTP(S)‑запросе веб‑сервер генерирует и возвращает значение‑«cookie» (каждый раз разное), означающее начало сессии; далее работа идет в рамках сессии, при этом аутентификация выполняется с помощью значения‑«cookie», полученного на первом шаге; в конце концов происходит окончание сессии, инициируемое либо веб‑сервером, либо пользователем из его веб‑клиента.

Значение‑«cookie» имеет ограниченный срок действия, по истечении которого веб‑сервер делает данное значение‑«cookie» непригодным для использования, таким образом завершая сессию. Программа‑сервер «Живого Журнала», согласно документации, может создать либо значение‑«cookie» с коротким сроком действия (24 часа), либо значение‑«cookie» с длинным сроком действия (30 суток). Срок действия может быть задан при HTTP(S)‑запросе на генерацию нового значения‑«cookie» программой‑сервером.

3.1. Получение «cookie» от веб-сервера (начало сессии)

Я использую для получения значения‑«cookie» от программы‑сервера разовую аутентификацию способом «challenge‑response», описанную ранее, так как до получения значения‑«cookie» мы еще не можем использовать аутентификацию способом «cookie». Для получения «cookie» следует запустить удалённую функцию «sessiongenerate» программы‑сервера «Живого Журнала».

$body = @{
  mode = "getchallenge"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$hPass = getHash "пароль" "MD5"
$hResp = getHash ($params["challenge"] + $hPass) "MD5"
$body = @{
  mode = "sessiongenerate"
  user = "vbgtut"
  auth_method = "challenge"
  auth_challenge = $params["challenge"]
  auth_response = $hResp
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST"
$params = toHashTable($Response.Content)
$ljsession = $params["ljsession"]
$ljsession
v2:u31363138:s291:aaIUcD3Vz5V:gc02cecf1bbfdc14b50d05a5f4a70372500e85433//1

Из блока кода выше можно видеть, что я успешно получил в переменную $ljsession значение — «cookie». Можно видеть, что по формату это значение несколько отличается от описанного ранее отклика (challenge) программы‑сервера. Тут следует обратить внимание на то, что значение‑«cookie» разбито на кусочки символом двоеточия :. В третьем кусочке содержится значение‑строка s291. Здесь буква s означает слово «сессия», а число 291 — это идентификатор (номер) сессии. Этот идентификатор сессии понадобится нам позже, когда мы будем инициировать завершение данной сессии.

3.2. Рабочая часть сессии

Пока срок действия полученного ранее значения‑«cookie» не закончился, или пока мы не инициировали завершение сессии, эта сессия является актуальной. В рамках сессии мы можем вызвать любую удалённую процедуру программы‑сервера «Живого Журнала», используя для аутентификации полученное ранее значение‑«cookie». При этом следует использовать параметр с названием «auth_method» и значением "cookie", а значение‑«cookie» следует поместить в специальный HTTP‑заголовок HTTP(S)‑запроса.

Моя демонстрационная рабочая часть сессии будет состоять только из одного вызова удалённой процедуры, как и ранее, для получения одного поста определенного журнала (блога). Но должно быть понятно, что в рабочей части сессии может быть сколько угодно HTTP(S)‑запросов с использованием аутентификации способом «cookie».

$body = @{
  mode = "getevents"
  user = "vbgtut"
  auth_method = "cookie"
  selecttype = "one"
  itemid = "148"
  ver = "1"
}
$headers = @{
  "X-LJ-Auth" = "cookie"
  Cookie = "ljsession=$ljsession"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST" -Headers $headers
$params = toHashTable($Response.Content)
$params["events_1_url"]
https://vbgtut.livejournal.com/37952.html

Как видно в блоке кода выше, HTTP‑заголовки можно задавать в отдельной хеш‑таблице, которую потом можно передать в параметр -Headers командлета «Invoke‑WebRequest». Отмечу, что я создал тут дополнительный HTTP‑заголовок с названием "X-LJ-Auth" (кавычки нужны, так как в названии содержатся символы дефиса), это требование из документации (дополнительная мера безопасности). Формат содержимого HTTP‑заголовка с названием Cookie описан в документе «RFC 6265», но для нашего случая этот формат, думаю, не требует пояснений.

3.3.1. Завершение сессии, инициированное веб-клиентом, по идентификатору сессии

Так как значение-«cookie» еще действует, то используем это значение для аутентификации в последний раз. Для завершения сессии требуется вызвать удалённую функцию «Живого Журнала» «sessionexpire».

$res = $ljsession -match '^.+:.+:.(.+):.+:.+$'
$body = @{
  mode = "sessionexpire"
  user = "vbgtut"
  auth_method = "cookie"
  "expire_id_$($Matches[1])" = "true"
}
$headers = @{
  "X-LJ-Auth" = "cookie"
  Cookie = "ljsession=$ljsession"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST" -Headers $headers
$params = toHashTable($Response.Content)
$params["success"]
OK

Так как для завершения сессии по ее идентификатору требуется сначала получить этот идентификатор, то я извлекаю его из содержимого переменной $ljsession с помощью оператора -match и регулярного выражения. Извлечение идентификатора сессии выполняется с помощью захвата подстроки (круглые скобки внутри регулярного выражения). Захваченные подстроки автоматически сохраняются во встроенной переменной $Matches. Таким образом, выражение $Matches[1] содержит первую захваченную в регулярном выражении подстроку, которая является нашим идентификатором сессии. После выделения идентификатора сессии я помещаю его в название входного параметра expire_id_291. Значением этого параметра является значение "true".

Как видно из блока кода выше, в итоге я получил от веб‑сервера «Живого Журнала» HTTP(S)‑ответ с параметром «success», содержащим значение «OK». Таким образом веб‑сервер ЖЖ сообщил, что завершение сессии с указанным идентификатором выполнено успешно. После этого я попробовал совершить действие из шага 3.2 (рабочая часть сессии), на что веб‑сервер ЖЖ возвратил HTTP(S)‑ответ с сообщением о неправильном пароле. Последнее говорит о том, что значение‑«cookie», полученное на шаге 3.1, было в результате шага 3.3.1 приведено в негодность, чего мы и добивались, завершая сессию.

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

Для этого действия тоже следует вызвать удалённую функцию ЖЖ «sessionexpire», только во входных параметрах нашего HTTP(S)‑запроса следует указать не параметр с названием вида expire_id_291, а параметр с названием expireall и значением "true". Указывать идентификаторы сессий не требуется, и это упрощает дело.

$body = @{
  mode = "sessionexpire"
  user = "vbgtut"
  auth_method = "cookie"
  expireall = "true"
}
$headers = @{
  "X-LJ-Auth" = "cookie"
  Cookie = "ljsession=$ljsession"
}
$Response = Invoke-WebRequest -URI "https://www.livejournal.com/interface/flat" `
            -Body $body -Method "POST" -Headers $headers
$params = toHashTable($Response.Content)
$params["success"]
OK

Я испробовал этот способ завершения сессии на другом значении‑«cookie», не том, на котором пробовал способ из пункта 3.3.1. В итоге у меня всё получилось, веб‑сервер «Живого Журнала» успешно закрыл все сессии, открытые для пользователя «vbgtut».

Полезная ссылка

Статья «Основы работы с сервером livejournal.com» Евгения Ильина от 14 октября 2007 года.