|
ВведениеЭта статья рассказывает про основы взаимодействия с сервером livejournal.com, также известный как Живой Журнал. Большая часть статьи представляет собой описание методов авторизации разными способами, а в конце приводится пример того, как программно отправлять сообщения в блог. Для статьи была написана тестовая программа LJServerTest, которая производит авторизацию различными способами и может отправлять тестовую строку в блог. Программа была написана на языке C# под платформу .NET Framework 2.0. Все дальнейшие примеры также демонстрируются участками кода на C#. Исходники программы можно скачать отсюда. Общие сведенияВся документация по работе с сервером Живого Журнала (далее ЖЖ) располагается по адресу http://www.livejournal.com/doc/server/, но для наших целей достаточно ограничиться разделом Client/Server Protocol, в котором и описываются принципы взаимодействия с сервером. Для передачи данных на сервер ЖЖ используется протокол HTTP с методом передачи POST. Работа с сервером возможна по двум внутренним протоколам: в виде так называемого плоского (Flat) протокола и в виде XML-RPC. Страница документации по работе по плоскому протоколу расположена здесь, а в формате XML-RPC здесь. Отличия этих протоколов в том, что в первом случае запрос задается в виде: mode=function&параметр1=значение1&параметр2=значение2&...&параметрN=значениеN где mode является обязательным параметром, значением которого определяет функцию сервера ЖЖ, которую мы хотим вызвать. Возможные функции для протокола Flat перечислены на этой странице. За параметром mode задаются параметры, которые зависят от вызываемой функции. Ответ представляет собой чередующиеся строки: первая строка - параметр, вторая - значение, третья - параметр, четвертая - значение и т.д. В формате XML-RPC и запрос, и ответ представляются в формате XML. Большую часть статьи мы будем использовать плоский протокол, а в конце кратко рассмотрим один пример, с протоколом XML-RPC. Для этой статьи я написал небольшую программку, которая использует некоторые функции сервера ЖЖ, которые мы рассмотрим в статье. Скачать ее исходники можно отсюда. Скриншот главного окна программы показан на рисунке 1. Программа написана на языке C# под платформу .NET Framework 2.0.
На рисунке 2 Изображена UML-диаграмма классов, отвечающих за работу с сервером в тестовой программе. Базовым является абстрактный класс LJServer, в котором собраны все действия, не зависящие от протокола обмена данными (Flat или XML-RPC). От него производятся два класса: FlatLJServer для работы по протоколу Flat и XmlLJServer для работы по протоколу XML-RPC. По сути класс LJServer выполняет всю сетевую работу, а производные классы только подготавливают необходимые запросы. Так как это демонстрационная программа, то выполнение запросов и получение ответов происходит синхронно в главном потоке программы. Поэтому на это время программа "подвисает". В реальных условиях, конечно, эти действия надо выполнять асинхронно или выделять им отдельный поток. Также здесь минимальное количество проверок на ошибки.
Обратите внимание на абстрактное свойство базового класса ServerUri. Через это свойство производный класс определяет адрес, куда следует слать запросы. Этот адрес для протоколов Flat и XML-RPC разный. В случае плоского протокола он должен быть http://www.livejournal.com/interface/flat, а в случае работы по протоколу XML-RPC - http://www.livejournal.com/interface/xmlrpc. Работа с серверомДавайте сначала рассмотрим то, каким образом мы будем отправлять запросы на сервер. Для этого в классе LJServer реализована функция SendRequest, принимающая текстовую строку с телом запроса и возвращающая текст ответа. Пояснения работы этой функции даны в комментариях в следующем участке кода. Отметим только, что здесь для простоты нет никаких проверок на ошибки соединения и т.п. В реальной программе этого, разумеется, допускать нельзя. Итак, исходник функции SendRequest: protected string SendRequest (string textRequest) { // Выводим в лог посылаемый запрос _log.WriteLine ("\r\n*** Request:"); _log.WriteLine (textRequest); // Преобразуем запрос из строки в byte[] byte[] byteArray = Encoding.UTF8.GetBytes (textRequest); // Поулчаем класс запроса HttpWebRequest request = (HttpWebRequest)WebRequest.Create (ServerUri); // Заполняем параметры запроса request.Credentials = CredentialCache.DefaultCredentials; request.Method = "POST"; request.ContentLength = textRequest.Length; request.ContentType = this.ContentType; // Очищаем коллекцию от старых cookie и добавляем туда новые request.CookieContainer = new CookieContainer (); request.CookieContainer.Add (_cookies); // Заполняем параметры Proxy (_proxy == null, если прокси не используется) request.Proxy = _proxy; // Если есть cookie с именем "ljsession", то для авторизации с ее помощью // необходимо добавить заголовок с именем "X-LJ-Auth" и значением "cookie" if (_cookies["ljsession"] != null) { request.Headers.Add ("X-LJ-Auth", "cookie"); } // Отправляем данные запроса Stream requestStream = request.GetRequestStream (); requestStream.Write (byteArray, 0, textRequest.Length); // Получаем класс ответа HttpWebResponse response = (HttpWebResponse)request.GetResponse (); // Читаем ответ Stream responseStream = response.GetResponseStream (); StreamReader readStream = new StreamReader (responseStream, Encoding.UTF8); string currResponse = readStream.ReadToEnd (); // Выводим в лог ответ сервера _log.WriteLine ("\r\n*** Response:"); _log.WriteLine (currResponse); // Выведем в лог полученные в ответе cookie _log.WriteLine ("\r\n*** Cookies:"); for (int i = 0; i < response.Cookies.Count; i++) { _log.WriteLine (response.Cookies[i].ToString()); } readStream.Close (); response.Close (); return currResponse; } Так как взаимодействие с сервером, как уже говорилось, происходит по протоколу HTTP, то мы будем использовать классы HttpWebRequest и HttpWebResponse из библиотеки .NET Framework соответственно для отправки запроса и получения ответа. Единственный момент, который в данный момент может быть непонятен, это участок кода, где создается заголовок с именем X-LJ-Auth. Для чего это делается, рассмотрим в разделе, посвященном авторизации с помощью cookie. Переменная _log отвечает за вывод текста в многострочный TextBox в главном окне программы. Переменная _proxy определяется также из главного окна программы. Она задает параметры прокси-сервера, или, если он не используется, этой переменной присваивается значение null. Обратите внимание, что запрос происходит с помощью метода POST, хотя данные для протокола Flat будут напоминать параметры для метода GET. С помощью абстрактного свойства ContentType производные классы определяют каким должен быть заголовок ContentType запроса. Для протокола Flat здесь должно быть application/x-www-form-urlencoded, а для XML-RPC text/xml. АвторизацияВажнейшей функцией сервера ЖЖ является авторизация пользователя. Без нее невозможно выполнить практически ни одного действия (есть только служебная функция getchallenge, которая не требует авторизации). Авторизация пользователя может проходить тремя способами. Будем называть их так же английскими словами, как они называются в документации:
Функция сервера loginАвторизацию использует практически каждая функция сервера, но мы в качестве примера будем использовать функцию login, документация по которой для протокола Flat расположена на этой странице. Эта функция предназначена для получения от сервера некоторой информации о пользователе ЖЖ, но нам это в данный момент не интересно, эта функция будет использоваться только для демонстрации того, каким образом происходит авторизация. Рассмотрим параметры этой функции. Так как эта статья не претендует на роль полной документации по серверу ЖЖ, то здесь мы рассмотрим только те параметры, которые будут использоваться в примерах, а некоторые необязательные параметры опустим. Их описание есть в документации.
Функция возвращает довольно большое число параметров, но нас пока будут интересовать только два:
Clear-авторизацияЭтот тип авторизации является самым простым, поэтому с него и начнем. Но следует помнить, что в реальных программах, работающих с сервером ЖЖ, эту авторизацию лучше не использовать из-за того, что в данном случае пароль от блога передается в явном виде, так что злоумышленники могут без проблем его перехватить. Даже хеш md5 в этом случае не поможет. На самом деле этот тип авторизации можно разделить на два варианта:
Передача пароля в явном видеНачнем с первого случая, когда пароль передается в явном виде, и заодно рассмотрим как формируется запрос по протоколу Flat. Для Clear-авторизации с передачей пароля в явном виде надо заполнить следующие параметры:
В программе LJServerTest запрос для этого типа авторизации формируется в методе LoginClear, который принимает два параметра: имя пользователя и пароль. Запрос выполняется следующим образом: public override void LoginClear (string user, string password) { string request = string.Format ("mode=login&auth_method=clear&user={0}&password={1}", user, password); this.SendRequest (request); } Как видно из этого участка кода, параметры разделяются с помощью символа '&', а значения присваиваются после знака '=', т.е. как при задании параметров способом GET через адресную строку браузера. Давайте посмотрим, что нам ответит на такой запрос сервер. Для игр с сервером ЖЖ я создал отдельного пользователя - jenyay_test, на котором и буду демонстрировать все примеры. Итак, ответ сервера: frgrp_1_name Я не буду полностью описывать все параметры, которые возвращает сервер ЖЖ, все они описаны в документации. В данный момент нас будет интересовать только параметр success, который определяет удалась ли нам авторизация. Как видите, в данном случае сервер вернул значение success как OK, т.е. пароль правильный. А вот как выглядел бы ответ, если бы мы ошиблись при вводе пароля: errmsg Как видите, параметр success равняется значению FAIL, а в параметре errmsg описана ошибка, в данном случае Invalid password. Возвращаемые сервером параметры при прочих видах авторизации не будут отличаться от того, что было описано здесь, поэтому далее мы не будем на них акцентировать внимание при вызове функции login. Передача MD5-хеша пароляВторой, формально чуть более защищенный способ авторизации состоит в том, чтобы передавать на сервер не сам пароль, а его хеш MD5. Запрос в этом случае выглядит практически так же, как и в предыдущем случае за тем исключением, что параметр password не используется, а используется hpassword, значение которого и будет хешем MD5. В программе LJServerTest за этот тип авторизации отвечает метод LoginClearMD5 класса LJServer. Вот как он выглядит: public override void LoginClearMD5 (string user, string password) { string request = string.Format ("mode=login&auth_method=clear&user={0}&hpassword={1}", user, ComputeMD5 (password) ); this.SendRequest (request); } Как видите, ничего сложного. Метод ComputeMD5 я взял из исходников OpenSource-программы LJ.NET, которая, кстати, очень помогла в изучении работы сервера ЖЖ. Вот как выглядит этот метод: /// <summary> /// Посчитать md5. Взято у lj.net. http://lj-net.cvs.sourceforge.net/lj-net/lj-net/Utils.cs?view=markup /// </summary> /// <param name="text"></param> /// <returns></returns> protected string ComputeMD5(string text) { MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider (); byte[] hashBytes = md5.ComputeHash( Encoding.UTF8.GetBytes( text ) ); StringBuilder sb = new StringBuilder(); foreach( byte hashByte in hashBytes ) sb.Append( Convert.ToString( hashByte, 16 ).PadLeft( 2, '0' ) ); return sb.ToString(); } Авторизация с помощью метода challenge / responseЭтот тип авторизации является более защищенным, чем предыдущий. Авторизация методом challenge / response состоит из нескольких этапов: 1. Запрос с сервера так называемого оклика (challenge). То есть через сеть пароль не передается ни в явном виде, ни в виде его MD5-хеша. Давайте рассмотрим эти этапы авторизации поподробнее. Запрос оклика с сервераТак называемый оклик (challenge) представляет собой случайную величину, используя которую шифруется пароль. Для получения оклика на сервере существует специальная функция getchallenge, которая не ожидает никаких параметров, поэтому весь запрос представляет собой просто строку mode=getchallenge Ответ сервера на такой запрос быдет примерно таким: auth_scheme При этом функция возвращает следующие параметры:
Как видим, оклик представляет собой строку вида c0:1192269600:2533:60:8TSdW2fFDrYhQeWM9dpl:10b1d9a13f0e61b5f084c844f74612a2. Каким образом она формируется в данный момент не важно. Главное, что на ее основе мы будем производить дальнейшие действия. Запрос оклика в программе LJServerTest выглядит следующим образом: public override string GetChallenge () { string request = "mode=getchallenge"; string challengeResponse = SendRequest (request); StringDictionary dict = MakeDict (challengeResponse); if (dict["success"] == "OK") { return dict["challenge"]; } return ""; } Здесь метод MakeDict является вспомогательный для формирования словаря типа StringDictionary из ответа сервера, в котором ключем является имя параметра, а значением, соответственно, значение полученного параметра. Метод GetChallenge посылает запрос на получение оклика и, если вызов произошел удачно, возвращает значение полученного оклика. Формирование ответаТеперь, когда мы получили оклик, мы можем сформировать ответ, используя который и будет происходить дальнейшая авторизация. Ответ вычисляется по следующей формуле: MD5 (challenge + MD5(password)) Т.е. вычисляем хеш MD5 пароля, слева добавляем строку отклика и из всего этого вычисляем еще раз MD5-хеш. Именно это значение и будет в дальнейшем передаваться на сервер. На языке C# это будет выглядеть примерно следующим образом: protected string GetAuthResponse (string password, string challenge) { // md5 от пароля string hpass = ComputeMD5 (password); string constr = challenge + hpass; string auth_response = ComputeMD5 (constr); return auth_response; } АвторизацияТеперь нам остается только вызвать нужную функцию (в нашем случае login), которая будет использовать авторизацию типа challenge / response. При таком типе авторизации не используются параметры password и hpassword, а используются auth_challenge, значение которого должно равняться значению оклика, полученного от сервера, и auth_response - параметр, который задает ответ серверу на оклик. При этом значение параметра auth_method должно равняться challenge. В программе LJServerTest за авторизацию типа challenge/response отвечает метод LoginChallenge класса LJServer, принимающий в качестве параметров имя пользователя и пароль: public override void LoginChallenge (string username, string password) { string challenge = GetChallenge (); string auth_response = GetAuthResponse (password, challenge); string request = string.Format ("mode=login&auth_method=challenge&auth_challenge={0}&auth_response={1}&user={2}", challenge, auth_response, username); SendRequest (request); } Полную последовательность запросов и ответов при таком типе авторизации можно увидеть в логе программы при нажатии на кнопку "Авторизоваться (Challenge / Response)". Авторизация с помощью cookieА теперь рассмотрим последний из возможных способов авторизации - с помощью cookie. В программе LJServer для этого типа авторизации предусмотрен метод LoginCookies. Суть этого метода состоит в том, что мы авторизуемся один раз любым описанным выше способом, но при этом создаем cookie, которое и будет использоваться для дальнейших авторизаций. В качестве значения cookie, должна быть так называемая сессия (ljsessoin), которую можно получить, вызвав функцию сервера sessiongenerate, описание которой находится на этой странице документации. Входные параметры ее напоминают параметры уже используемой нами функции login, поэтому повторно описывать их не стоит. Также эта функция имеет еще несколько необязательных параметров, которые в данный момент нас также не интересуют. В результате работы этой функции мы получаем ljsessoin (в качестве одноименного возвращаемого значения). В нашей тестовой программе для генерации сессии предназначен метод SessionGenerate из класса LJServer. В данном случае для получения ljsessoin используется метод авторизации challenge / response. Здесь нет ничего принципиально нового, поэтому просто приведу код этой функции: public override string SessionGenerate (string login, string password) { string challenge = GetChallenge (); string auth_response = GetAuthResponse (password, challenge); string request = string.Format ("mode=sessiongenerate&auth_method=challenge&auth_challenge={0}&auth_response={1}&user={2}&expiration=long", challenge, auth_response, login); string challengeResponse = SendRequest (request); StringDictionary dict = MakeDict (challengeResponse); if (dict["success"] != "OK") { return ""; } return dict["ljsession"]; } Когда сессия получена, необходимо проделать следующие операции для авторизации: 1. Создать cookie с этим значением и именем ljsession. В качестве хоста для cookie необходимо указать "livejournal.com", а в качестве пути "/". В программе LJServerTest авторизацию с помощью cookie выполняем метод LoginCookies из класса LJServer: public override void LoginCookies (string login, string password) { string ljsession = SessionGenerate (login, password); Cookie cookie = new Cookie ("ljsession", ljsession, "/", "livejournal.com"); _cookies = new CookieCollection (); _cookies.Add (cookie); string request = string.Format ("mode=login&auth_method=cookie&user={0}", login); SendRequest (request); } Напомню лишь, что переменная _cookies имеет тип CookieCollection и хранит в себе cookies, которые добавляются в запрос перед отправкой. Как видите, после создания cookie строка запроса становится намного короче и нагляднее, чем при других типах авторизации. Ну что ж, мы рассмотрели с вами три типа авторизации. Каким из них пользоваться решать вам. Скажу только, что Semagic для отправки сообщений использует метод challenge / response, а LJ.NET - cookie. Отправка сообщений в блогИтак, мы с вами научились проходить авторизацию на сервере Живого Журнала. Для демонстрации этого мы использовали функцию сервера login, через которую можно получить некоторые сведения о пользователе. Давайте применим полученные знания для еще более интересной задачи - отправки сообщений в свой блог. Для этого предназначена функция postevent. Давайте рассмотрим ее параметры. Я не буду здесь перечислять те параметры, которые используются для авторизации - они те же самое, что и в других функциях. Также я не буду описывать необязательные поля (некоторые из них мы рассмотрим ниже отдельно).
Вызов этой функции ничем не отличается от вызовов других функций, но надо помнить, что все поля, которые могут содержать русские буквы и другие нелатинские символы или цифры, необходимо предварительно url-закодировать. В библиотеке .NET Framework для этого предназначена функция HttpUtility.UrlEncode(). Если забыть сделать url-кодирование, то сервер возвращает ошибку, но вот описание ошибки в возвращенном параметре errmsg будет скорее всего указывать на неправильное значение совсем другого параметра. Прежде чем рассматривать код для отправки сообщений давайте сначала кратко рассмотрим дополнительные параметры, имена которых начинаются с prop_. Полный список этих параметров описан на этой странице документации, но мы рассмотрим только один из них, чтобы понять в целом как их использовать. Рассматриваемым параметром будет current_music, который определяет музыку, которую слушает в данный момент автор сообщения. Таким образом, параметр, отвечающий за музыку, должен иметь имя prop_current_music. Значение параметра, разумеется, тоже должно быть url-закодировано. В нашей тестовой программе в качестве музыки передается значение, которое определяет из какого класса был отправлен текст. Т.е. для протокола Flat это будет "FlatLJserver". А теперь, собственно, сам код: public override void PostEventChallenge (string text, string subj, string user, string password) { string challenge = GetChallenge (); string auth_response = GetAuthResponse (password, challenge); DateTime date = DateTime.Now; string request = string.Format ("mode=postevent&auth_method=challenge&auth_challenge={0} &auth_response={1}& user={2}& event={3}& subject={4}& allowmask=0& year={5}&mon={6}&day={7}& hour={8}&min={9}&ver=1& prop_current_music={10}& ver=1", challenge, auth_response, user, HttpUtility.UrlEncode (text), HttpUtility.UrlEncode (subj), date.Year, date.Month, date.Day, date.Hour, date.Minute, HttpUtility.UrlEncode ("FlatLJserver")); SendRequest (request); } Здесь, чтобы не делать слишком длинную строку на странице, часть кода, где формируется запрос я разбил на несколько строк. Для отправки сообщений здесь используется авторизация типа challenge / response. Для простоты программа LJServerTest отправляет все время один и тот же текст на сервер. Для Flat-протокола это "Этот пост отправлен из тестовой программы" с заголовком "Проверка". Сервер ЖЖ похоже проверяет, чтобы нельзя было отправить один и тот же текст несколько раз. Поэтому, если вы захотите повторно отправить текст из тестовой программы, то удалите предварительно сообщение, которое было отправлено программой до этого. Что интересно, при повторной отправке сообщения с таким же текстом сервер не сообщает об ошибке, а просто возвращает ссылку на уже существующую запись. Кстати, раз уж мы заговорили о возвращаемых значениях, то тут мы натыкаемся на противоречия между тем, что написано в документации и что сервер возвращает на самом деле. Скорее всего после изменений в коде сервера забыли изменить документацию. В справке кроме стандартных возвращаемых значений success и errmsg обещают еще один параметр itemid - уникальный номер поста. В реальности есть еще несколько возвращаемых значений. Вот, например, как выглядит ответ, полученный мной только что: anum Описание возвращаемого значения anum нашлось в описании функции postevent для протокола XML-RPC (http://www.livejournal.com/doc/server/ljp.csp.xml-rpc.postevent.html). Правда описание этого параметра довольно скромное. Из него можно узнать, что это число используется для вычисления другого возвращаемого значения - itemid. Каким образом это происходит остается загадкой. Возможно, это описано где-то в другом месте документации, но мне этого найти не удалось. Еще одним возвращаемым значением является адрес нового сообщения. Этот параметр, тоже не описан в документации, но тут и так все понятно. Кстати, интересно каким образом рассчитывается число для адреса html-страницы нового сообщения. Работа по протоколу XML-RPCПод конец давайте рассмотрим какой-нибудь пример авторизации с использованием протокола XML-RPC. По сути здесь не будет ничего нового, просто по-другому формируются запросы и ответы (в виде XML). Отмечу только, что значение заголовка Content-Type запроса должно быть text/xml. Давайте для простоты рассмотрим Clear-авторизацию с передачей MD5-хеша пароля. Запрос в этом случае выглядит следующим образом: <?xml version='1.0'?> <methodCall> <methodName>LJ.XMLRPC.login</methodName> <params> <param> <value><struct> <member><name>username</name> <value><string>jenyay_test</string></value> </member> <member><name>hpassword</name> <value><string>XXXXXXXXX</string></value> </member> <member><name>ver</name> <value><int>1</int></value> </member> </struct></value> </param> </params> </methodCall> Обратите внимание, что при работе по такому протоколу переменные имеют разные типы. Например, имя пользователя является строкой и задается следующим образом: <member><name>username</name> <value><string>jenyay_test</string></value> </member> Как видите, параметр заключен внутрь тега member. Имя параметра окружено тегом name, а его значение - тегом value, внутрь которого вставлен тег, описывающий тип параметра, в нашем случае string. Для примера, вот как выглядит целочисленный параметр: <member><name>ver</name> <value><int>1</int></value> </member> В таком же виде приходит и ответ сервера. Для экономии места я не буду приводить здесь ответ сервера на предыдущий запрос. При желании вы можете его увидеть, самостоятельно выполнив запрос в тестовой программе. Не забудьте только переключить протокол XML-RPC с помощью соответствующего ComboBox-а. Напомню, что за работу с сервером ЖЖ в формате XML-RPC в тестовой программе отвечает класс XmlLJServer, производный от LJServer. Тестовая программа может выполнять все те операции, которые мы рассмотрели в этой статье и по протоколу XML-RPC. Есть только один момент, который хотелось бы отметить. Мне так и не удалось отправить в журнал текст на русском языке, скорее всего я что-то делал не так. Кодировка при передаче данных должна быть UTF-8. Еще хотелось бы предупредить, что в примере из документации к функции getchallenge есть одна неточность. Так как функция не принимает никаких параметров, то не надо в запросе писать "<param></param>". Правильный запрос должен выглядеть следующим образом: <?xml version='1.0'?> <methodCall> <methodName>LJ.XMLRPC.getchallenge</methodName> <params> </params> </methodCall> СсылкиСледующие статьи из серии про про работу с сервером ЖЖ:
Пожалуйста, оцените материал
|