PowerShell и ЖЖ: аутентификация с «cookie», интерфейс «flat»

Feb 10, 2023 22:58

Ранее в этой серии постов:
...
14. PowerShell и ЖЖ: аутентификация challenge-response и регистр букв хеш-суммы, ч.1
15. PowerShell и ЖЖ: аутентификация challenge-response, ч.2 (практика через интерфейс «flat»)
16. PowerShell и ЖЖ: аутентификация challenge-response, ч.3 (практика через интерфейс «XML-RPC»)

Окружение: операционная система «Windows 10», программа-оболочка «PowerShell» версии 7.

Протокол HTTP(S) и сессии

В предыдущих постах я разбирал выполнение аутентификации на веб-сервере ЖЖ («Живого Журнала») двумя видами простейшего (clear) способа (через параметр «password» и через параметр «hpassword») и способом «challenge-response». При разборе этих способов примерно понятно (по контексту), что каждый из них используется при каждом HTTP(S)-запросе от веб-клиента к веб-серверу ЖЖ.

Протокол HTTP(S) изначально является протоколом без сохранения состояния (по-английски «stateless»). Это означает, что при очередном обращении веб-клиента к веб-серверу этот протокол не требует наличия информации из каких-либо предыдущих обращений.

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

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

Что такое «cookie» в программировании

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

Примером такого термина является «cookie». Вбейте это слово в любую англоязычную поисковую систему по картинкам и полу́чите множество фотографий, картинок и рисунков домашнего печенья, которое любят дети. В обычной жизни «cookie» - это «печенье», но в жаргон американских программистов (а далее - и в жаргон российских программистов) это слово пришло от ассоциации с печеньем с предсказанием. Такие печенья содержат внутри записку с каким-нибудь пространным утверждением («предсказанием»), которое получатель печенья применяет к себе. Это известное в США (и в некоторых других странах) развлечение с друзьями за столом.

В программировании термин «cookie» появился задолго до появления веба (веб появился в 1990-х годах). Когда потребовалось реализовать для общения по протоколу HTTP(S) механизм сессии, то взяли похожий механизм, который применялся при общении между программами до появления веба. По-русски «cookie» можно назвать «небольшим фрагментом данных», но чаще в русских текстах прямо используют слово «cookie» (даже использование кальки «ку́ки» встречается нечасто).

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

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

Нам понадобятся те же две вспомогательные функции, которые я использовал в предыдущих постах при общении с веб-сервером ЖЖ по протоколу «flat». Ранее я уже несколько раз описывал их код, но повторю еще раз, чтобы не нужно было переходить ради них в другие посты. Функция «toHashTable» преобразует полученную от сервера ЖЖ в теле HTTP(S)-ответа строку в хеш-таблицу (ассоциативный массив). Функция «getHash» возвращает хеш-сумму заданной строки, вычисленную по заданному алгоритму.

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
}

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()
}

Общий план действий при аутентификации с «cookie»

1. Отправить HTTP(S)-запрос на получение «cookie» (небольшого фрагмента данных) от веб-сервера ЖЖ. При этом следует использовать разовую аутентификацию с помощью либо простейшего (clear) способа аутентификации, либо с помощью способа аутентификации «challenge-response». Получить от веб-сервера ЖЖ HTTP(S)-ответ с «cookie». Это начало сессии;

2. Отправлять веб-серверу ЖЖ HTTP(S)-запросы с нужными параметрами, добавляя в каждый из HTTP(S)-запросов полученный на первом шаге «cookie» (небольшой фрагмент данных) для аутентификации. Это рабочая часть сессии, в течение которой мы выполняем нужную нам работу;

3. Завершение сессии. В принципе, пользователю из своего веб-клиента это делать необязательно. Работу, которая нам была нужна, мы уже сделали на втором шаге. Однако, следует понимать, что выданный веб-сервером ЖЖ на первом шаге «cookie» (небольшой фрагмент данных) имеет довольно большой срок действия (в документации сказано, что возможны «короткий» срок действия в 24 часа и «длинный» срок действия в 30 дней; можно выбрать один из этих двух вариантов при первоначальном запросе «cookie» на первом шаге).

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

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

Я буду использовать для этого действия разовую аутентификацию по способу «challenge-response», которую подробно описал в предыдущих постах (сжато описал в отдельном посте). Для получения «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

В блоке кода выше я сначала отправил веб-серверу ЖЖ HTTP(S)-запрос на запуск удалённой функции «getchallenge». Напомню, для запуска этой удалённой функции аутентификация не требуется. В результате я получил от веб-сервера ЖЖ в теле HTTP(S)-ответа значение «challenge», которое я использовал во втором HTTP(S)-запросе для аутентификации по способу «challenge-response». Во втором HTTP(S)-ответе от веб-сервера ЖЖ я получил параметр «ljsession», в котором содержится значение-«cookie».

В конце блока кода выше я вывел значение-«cookie» в окно программы-оболочки. На этом примере можно познакомиться, как выглядит значение-«cookie», возвращаемое веб-сервером ЖЖ (функция «sessiongenerate» каждый раз генерирует разные значения, у них только один и тот же формат). Естественно, «светить» это значение в интернете до завершения сессии (при котором это значение станет непригодным для использования) не следует, это небезопасно. Как можно видеть, значение-«cookie» немного отличается по формату от значения «challenge», но принципиально эти значения друг от друга не отличаются: оба формируются веб-сервером ЖЖ, оба имеют ограниченный срок действия, оба являются небольшим фрагментом данных.

В приведенном выше значении-«cookie» следует обратить внимание на кусочек «s291», в блоке кода выше я выделил его красным цветом. Буква «s» здесь означает слово «session», а число 291 - это идентификатор сессии. Идентификатор сессии понадобится нам на третьем шаге, когда мы будем завершать сессию.

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

Рабочую часть сессии я буду демонстрировать на задаче получения одного определенного поста определенного журнала. На этой же задаче я показывал простейшую (clear) аутентификацию и аутентификацию способом «challenge-response» в предыдущих постах. Для решения этой задачи нам понадобится только один HTTP(S)-запрос веб-серверу ЖЖ, но после вышеизложенного должно быть понятно, что в рабочей части сессии можно отправить сколько угодно HTTP(S)-запросов с использованием аутентификации со значением-«cookie», полученным на первом шаге. HTTP(S)-запросы можно отправлять, пока не закончится срок действия «cookie» (если работа к моменту окончания срока действия «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(S)-запрос с командой запуска удалённой функции «getevents». Во входящих параметрах, помещаемых в тело этого HTTP(S)-запроса я указал способ аутентификации "cookie" в параметре с названием auth_method. Для передачи веб-серверу ЖЖ самого значения-«cookie» используется HTTP-заголовок нашего HTTP(S)-запроса с названием Cookie.

Формат содержимого этого HTTP-заголовка описан в документе «RFC 6265» или описание попроще можно найти в справочнике на сайте «MDN Web Docs». В принципе, там всё несложно: содержимое этого HTTP-заголовка может состоять из пар название-значение, в которых название и значение отделяются символом = (знак «равно»). Таких пар может быть сколько угодно, пары отделяются друг от друга символами ;  (точка с запятой и пробел). В нашем случае в HTTP-заголовок Cookie следует поместить лишь одну пару название-значение с названием ljsession и значением-«cookie», полученным на первом шаге (оно хранится в переменной $ljsession).

Для формирования HTTP-заголовков HTTP(S)-запроса с помощью командлета «Invoke-WebRequest» я использую параметр -Headers этого командлета, а в этот параметр я передаю хеш-таблицу с HTTP-заголовками, созданную ранее в переменной $headers.

Можно заметить, что кроме HTTP-заголовка с названием Cookie я создал еще HTTP-заголовок с названием X-LJ-Auth (это название пришлось взять в кавычки из-за того, что в нем используются символы дефиса, иначе скрипт будет интерпретирован с ошибкой) и значением "cookie". Наличие этого HTTP-заголовка требуется в документации, это дополнительная мера безопасности.

Как видно в блоке кода выше, в результате я успешно получил от веб-сервера ЖЖ данные запрошенного поста (я вывел в окно программы-оболочки только постоянный URL-адрес этого поста, но, конечно, я получил и все остальные параметры с данными поста; всё это я уже подробно описывал в предыдущих постах). Можно убедиться, что в рабочей части сессии для аутентификации достаточно лишь значения-«cookie» и правильной настройки HTTP(S)-запроса. Пользователь лишь вводит пароль на первом шаге, а в рабочей части сессии аутентификация выполняется автоматически веб-клиентом.

3. Завершение сессии, инициированное веб-клиентом

Как я уже писал выше, сессия может быть автоматически завершена веб-сервером ЖЖ в момент окончания срока действия значения-«cookie», полученного на первом шаге. Но мы выполним завершение сессии самостоятельно, со стороны веб-клиента. Это можно сделать одним из двух способов: завершить конкретную сессию, указав для этого идентификатор этой сессии, либо завершить все открытые для указанного пользователя ЖЖ сессии (при этом, понятно, идентификаторы сессий указывать не требуется).

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

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

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

Как видно из блока кода выше, в итоге я получил от веб-сервера ЖЖ HTTP(S)-ответ с параметром «success», содержащим значение «OK». Таким образом веб-сервер ЖЖ сообщил, что завершение сессии с указанным идентификатором выполнено успешно. После этого я попробовал совершить действие из шага 2, на что веб-сервер ЖЖ возвратил HTTP(S)-ответ с сообщением о неправильном пароле. Последнее говорит о том, что значение-«cookie», полученное на первом шаге, было в результате шага 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.1. В итоге у меня всё получилось, веб-сервер ЖЖ успешно закрыл все сессии, открытые для пользователя «vbgtut».

В следующих постах я постараюсь сделать то же самое, но только через интерфейс «XML-RPC».

Инструмент, Образование, Сайтостроение, Программирование, Английский язык, ЖЖ

Previous post Next post
Up