Obsługa protokołu HTTP przy użyciu pakietu Synapse

olesio

Ostatnio coraz więcej osób przekonuje się do pakietu Synapse zamiast Indy. Niestety nie chcą szukać w Google ani czytać dołączonej dokumentacji czy też analizować dołączonych przykładów. Dlatego postanowiłem w końcu napisac artykuł opisujący po krótce przykładowo użycie komponentu THttpSend z pakietu Synapse do obslugi protokołu HTTP. Jeżeli coś z Indy nie działa, a strona www którą chcesz okiełznać nie potrzebuje JavaScriptu do wykonania żądanych przez Ciebie operacji, nie używaj armaty na muchy czyli TwebBrowsera – skorzystaj z Synapse :)

1. Instalacja pakietu.
Synapse się nie instaluje. Synapse się po prostu używa :) Swój artykuł będę pisał na podstawie doświadczeń z używaną przez siebie wersją Delphi 7 Personal. Z tego co się orientuje to niższych wersji aby w pełni korzystać z dobrodziejstw aktualnego wydania Synapse - nie ma co używać. A powyżej wersji 2009 niestety - z tego co wiem - są problemy z typami przechowującymi łańcuchy tekstów i pakiet póki co nie działa. Może w niedalekiej przyszłości autor to poprawi i wszystko będzie działać również z nowszymi wersjami środowiska. Ok, do rzeczy - pobieramy pakiet z: http://www.ararat.cz/synapse/doku.php/download i wypakowujemy do jakiegoś katalogu. Teraz aby użyć Synapse możemy przenieść zawartość katalogu LIB do podkatalogu LIB w swoim Delphi. Tak będzie najprościej. Ja jednak stosuje taki sposób, że staram się niestandardowych komponentów uzywać dynamicznie (co w przypadku Synapse jest koniecznością, bo pakiet nie posiada w modułach procedur Register pozwalających na dodanie komponentów do ich paska w jakiejkolwiek zakładce), a wszelkie potrzebne moduły do obsługi protokołu HTTP umieścić w katalogu z projektem. Daje to tę możliwość, że nasz projekt z kodem możemy na szybko wysłać innej osobie, a ona bez konieczności pobierania komponentu będzie mogła obsłużyć protokół HTTP z użyciem Synapse. Wprawdzie jest też Visual Synapse do obsługi komponentów przez umieszczanie ich ikonek na formatce tak jak w Indy, ale z tej wersji nie korzystałem nigdy i w tym artykule skupię się na wersji niewizualnej, którą również bez problemu użyją posiadacze Turbo Delphi. Do działania protokołu HTTP wymagane są moduły w plikach, o nazwach poniżej:

blcksock.pas
httpsend.pas
sswin32.pas
synacode.pas
synafpc.pas
synaip.pas
synautil.pas
synsock.pas

Do obsługi szyfrowanego protokołu HTTPS potrzebne są dodatkowo te moduły:

ssl_openssl.pas
ssl_openssl_lib.pas

Jeżeli chcemy korzystać z protokołu HTTPS to dodatkowo do uses należy dołączyć moduł ssl_openssl, a w katalogu projektu lub w ścieżce systemowej umieścić biblioteki dll:

libeay32.dll
ssleay32.dll

Możemy je pobrać z: http://synapse.ararat.cz/files/crypt

2. Pobieranie danych metodą GET.
Najprościej plik pobierzemy tworząc obiekt typu THttpSend i wywołując funkcję HttpMethod z parametrem 'GET' i pełnym adresem do pliku lub strony. To co pobierzemy zostanie umieszczone w zmiennej Document typu TMemoryStream. Poniższy kod pokazuje jak pobrać obrazek typu JPEG do komponentu Image:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, JPEG, httpsend;

type
  TForm1 = class(TForm)
    Button1 : TButton;
    Image1 : TImage;
    procedure Button1Click(Sender : TObject);
  private
  public
  end;

var
  Form1 : TForm1;

const
  Opera_UserAgent = 'Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.10';

implementation

{$R *.dfm}

procedure DownloadToStream(const URL : string; const SomeStream : TStream);
const
  Location_Prefix = 'Location:' + #32;
var
  SynHttp : THttpSend;
  I, Position : integer;
  Str, DirectLink : string;
begin
  SynHttp := THttpSend.Create;
  try
    SynHttp.UserAgent := Opera_UserAgent;
    SynHttp.HTTPMethod('GET', Url);
    case SynHttp.ResultCode of
      301, 302 :
        begin
          for I := 0 to SynHttp.Headers.Count - 1 do
          begin
            Str := SynHttp.Headers[I];
            Position := Pos(Location_Prefix, Str);
            if Position > 0 then
            begin
              DirectLink := Copy(Str, Position + Length(Location_Prefix), MaxInt);
              Break;
            end;
          end;
          DownloadToStream(DirectLink, SomeStream);
        end;
    else
      SynHttp.Document.SaveToStream(SomeStream);
      SomeStream.Position := 0;
    end;
  finally
    SynHttp.Free;
  end;
end;

procedure TForm1.Button1Click(Sender : TObject);
var
  Jpg : TJpegImage;
  MemStream : TMemoryStream;
begin
  Jpg := TJpegImage.Create;
  MemStream := TMemoryStream.Create;
  MemStream.Position := 0;
  DownloadToStream('http://www.gover.pl/userfiles/publikacje/szczecin.jpg', MemStream);
  MemStream.Position := 0;
  try
    Jpg.LoadFromStream(MemStream);
    Image1.Picture.Assign(Jpg);
  finally
    MemStream.Free;
  end;
end;

Wyjaśnienia wymaga tutaj linijka z ustawieniem UserAgenta. UserAgent to ciąg znaków, który wysyłają niemal wszystkie aplikacje korzystające z protokołu HTTP. Nasz UserAgent zależy od użytej przeglądarki i systemu operacyjnego. Akurat ten powyżej to starsza wersja Opery i system Windows XP, z którego już się przesiadłem :) Po UserAgencie serwer HTTP identyfikuje przeglądarkę i system użytkownika lub dodatkowe informacje o użytej nakładce czy pluginach. Swój UserAgent możesz sprawdzić na stronie: http://whatsmyuseragent.com i zalecane jest ustawienie go na jakiś zbliżony do prawdziwej przeglądarki www, ponieważ może się zdarzyć, że zwrócona zawartość plików będzie się nieco różnić na przykład formatowaniem, co ma miejsce w przypadku na przykład FaceBook'a. Dodatkowo widać też pętlę w przypadku zwrócenia przez obiekt SynHttp kodu rezultatu wynoszącego 301 lub 302. Zgodnie z http://pl.wikipedia.org/wiki/Kod_odpowiedzi_HTTP widzimy iż jest to kod świadczący o tym, że strona przekierowuje nas na inny adres. Nowy adres należy pobrać z nagłówka będącego we własności Headers.Text po słowie Location: . Przekierowania w Synapse trzeba obsłużyć "ręcznie". W przypadku wielu stron opartych na kodzie w php po Location: często będzie znajdował się tylko dokument, na który będzie przekierowanie bez pełnego adresu serwera – na przykład /cosik.php w takim przypadku, należy dodać odpowiedni adres podstawowy i dopiero taką stronę pobrać.

3. Wysyłanie danych metodą POST.
Często wszelkie formularze logowania na stronach wymagają podania nazwy użytkownika i hasła. Można jednak, jeżeli strona nie potrzebuje JavaScriptu do działania logowania zalogowac się przy użyciu Synapse. Tak jak przy pobieraniu danych metodą GET przy metodzie POST ważny jest UserAgent, ale również własność MimeType. Najlepiej ustawić ją na application/x-www-form-Urlencoded. Dane które należy wysłać do serwera metodą POST muszą znajdować się we własności Document. Najprościej przypisać potrzebne dane do zmiennej typu string, a później zapisać do własności Document. Skąd wiadomo co wysłać metodą post do serwera? Otóż można posłużyć się Debuggereami HTTP. Ja jednak najczęściej używam do tego celu sniffera WireShark, również dostępnego na stronie Synapse. Jak go używać można dowiedzieć się z tutoriali na YouTube. Przy wysyłaniu danych metodą POST również musimy obsłużyć przekierowanie. Najlepiej pokażę na przykładzie logowanie do serwisu www.wupload.com w przypadku którego po podaniu prawidlowych danych do zalogowania zawsze następuje przekierowanie na kolejną stronę.

//...
uses
  httpsend;

procedure TForm1.LogonBtnClick(Sender : TObject);
const
  UserHandle = 'user';
  UserPassword = 'ps';
  Base_Url = ' http://www.wupload.com';
  Location_Prefix = 'Location:' + #32;
  Error_Text1 = 'No user found with such email';
  Error_Text2 = 'Provided password does not match';
  ToPost_MimeType = 'application/x-www-form-Urlencoded';
  Opera_UserAgent = 'Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.8.131 Version/11.10';
var
  SynHttp : THttpSend;
  I, Position : integer;
  URLData, Str, RedirUrl, FPage : string;
begin
  Application.Title := Caption;
  SynHttp := THttpSend.Create;
  try
    SynHttp.KeepAlive := True;
    SynHttp.Protocol := '1.1';
    SynHttp.MimeType := ToPost_MimeType;
    SynHttp.UserAgent := Opera_UserAgent;
    UrlData := 'email=' + UserHandle + '&redirect=%2Faccount%2Ffree-signup' +
      '&password=' + UserPassword + '&rememberMe=1';
    SynHttp.Document.Write(Pointer(URLData)^, Length(URLData));
    SynHttp.HTTPMethod('POST', Base_Url + '/account/login');
    case SynHttp.ResultCode of
      301, 302 :
        begin
          for I := 0 to SynHttp.Headers.Count - 1 do
          begin
            Str := SynHttp.Headers[I];
            Position := Pos(Location_Prefix, Str);
            if Position > 0 then
            begin
              RedirUrl := Copy(Str, Position + Length(Location_Prefix), MaxInt);
              if Pos('/', RedirUrl) = 1 then
              begin
                RedirUrl := Base_Url + RedirUrl;
              end
              else
              begin
                RedirUrl := Base_Url + '/' + RedirUrl;
              end;
            end;
          end;
          SynHttp.Headers.Clear;
          SynHttp.Document.Write(Pointer(URLData)^, Length(URLData));
          SynHttp.HTTPMethod('POST', RedirUrl);
          SetLength(FPage, SynHttp.Document.Size);
          SynHttp.Document.Read(PChar(FPage)^, Length(FPage));
          if Pos('Welcome, ' + UserHandle, FPage) > 0 then
          begin
            MessageBox(Application.Handle, PChar('Zalogowany prawidłowo.'),
              PChar(Application.Title), MB_ICONINFORMATION + MB_OK);
          end;
        end
    else
      begin
        SetLength(FPage, SynHttp.Document.Size);
        SynHttp.Document.Read(PChar(FPage)^, Length(FPage));
        if Pos(Error_Text1, FPage) > 0 then
        begin
          MessageBox(Application.Handle, PChar(Error_Text1),
            PChar(Application.Title), MB_ICONERROR + MB_OK);
        end;
        if Pos(Error_Text2, FPage) > 0 then
        begin
          MessageBox(Application.Handle, PChar(Error_Text2),
            PChar(Application.Title), MB_ICONERROR + MB_OK);
        end;
      end;
    end;
  finally
    SynHttp.Free;
  end;
end;

Dodam, że ważne jest czyszcznie nagłowków przez Headers.Clear przed pobraniem metodą GET nowej strony. Powyżej jest również proste sprawdzenie czy zwrócony w location adres jest kompletny (czy zawiera prefix http://lockerz.comie zaczyna się od znaku „/”. Oczywiście jeżeli chcemy uniknąc zamrożenia formatki to operacje róbmy w wątkach, a jeżeli po zalogowaniu mamy zamiar dokonywać kolejnych operacji na posiadającym odpowiednie już ciasteczka obiekcie ThttpSend to oczywiście deklarujmy zmienną typu THttpSend jako globalną lub w sekcji public naszej formatki. Poniżej podam najlepiej kolejny przykład logowania tym razem na serwisu http://lockerz.com

//...
uses
  httpsend;

procedure TForm1.LogonBtnClick(Sender : TObject);
const
  UserHandle = 'e-m@il.net';
  UserPassword = 'password';
  Base_Url = 'http://lockerz.com/';
  Location_Prefix = 'Location:' + #32;
  Error_Text1 = 'Invalid sign in, please try again.';
  ToPost_MimeType = 'application/x-www-form-Urlencoded';
  Opera_UserAgent = 'Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.8.131 Version/11.10';
var
  SynHttp : THttpSend;
  I, Position : integer;
  URLData, Str, DirectLink, FPage : string;
begin
  Application.Title := Caption;
  SynHttp := THttpSend.Create;
  try
    SynHttp.KeepAlive := True;
    SynHttp.Protocol := '1.1';
    SynHttp.MimeType := ToPost_MimeType;
    SynHttp.UserAgent := Opera_UserAgent;
    SynHttp.Headers.Insert(0, 'Accept: text/html,');
    UrlData := 'handle=' + UserHandle + '&password=' + UserPassword;
    SynHttp.Document.Write(Pointer(URLData)^, Length(URLData));
    SynHttp.HTTPMethod('POST', Base_Url + 'auth/login');
    case SynHttp.ResultCode of
      301, 302 :
        begin
          for I := 0 to SynHttp.Headers.Count - 1 do
          begin
            Str := SynHttp.Headers[I];
            Position := Pos(Location_Prefix, Str);
            if Position > 0 then
            begin
              DirectLink := Copy(Str, Position + Length(Location_Prefix), MaxInt);
              Break;
            end;
          end;
          SynHttp.Clear;
          SynHttp.Document.Clear;
          SynHttp.KeepAlive := True;
          SynHttp.Protocol := '1.1';
          SynHttp.MimeType := ToPost_MimeType;
          SynHttp.UserAgent := Opera_UserAgent;
          SynHttp.Headers.Insert(0, 'Accept: text/html,');
          SynHttp.Document.Write(Pointer(URLData)^, Length(URLData));
          SynHttp.HTTPMethod('POST', DirectLink);
          SetLength(FPage, SynHttp.Document.Size);
          SynHttp.Document.Read(PChar(FPage)^, Length(FPage));
          if Pos(Error_Text1, FPage) > 0 then
          begin
            MessageBox(Application.Handle, PChar(Error_Text1),
              PChar(Application.Title), MB_ICONERROR + MB_OK);
          end
          else
          begin
            MessageBox(Application.Handle, PChar('Zalogowany prawidłowo.'),
              PChar(Application.Title), MB_ICONINFORMATION + MB_OK);
          end;
        end;
    end;
  finally
    SynHttp.Free;
  end;
end;

Ważne jest że jeżeli wywołamy metodę Clear dla ThttpSend to musimy na nowo ustawić MimeType. Ten kod jest specyficzny. Otóż robiłem już wiele programów logujących się na różne serwisy z użyciem Synapse. Jednak ten był pierwszym, który tak restrykcyjnie wymagał nagłówków podobnych do wysyłanych przez przeglądarkę www. Kluczowa jest poniższa linijka.

  SynHttp.Headers.Insert(0, 'Accept: text/html,');

Bez niej kod nie działal jak należy. Pod WireSharkiem lub innym tego typu oprogramowaniem można zobaczyć, że na przykład Opera wysyła znacznie więcej wartości Accept. Przy okazji można wspomnieć o tym, że nagłowki można uzupełniac i modyfikować. Nie można jednak dodać pewnych wartości. Najlepiej przytoczę fragment dokumentacji:

property Headers: TStringList read FHeaders;

 Before HTTP operation you may define any non-standard headers for HTTP request, except of: 'Expect: 100-continue', 'Content-Length', 'Content-Type', 'Connection', 'Authorization', 'Proxy-Authorization' and 'Host' headers. After HTTP operation contains full headers of returned document.

Najczęściej przyda nam się ta możliwośc jeżeli do nagłówka należy wstawić tak zwany Referer. Czasami jakaś strona z możliwością pobrana plików ale niekoniecznie tylko taka sprawdzi czy weszliśmy na nią z innej konkretnej strony i w zależności od tego dostosuje swoją treść. Przykładem może być serwis Experts Exchange, który jeżeli nie mamy tam konta premium pokaże nam pełną zawartość jeżeli wejdziemy na niego z odsyłacza na stronie wyników wyszukiwania Google. Także mozemy użyć poniższego kodu najczęściej przed wywołaniem metody GET:

  SynHttp.Headers.Insert(0, 'Referer: http://jakas-strona.net');

Modyfikować możemy - w razie konieczności - również własność Cookies.Text w celu zmiany lub uzupełnienia ciasteczek.

4. Pokazanie postępu pobierania.
Na koniec jeszcze jedna ważna i przydatna rzecz. Mianowicie postęp pobierania pliku. Można go zwizualizować używając zdarzenia .Sock.OnStatus. Najpierw do sekcji uses należy dodać moduł blcksock. Następnie zadeklarować procedurę obsługi zdarzenia:

    procedure SockCallBack(Sender : TObject; Reason : THookSocketReason; const Value : string);

W momencie kiedy chcemy pokazać postęp przypisujemy procedurę do zdarzenia:

  SynHttp.Sock.OnStatus := SockCallBack;

Przykładowa definicja procedury wyglądać może tak:

procedure TDownloadProgressForm.SockCallBack(Sender : TObject; Reason : THookSocketReason; const Value : string);
begin
  begin
    if SynHttp.DownloadSize > 0 then
    begin
      DownloadedPB.Max := SynHttp.DownloadSize;
      if (Reason = HR_ReadCount) then
      begin
        DownloadedPB.Position := DownloadedPB.Position + StrToInt(Value);
      end;
    end;
  end;
end;

Oczywiście najlepiej by SynHttp była zmienną globalną utworzoną przed rozpoczęciem pobierania metodą GET. I to tyle. Wybaczcie jak zwykle u mnie rozpisanie się. Ale teraz mam nadzieję początki z obsługą protokołu HTTP z użyciem Synapse będą łatwiejsze. W razie problemów polecam zajrzeć do dokumentacji oraz przykładowych kodów dołaczonych do archiwum z pakietem. Dodam jeszcze, że jeżeli Document po POST ma zero bajtów to oznacza to na ogół konieczność obsłużenia przekierowania. Natomiast jeżeli otrzymujemy błąd Bad Request w zawartości Document to na ogół należy wyczyścić nagłowki. Jednak jeżeli mimo waszych ustaleń strona przy metodzie POST nie chce dopuścić do zalogowania i otrzymujecie inną treść niż spodziewana, a w grę nie wchodzi JavaScript użyty do modyfikowania zawartości strony – to zawsze możecie spróbować takiej metody. Pod WireSharkiem śledząc pakiety z prawdziwej przeglądarki po udanym zalogowaniu - skopiuj cała zawartość nagłówka w podglądzie strumienia TCP i zapiszcie go do pliku. Wytnij pola określających długość nagłówka i ewentualnie inne, o których wspomniałem powyżej cytując fragment dokumentacji. Następnie taki plik można załadować do nagłówków przez Headers.LoadFromFile('plik.txt'); - jeżeli logowanie się powiedzie to znaczy, że w nagłówku są jakieś ważne informacje, których nie przekazujecie. Najczęściej trzeba odwiedzić na przykład stronę głowną jakiegoś serwisu. Zapamiętać ciasteczka w zmiennej typu string, później wywołać procedurę Clear; Ustawić Cookies.Text na to co ustawiliśmy wcześniej i ponownie się zalogować. Myślę, że Synapse poradzi sobie z niemal każdą stroną, która nie wymaga JavaScriptu. To kończę swój wywód. Mam nadzieję, że się komuś on przyda. Powodzenia w stosowaniu Synapse przy obsłudze protokołu HTTP.

8 komentarzy

Dzięki. Jednak z HTTPS żadnej filozofii nie ma. Po prostu dodajesz wspomniane moduły. I odwołujesz się do linków z "https://".

Wlasnie testuje pakiet synapse. W porowaniu z indy jest niesamowicie szybszy i stabilniejszy. Artykul znakomicie wyjasnia obsluge HTTP. GORĄCO POLECAM!!!

Już kiedyś o to pytałeś na forum chyba. Najprostszym rozwiązaniem byłby chyba prosty server HTTP, który można stworzyć w Indy (jest nawet gotowy przykład w Demach) jak i również z wykorzystaniem modułów z pakietu Synapse.

Czy dało by się zrobić taki myk, że to nie program wysyła zapytanie do skryptu PHP tylko skrypt do programu?

Wedlug mnie jest wydajniejsze, ale testów żadnych nie robiłem, więc mogę się mylić. Możesz raczej sprawdzić, który pakiet jest szybszy robiąc ze 100 prób pobierania tej samej strony przez Indy i Synapse na tym samym łaczu i o tej samej porze mierząć czasy GetTickCount i je sumując, a później pokazując CzasKoncowy - CzasPoczatkowy to Tobie wyjdzie liczba milisekund, ale czy to będzie faktycznie dobry wyznacznik prędkości to nie wiem. Wiem, że jak tak robiłem dla pojedyńczego pobrania to na ogół wychodzilo mi, że ciut szybsze jest Synapse niż Indy.

Czy Synapse jest wydajniejsze niż Indy? Mógłby ktoś rozpisać które jest szybsze?

Co do wersji Delphi, to na 2007 nie mam problemu

Bardzo, ale to bardzo przydatny artykuł! i przykładziki świetne :)