Indy Cookie Manager своими руками

Indy Cookie
Компоненты для работы с интернетом Indy очень популярны у приверженцев Delphi, но при этом славятся множеством досадных ошибок в своей реализации. С одной проблемой нам уже удалось разобраться, речь идет о неверной кодировке в передаваемых данных методом TIdHTTP.POST. В этой статье речь пойдет о проблемах работы с Cookies компонента TidHTTP
Проблема с Cookies в Indy насчитывает давнюю историю. В версиях Indy до 10.5.7 требовалось какое-то шаманское вмешательство в исходный код, чтобы заставить TIdHTTP считывать и передавать Cookies. В версии 10.5.7. ситуация вроде бы как нормализовалась, Cookies считываются и передаются. Однако при попытке использовать более новую версию Indy, которую можно получить с официального сайта Indy из репозитория исходного кода разработки у автора Cookies опять отказались работать. В общем то это и не официальный релиз, однако звоночек прозвучал настораживающий. В итоге на данный момент приходится работать на последнем официальном релизе Indy 10.5.7. 
Однако не все так просто. При попытке работать с некоторыми сайтами выяснилось, что формат, в котором TIdCookieManager формирует Cookies в заголовке запроса при выполнении метода TIdHTTP.POST вызывает проблемы. Например, возьмем популярный форум, основанный на движке phpBB. Если попытаться написать автоматический регистратор для этого форума при помощи Indy 10.5.7, то ничего не получится по той причине, что формат Cookies Indy несколько не совпадает с тем, который формирует, например, Internet Explorer или Mozilla Firefox. Из-за этого форум на phpBB считает ваши запросы сформированные ботом, и не позволяет выполнить такую автоматическую регистрацию. Что проявляется например в том, что к URL каптчи он добавляет идентификатор сессии, а это происходит в том случае, если phpBB считает, что ему вообще не переданы Cookies!
В процессе изучения проблемы при помощи сниффера и сравнивая POST запросы, формируемые Indy и Mozilla Firefox и Internet Explorer выяснилось, что форматы передаваемых Cookies заметно отличаются. И если многие сайты на других движках на это не обращают внимания (например, WordPress понимает формат Indy), то phpBB относится к иному формату крайне болезненно, просто не воспринимая Cookies от Indy. 
В итоге автор принял решение отказаться от использования стандартного TIdCookieManager от Indy и вручную считывать и формировать Cookies. Такое решение выглядит оправданным вследствие того, что нет особой надежды на исправление ошибок при работы с Cookies в Indy в будущем.
Для собственной обработки Cookies было написано несколько несложных функций, которые можно встроить, например, в наследник компонента TIdHTTP. Для экземпляра объекта TIdHTTP необходимо выставить свойство AllowCookies в false и, соответственно, не создавать стандартный TIdCookieManager.
Перед выполнением методов  GET и POST необходимо вызвать функцию WriteCookies(URL), а после выполнения – функцию ReadCookies(URL)
Для использования функций для работы с Cookies необходимо в наследнике TIdHTTP создать TStringList для хранения списка Cookies c именем FCookieList. В приведенных ниже функциях http означает экземпляр TIdHTTP, для использования функций в прямом наследнике замените http на Self. Реализация функций для обработки Cookies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
function THTTP1.GetCookieName(Cookie: string): string;
  var i: integer;
begin
  Result:='';
  i:=Pos('=',Cookie);
  if i=0 then Exit;
  Result:=Copy(Cookie,1,i-1);
end;

function THTTP1.GetCookieDomain(Cookie: string): string;
  var i,j: integer;
begin
  Result:='';
  i:=Pos('domain=',Cookie);
  if i=0 then Exit;
  j:=PosEx(';',Cookie,i);
  if j=0 then j:=Length(Cookie)+1;
  Result:=Copy(Cookie,i+7,j-i-7);
  // Remove dot
  if (Result<>'') and (Result[1]='.') then
    Result:=Copy(Result,2,Length(Result)-1);
end;

function THTTP1.GetURLDomain(url: string): string;
  var i,j,l: integer;
begin
  Result:=SysUtils.StringReplace(LowerCase(url),'http://','',[]);
  Result:=SysUtils.StringReplace(Result,'www.','',[]);
  i:=Pos('.',Result);
  if i=0 then Exit;
  j:=PosEx('/',Result,i);
  if j=0 then j:=PosEx('',Result,i);
  if j>0 then Result:=Copy(Result,1,j-1);
  j:=0;
  l:=Length(Result);
  for i:=1 to l do
    if Result[i]='.' then inc(j);
  if j>1 then
  begin
    j:=0;
    for i:=l downto 1 do
    if Result[i]='.' then
    begin
      inc(j);
      if j=2 then
      begin
        Result:=Copy(Result,i+1,l-1);
        Exit;
      end;
    end;
  end;
end;

procedure THTTP1.ReadCookies(url: string);
  var i: integer;
      Cookie: string;

  function DelleteIfExist(Cookie: string): boolean;
    var i: integer;
  begin
    Result:=false;
    for i := 0 to FCookieList.Count - 1 do
    if (GetCookieDomain(FCookieList[i])=GetCookieDomain(Cookie))
    and (GetCookieName(FCookieList[i])=GetCookieName(Cookie)) then
    begin
      FCookieList.Delete(i);
      Exit;
    end;
  end;

begin
  for i := 0 to http.Response.RawHeaders.Count - 1 do
  if Pos('Set-Cookie: ',http.Response.RawHeaders[i])>0 then
  begin
    Cookie:=SysUtils.StringReplace(http.Response.RawHeaders[i],'Set-Cookie: ','',[]);
    if GetCookieDomain(Cookie)='' then Cookie:=Cookie+'; domain='+GetURLDomain(url);
    DelleteIfExist(Cookie);
    FCookieList.Add(Cookie);
  end;
end;

procedure THTTP1.WriteCookies(url: string);
  var i,j: integer;
      Cookies: string;
begin
  for i := 0 to FCookieList.Count - 1 do
  if GetCookieDomain(FCookieList[i])=GetURLDomain(url) then
  begin
    j:=Pos(';',FCookieList[i]);
    Cookies:=Cookies+Copy(FCookieList[i],1,j)+' ';
  end;
  if Cookies<>'' then
  begin
    Cookies:='Cookie: '+Copy(Cookies,1,Length(Cookies)-2);
    http.Request.CustomHeaders.Clear;
    http.Request.CustomHeaders.Add(Cookies);
  end;
end;
Функция GetCookieName считывает имя Cookie из переданной в нее строки, GetCookieDomain получает домен, к которому относится Cookie, GetCookieDomain вычисляет имя домена из URL. Эти функции являются вспомогательными. 
Функция ReadCookies считывает Cookies из http.Response.RawHeaders и записывает их в FCookieList. Если в Cookie отсутствует привязка к домену, то домен вычисляется из URL запроса, переданного в функцию, и добавляется к строке Cookie. При записи в FCookieList проверяется наличие в списке Cookie с таким же именем такого-же домена, если таковой был найден, то он считается устаревшим и удаляется. Функция WriteCookies требует в качестве параметры URL, который будет использован в POST запросе, из этого URL выделяется имя домена, затем формируется строка со списком Cookies для домена запроса. Затем эта строка записывается в список http.Request.CustomHeaders
Основное отличие приведенного метода формирования Cookies для POST запроса от метода, примененного в Indy, состоит в том, что Cookies формируются всегда одной строкой, а не списком строк, если Cookie больше одного. Не добавляется также как в TIdCookieManager строка “Version”, которая на взгляд автора статьи является бессмысленной. Формирование строки с Cookies происходит точно так же, как и в браузерах Mozilla Firefox и Internet Explorer
Конечно, можно заметить, что логика формирования Cookies в предлагаемом методе не учитывает срок действия Cookies и некоторых дополнительных параметров, но кардинально на работу достаточно коротких HTTP сессий это не влияет, кроме того, реализацию поддержки этих параметров автор предполагает произвести в будущем, когда в этом назреет необходимость. На данном же этапе собственные функции для обработки Cookies позволили решить проблемы с автоматической регистрацией на многих сайтах, в частности, на phpBB.

Related Post

Synapse THttpSend В этой статье мы рассмотрим один из основных модулей замечательной библиотеки Synapse, предназначенный для работы по протоколу HTTP. Synapse это неви...
Livejournal XML-RPC. Часть 2 Продолжаем разговор про...
Взаимные преобразования OleVariant и String... Ниже приведены две прост...

12 thoughts on “Indy Cookie Manager своими руками

  1. Исправлены функции:
    GetCookieDomain – Теперь учитывается возможная точка перед доменом в Cookie – прямо те же грабли, на которые наступали когда-то разработчики Indy
    GetURLDomain – неправильно вычислялся домен из URL третьего уровня – исправлено

  2. Полезная штука конечно.
    Особенно ввиду того что даже если старые версии работали например с Delphi 2010 то с XE старушки не работают.
    А новые так и подавно.

    Не составит ли труда автору описать непосредственно исправление родного CookieManager?
    Потому как например в больших приложениях постоянно баловаться с Write/ReadCookies как-то не особо.

    Заранее спасибо.

  3. ange007, дело в том, что когда я начал реализовывать действительно крупный проект на Delphi с использованием Indy и понял, что в наличии куча проблем с куками, кодировками и т.п. встал вопрос, что делать? Править исходники родного CookieManager я в итоге не решился, так как он значительно изменился, скажем, от версии 10.5.7 до последней, которая доступна в репозитории Indy. В этой последней версии на момент, когда я ее смотрел, CookieManager опять перестал работать даже по сравнению с корявой работой в 10.5.7. В итоге чтобы не заниматься в дальнейшем возможной правкой исходников следующих версий я решил сделать свой CookieManager как некоторый набор функций, который не трогает исходников Indy. На первый взгляд это выглядит несколько коряво, но у меня сделан наследник TIdHTTP, у которого добавлено свойство – использовать новый CookieManager или старый. Плюс написаны новые Post и Get, которые не имеют проблем с кодировкой и куками. Мне такой путь показался более перспективным, и, главное для меня, не трогающим исходников Indy, которые периодически сильно меняются.
    Пока мой CookieManager видимо еще довольно сырой, периодически правлю ошибки и дорабатываю соответственно этот пост. Но, он на данный момент работает, в отличие от стандартного, со всеми сайтами, а не только с некоторыми. И форумами, и редиректами Яндекса и прочими.
    Поэтому в итоге пока что не планирую править исходники родного CookieManager-а за полным отсутствием времени.

  4. Ну в принципе да, постоянно переделывать.
    Ну тогда займусь я созданием наследника idHTTP с вашими наработками.

    Ещё раз спасибо за статью.

  5. И кстати, выложите пример с этим модулем.
    А-то CMS корячит код.
    И многие символы меняет.

  6. Есть такая проблема, пока не знаю, как решить. Текст в редакторе формируется нормально, при выводе косячит. Одинарная кавычка меняется на непонятно что

  7. Пробую по Вашим наработкам залогиниться по https, но не получается 🙁

    URLLogin := ‘https://secret.myhost.ru/secret/login.htms?LOGIN=aaa&PASSWD=bbb&enter=Enter’;
    URL := ‘https://secret.myhost.ru/secret/’;

    http := THTTP1.Create;
    http.HandleRedirects := True;
    http.IOHandler := IOSSL; // в его свойствах прописан сертификат

    http.AllowCookies := False;
    http.Request.BasicAuthentication := False; //True – без разницы
    http.Request.Host := URL;
    http.Request.Referer:= URL;

    s:= http.Get(URL);
    http.ReadCookies(URL);
    http.WriteCookies(URL);
    // нет куков

    s:= http.Get(URLLogin);
    http.ReadCookies(URLLogin);
    http.WriteCookies(URLLogin);
    // кука определилась: PHPSESSID=839b6935d0d335ad9a00ae84f87558af; path=/; domain=secret.myhost.ru
    // но в ответ https://secret.myhost.ru/secret/index.htms?AUTH_ERR=INIT&FAIL=1

    data:= TStringList.Create;
    data.Add(‘LOGIN=aaa’);
    data.Add(‘PASSWD=bbb’);
    data.Add(‘enter=Enter’);
    s:= http.Post(‘https://secret.myhost.ru/secret/login.htms’, data);
    // кука определилась: PHPSESSID=839b6935d0d335ad9a00ae84f87558af; path=/; domain=secret.myhost.ru
    // но в ответ https://secret.myhost.ru/secret/index.htms?AUTH_ERR=INIT&FAIL=1

    что я делаю не так?
    убирал https в GetURLDomain:
    Result:=SysUtils.StringReplace(LowerCase(url),’https://’,”,[]);
    все равно безрезультатно

  8. edward, я сделал работающий пример по всем этим технологиям:
    Indy Cookie Manager своими руками. Работающий пример
    По вашему коду мне сложно разобраться, так как сайт secret.myhost.ru не существует. Если есть какие-то проблемы в Cookie Manager-е, то необходимо будет смотреть сниффером, что делает браузер и программа на Delphi, и только так можно будет найти проблему. Попробуйте пример с ЖЖ через https, если что-то не получится, тогда дайте конкретный URL сайта (можно на мою почту, чтобы не светить на блоге), попробую разобраться.

  9. Спасибо огромное, с этим разобрался. Можно пост прибить. Дело было в редиректе. Сейчас бьюсь с “???????” вместо русского текста 🙂 так как Character.dcu в D7 не нашел (видимо TCharacter.ConvertToUtf32 оттуда), да и TStringStream в D7 без указания кодировки

  10. Попробовал на основе ваших функций сформировать новый класс-потомок. Но при компиляции модуля в Delphi 5 выскакивает ошибка на функцию PosEx, ибо ее нет в стандарных модулях. Приведите ее текст пожалуйста. У меня Indy 9.0.18.

  11. Сможете за вознаграждение на примерах расжевать за какойто часик работу с куками и авторизацией на сайтах?
    интересует понять в деталях сохранение и выдачу всех параметров. Спасибо

Leave a Reply

Your email address will not be published. Required fields are marked *