1139 lines
34 KiB
ObjectPascal
1139 lines
34 KiB
ObjectPascal
{$IFDEF FPC}
|
|
{$mode delphi}
|
|
{$ENDIF}
|
|
unit KOLHTTPDownload;
|
|
{
|
|
|
|
("`-''-/").___..--''"`-._
|
|
`6_ 6 ) `-. ( ).`-.__.`)
|
|
(_Y_.)' ._ ) `._ `. ``-..-'
|
|
_..`--'_..-_/ /--'_.' ,'
|
|
(il).-'' (li).' ((!.-'
|
|
|
|
Download with HTTP-protocol
|
|
|
|
Copyright © 2007-2008 Denis Fateyev (Danger)
|
|
Website: <http://www.fateyev.com>
|
|
E-Mail: <denis@fateyev.com>
|
|
|
|
'ParseURL' and 'Posn' functions are copyright (C) 1997-2001 by Francois Piette
|
|
"Permission is granted to anyone to use this software for any purpose, including
|
|
commercial applications, and to alter it and redistribute it freely." }
|
|
|
|
{* TKOLHTTPDownload is the non-visual component that provides a downloading resources with HTTP-protocol. Now uses WinInet routines.
|
|
|<pre>
|
|
|Copyright (C) 2007-2008 Denis Fateyev (Danger) (<a href="mailto:denis@fateyev.com">denis@fateyev.com</a>).
|
|
|</pre>
|
|
|TKOLHTTPDownload coming under the form of a KOL library unit, it can be simply used
|
|
by creating object at runtime, setting the necessary properties:
|
|
!uses Windows, Messages, KOL, ..., KOLHTTPDownload;
|
|
! //...
|
|
!var DL : PHTTPDownload;
|
|
! //...
|
|
!DL := NewHTTPDownload;
|
|
!DL.OnDownload:= MyDownload_Proc;
|
|
!DL.GetResource( 'http://example.com/foo/bar.zip' );
|
|
!DL. ...
|
|
!DL.Free;
|
|
|<p>Certainly you can use the 'MCK mirror' provided with component to manage control properties at design time. }
|
|
|
|
interface
|
|
|
|
// This conditional define allows some manupulations with HTTP-headers,
|
|
// you can disable it (if you really don't need it) by commenting the following line.
|
|
{$DEFINE USE_CUSTOMHEADERS}
|
|
|
|
//-----------------------------------------------------
|
|
uses
|
|
Windows, WinInet, KOL;
|
|
|
|
//-----------------------------------------------------
|
|
const
|
|
iDefProxyPort = 3128;
|
|
iTimeOutValue = 200; // 0.2 sec
|
|
iDataBufSize = 4096; // 4 KByte buffer
|
|
strUserAgent = 'Dangers HTTPClient/2.1';
|
|
strConnectType = 'Connection: close';
|
|
strProxyConnectType = 'Proxy-Connection: close';
|
|
|
|
//-----------------------------------------------------
|
|
{ THTTPHeader }
|
|
|
|
type
|
|
PHTTPHeader = ^THTTPHeader;
|
|
THTTPHeader = record
|
|
{* |<p>Most important values that can be extracted from http-servers response
|
|
|(see <a href="thttpdownload.htm#parseheaders">ParseHeaders</a> procedure
|
|
|below for more details).</p> }
|
|
HTTPVersion: KOLstring;
|
|
StatusCode: Integer;
|
|
ReasonPhrase: KOLstring;
|
|
ServerDate: KOLstring;
|
|
ServerStr: KOLstring;
|
|
LastModified: KOLstring;
|
|
Location: KOLstring;
|
|
SetCookie: KOLstring;
|
|
Expires: KOLstring;
|
|
ContentLength: Integer;
|
|
TransferEncoding: KOLstring;
|
|
ContentType: KOLstring;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
{ THTTPDownload }
|
|
|
|
PHTTPDownload = ^THTTPDownload;
|
|
PDownloadWorker = ^TDownloadWorker;
|
|
TKOLHTTPDownload = PHTTPDownload;
|
|
THTTPHdrRecvEvent = procedure( Sender: PHTTPDownload; HeaderList: PStrList ) of object;
|
|
{* |Event to be called when http-headers received from http-server. }
|
|
|
|
THTTPProgressEvent = procedure( Sender: PHTTPDownload;
|
|
BytesRecv: Integer; BytesAll: Integer ) of object;
|
|
{* |Event to be called when download progress is changed. }
|
|
|
|
THTTPErrorEvent = procedure( Sender: PHTTPDownload; Error: Word ) of object;
|
|
{* |Event to be called when error occured while download process. }
|
|
|
|
THTTPDownloadEvent = procedure( Sender: PHTTPDownload; Data: PStream ) of object;
|
|
{* |Event to be called when resource download completed. }
|
|
|
|
THTTPDownload = object( TObj )
|
|
{* |This object implements all functionality of component.<br>
|
|
|Use <i>NewHTTPDownload</i> constuction function for creation of object instance. Here is the prototype:
|
|
! function NewHTTPDownload: PHTTPDownload; }
|
|
private
|
|
fWorker: PDownloadWorker;
|
|
fHeaderList: PStrList;
|
|
{$IFDEF USE_CUSTOMHEADERS}
|
|
fCHeaderList: PStrList;
|
|
{$ENDIF}
|
|
fDataStream: PStream;
|
|
fResource: string;
|
|
fBusy: Boolean;
|
|
fPort: Word;
|
|
fHostName: string;
|
|
fPath: string;
|
|
fUserName: string;
|
|
fPassword: string;
|
|
fProxySrv: string;
|
|
fProxyPort: Word;
|
|
fPreConfigProxy: Boolean;
|
|
|
|
fOnError: THTTPErrorEvent;
|
|
fOnHeaderReceived: THTTPHdrRecvEvent;
|
|
fOnProgress: THTTPProgressEvent;
|
|
fOnDownload: THTTPDownloadEvent;
|
|
|
|
public
|
|
function CheckConnection( AResourceName: string ): Boolean;
|
|
{* Simple check if a connection to host that provides specified resource can be established,
|
|
and requested resource can be retrieved. By example:
|
|
! CheckConnection( 'http://www.example.com/foo/bar.zip' );
|
|
Note that this function may give the wrong results if destination host doesn't accept 'ping' requests.
|
|
|Return value: <i>True</i> if a connection is made successfully, or <i>False</i> otherwise. }
|
|
|
|
function GetResource( AResourceName: string ): Boolean;
|
|
{* |Initiate download process for the specified resource.<br>
|
|
|The parameter <i>AResourceName</i> must contains full path of the requested resource
|
|
in such syntax:
|
|
! protocol://[user[:password]@]server[:port]/path
|
|
|If parameter <i>port</i> not specified, then <i>standard http-port (80)</i> will be used in request.
|
|
Authorization parameters can be omitted too, if isn't needed.
|
|
In simple case can be used, by example:
|
|
! GetResource( 'http://www.example.com/foo/bar.zip' );
|
|
|Return value: the function returns <i>False</i> if resource request has invalid syntax,
|
|
|otherwise <i>True</i> returned. }
|
|
|
|
procedure SetProxySettings( AProxyServer: string; iProxyPort: Integer = iDefProxyPort );
|
|
{* |Proxy settings for the resource request.<br>
|
|
|<i>iProxyPort</i> parameter can be omitted then <i>standard proxy port (3128)</i> will be used. }
|
|
|
|
procedure SetAuthInfo( AUserName: string; APassword: string );
|
|
{* Authorization parameters for the resource request. }
|
|
|
|
function ParseHeaders( var Header: PHTTPHeader ): Boolean;
|
|
{* Extract http-headers information and put into the specified HTTPHeader. By example:
|
|
!var
|
|
! DL: PHTTPDownload;
|
|
! Header: PHTTPHeader;
|
|
!// ...
|
|
!procedure TForm1.DLHeaderReceived( Sender: PHTTPDownload; HeaderList: PStrList );
|
|
!begin
|
|
! New( Header );
|
|
! DL.ParseHeaders( Header );
|
|
! // ... do something with Header ...
|
|
! MsgOk( Header.ReasonPhrase );
|
|
! // ...
|
|
! Dispose( Header );
|
|
!end;
|
|
|Return value: <i>False</i> if http-headers doesn't exists (nothing to analyze). }
|
|
|
|
{$IFDEF USE_CUSTOMHEADERS}
|
|
procedure AddCustomHeader( AHeader: string );
|
|
{* |Add custom line to requests http-header. By example:
|
|
!var
|
|
! DL: PHTTPDownload;
|
|
!// ...
|
|
!procedure TForm1.Button1Click( Sender: PObj );
|
|
!begin
|
|
! DL.AddCustomHeader( 'Cookie: PHPSESSID=abcdef' );
|
|
! DL.GetResource( 'http://www.example.com/foo/bar.zip' );
|
|
!end;
|
|
Once assigned these headers will be added automatically to each request sent to http-server
|
|
(while the current THTTPDownload object is in use). Custom headers are not assigned by default.
|
|
|To clear user defined http-headers list, call <i>ClearCustomHeaders</i> procedure.
|
|
|Note that <i>'Connection: close'</i> or <i>'Proxy-Connection: close'</i> (depends on connection type)
|
|
|will be included in the request headers anyway.<br>
|
|
|You must add <b>USE_CUSTOMHEADERS</b> conditional symbol into the project options list. }
|
|
|
|
procedure SetCustomHeaders( AHeaderList: PStrList );
|
|
{* |Assign the custom http-headers list from another one. By example:
|
|
!var
|
|
! DL: PHTTPDownload;
|
|
! CList: PStrList;
|
|
!// ...
|
|
!procedure TForm1.Button1Click( Sender: PObj );
|
|
!begin
|
|
! CList:= NewStrList;
|
|
! CList.Add( 'Cookie: PHPSESSID=abcdef' );
|
|
! DL.SetCustomHeaders( CList );
|
|
! DL.GetResource( 'http://www.example.com/foo/bar.zip' );
|
|
! CList.Free;
|
|
!end;
|
|
|You must add <b>USE_CUSTOMHEADERS</b> conditional symbol into the project options list. }
|
|
|
|
procedure ClearCustomHeaders;
|
|
{* |Clear user defined http-headers list (restore to defaults).
|
|
|You must add <b>USE_CUSTOMHEADERS</b> conditional symbol into the project options list. }
|
|
{$ENDIF}
|
|
|
|
procedure CancelDownload;
|
|
{* |Drop current download process immediately. }
|
|
|
|
property Resource: string read fResource;
|
|
{* |Currently requested resource. By default: <i>None.</i> }
|
|
|
|
property ProxyServer: string read fProxySrv write fProxySrv;
|
|
{* |IP-address or hostname of http-proxy server. By default: <i>None.</i> }
|
|
|
|
property ProxyPort: Word read fProxyPort write fProxyPort;
|
|
{* |TCP Port of http-proxy server. By default: <i>3128.</i> }
|
|
|
|
property UserName: string read fUserName write fUserName;
|
|
{* |HTTP Autorization parameters: username. By default: <i>None.</i> }
|
|
|
|
property Password: string read fPassword write fPassword;
|
|
{* |HTTP Autorization parameters: password. By default: <i>None.</i> }
|
|
|
|
property UsePreconfigProxy: Boolean read fPreConfigProxy write fPreConfigProxy;
|
|
{*|Parameter that allows to use connection settings stored in Internet Explorer.
|
|
Retrieves the proxy or direct configuration from the Windows registry.
|
|
|By default: <i>False.</i> }
|
|
|
|
property HeaderList: PStrList read fHeaderList;
|
|
{*|Retrieves all received http-headers in raw format (as is).
|
|
Most important parameters can be retrieved with ParseHeaders procedure. }
|
|
|
|
{$IFDEF USE_CUSTOMHEADERS}
|
|
property CustomHeaderList: PStrList read fCHeaderList;
|
|
{*|Retrieves custom http-header list assigned by user.
|
|
See SetCustomHeaders procedure for more details. }
|
|
{$ENDIF}
|
|
|
|
property ReceivedData: PStream read fDataStream;
|
|
{*|Retrieves downloaded resource if present. }
|
|
|
|
property Busy: Boolean read fBusy;
|
|
{*| If <i>True</i>, the object is busy and resource download is in progress at the moment.
|
|
If you wish, you can terminate download process at any moment with CancelDownload procedure. }
|
|
|
|
property OnError: THTTPErrorEvent read fOnError write fOnError;
|
|
{* |Event to be called when error occured while download process. }
|
|
|
|
property OnHeaderReceived: THTTPHdrRecvEvent read fOnHeaderReceived write fOnHeaderReceived;
|
|
{* |Event to be called when http-headers received from http-server. }
|
|
|
|
property OnProgress: THTTPProgressEvent read fOnProgress write fOnProgress;
|
|
{* |Event to be called when download progress is changed.
|
|
Note that there's no way to automatically determine the whole size of requested resource
|
|
|if <i>'Content-Length'</i> field is missing in the http-header (i.e. if <i>Transfer-Encoding</i>
|
|
|header field (rfc-2068 section 14.40) is present and indicates that the <i>"chunked"</i> transfer
|
|
|coding has been applied). Therefore, if <i>'Content-Length'</i> is present, <i>BytesAll</i>
|
|
|parameter indicates the requested resource size, otherwise it's equal to <i>'-1'</i>. }
|
|
|
|
property OnDownload: THTTPDownloadEvent read fOnDownload write fOnDownload;
|
|
{* |Event to be called when resource download completed. }
|
|
|
|
destructor Destroy; virtual;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
{ TDownloadWorker }
|
|
|
|
TDownloadWorker = object (TObj )
|
|
private
|
|
// Contains parent object's pointer (or NIL if download terminated)
|
|
fOwner: PHTTPDownload;
|
|
fWThread: PThread;
|
|
fDLThread: PThread;
|
|
fCritSection: TRTLCriticalSection;
|
|
fDataBuf: PChar;
|
|
fPort: Word;
|
|
fHostName: string;
|
|
fPath: string;
|
|
fUserName: string;
|
|
fPassword: string;
|
|
fProxySrv: string;
|
|
fProxyPort: Word;
|
|
fPreConfigProxy: Boolean;
|
|
iContentLen: Integer;
|
|
iReadCount: Integer;
|
|
|
|
function On_DownloadExecute( Sender: PThread ): Integer;
|
|
function On_WatchExecute( Sender: PThread ): Integer;
|
|
procedure On_UpdateProgress;
|
|
|
|
public
|
|
procedure StartDownload;
|
|
function StopDownload: Integer;
|
|
destructor Destroy; virtual;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function NewHTTPDownload: PHTTPDownload;
|
|
function NewDownloadWorker( AOwner: PHTTPDownload ): PDownloadWorker;
|
|
//-----------------------------------------------------
|
|
|
|
implementation
|
|
|
|
//-----------------------------------------------------
|
|
function NewHTTPDownload: PHTTPDownload;
|
|
begin
|
|
New( Result, Create );
|
|
with ( Result^ ) do
|
|
begin
|
|
fBusy:= false;
|
|
fPreConfigProxy:= false;
|
|
fProxyPort:= iDefProxyPort;
|
|
end;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function NewDownloadWorker( AOwner: PHTTPDownload ): PDownloadWorker;
|
|
begin
|
|
New( Result, Create );
|
|
with ( Result^ ) do
|
|
begin
|
|
fOwner:= AOwner;
|
|
InitializeCriticalSection( fCritSection );
|
|
end;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function StrPas(const Str: PChar): string;
|
|
begin
|
|
Result:= Str;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
{ Find the count'th occurence of the s string in the t string. }
|
|
{ If count < 0 then look from the back }
|
|
function Posn(const s , t : String; Count : Integer) : Integer;
|
|
var
|
|
i, h, Last : Integer;
|
|
u : String;
|
|
begin
|
|
u := t;
|
|
if Count > 0 then
|
|
begin
|
|
Result := Length(t);
|
|
for i := 1 to Count do
|
|
begin
|
|
h := Pos(s, u);
|
|
if h > 0 then
|
|
u := Copy(u, h + 1, Length(u))
|
|
else
|
|
begin
|
|
u := '';
|
|
Inc(Result);
|
|
end;
|
|
end;
|
|
Result := Result - Length(u);
|
|
end
|
|
else if Count < 0 then
|
|
begin
|
|
Last := 0;
|
|
for i := Length(t) downto 1 do
|
|
begin
|
|
u := Copy(t, i, Length(t));
|
|
h := Pos(s, u);
|
|
if (h <> 0) and ((h + i) <> Last) then
|
|
begin
|
|
Last := h + i - 1;
|
|
Inc(count);
|
|
if Count = 0 then
|
|
break;
|
|
end;
|
|
end;
|
|
if Count = 0 then
|
|
Result := Last
|
|
else
|
|
Result := 0;
|
|
end
|
|
else
|
|
Result := 0;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
{ Syntax of an URL: protocol://[user[:password]@]server[:port]/path }
|
|
procedure ParseURL(const url : String; var Proto, User, Pass, Host, Port, Path : String);
|
|
var
|
|
p, q : Integer;
|
|
s : String;
|
|
CurPath : String;
|
|
begin
|
|
CurPath := Path;
|
|
proto := '';
|
|
User := '';
|
|
Pass := '';
|
|
Host := '';
|
|
Port := '';
|
|
Path := '';
|
|
|
|
if Length(url) < 1 then Exit;
|
|
|
|
{ Handle path beginning with "./" or "../". }
|
|
{ This code handle only simple cases ! }
|
|
{ Handle path relative to current document directory }
|
|
if (Copy(url, 1, 2) = './') then
|
|
begin
|
|
p := Posn('/', CurPath, -1);
|
|
if p > Length(CurPath) then
|
|
p := 0;
|
|
if p = 0 then
|
|
CurPath := '/'
|
|
else
|
|
CurPath := Copy(CurPath, 1, p);
|
|
Path := CurPath + Copy(url, 3, Length(url));
|
|
Exit;
|
|
end
|
|
{ Handle path relative to current document parent directory }
|
|
else if (Copy(url, 1, 3) = '../') then
|
|
begin
|
|
p := Posn('/', CurPath, -1);
|
|
if p > Length(CurPath) then
|
|
p := 0;
|
|
if p = 0 then
|
|
CurPath := '/'
|
|
else
|
|
CurPath := Copy(CurPath, 1, p);
|
|
|
|
s := Copy(url, 4, Length(url));
|
|
{ We could have several levels }
|
|
while TRUE do
|
|
begin
|
|
CurPath := Copy(CurPath, 1, p-1);
|
|
p := Posn('/', CurPath, -1);
|
|
if p > Length(CurPath) then
|
|
p := 0;
|
|
if p = 0 then
|
|
CurPath := '/'
|
|
else
|
|
CurPath := Copy(CurPath, 1, p);
|
|
if (Copy(s, 1, 3) <> '../') then
|
|
break;
|
|
s := Copy(s, 4, Length(s));
|
|
end;
|
|
|
|
Path := CurPath + Copy(s, 1, Length(s));
|
|
Exit;
|
|
end;
|
|
|
|
p := pos('://',url);
|
|
if p = 0 then
|
|
begin
|
|
if (url[1] = '/') then
|
|
begin
|
|
{ Relative path without protocol specified }
|
|
proto := 'http';
|
|
p := 1;
|
|
if (Length(url) > 1) and (url[2] <> '/') then
|
|
begin
|
|
{ Relative path }
|
|
Path := Copy(url, 1, Length(url));
|
|
Exit;
|
|
end;
|
|
end
|
|
else if lowercase(Copy(url, 1, 5)) = 'http:' then
|
|
begin
|
|
proto := 'http';
|
|
p := 6;
|
|
if (Length(url) > 6) and (url[7] <> '/') then
|
|
begin
|
|
{ Relative path }
|
|
Path := Copy(url, 6, Length(url));
|
|
Exit;
|
|
end;
|
|
end
|
|
else if lowercase(Copy(url, 1, 7)) = 'mailto:' then
|
|
begin
|
|
proto := 'mailto';
|
|
p := pos(':', url);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
proto := Copy(url, 1, p - 1);
|
|
inc(p, 2);
|
|
end;
|
|
s := Copy(url, p + 1, Length(url));
|
|
|
|
p := pos('/', s);
|
|
q := pos('?', s);
|
|
if (q > 0) and ((q < p) or (p = 0)) then
|
|
p := q;
|
|
if p = 0 then
|
|
p := Length(s) + 1;
|
|
Path := Copy(s, p, Length(s));
|
|
s := Copy(s, 1, p-1);
|
|
|
|
p := Posn(':', s, -1);
|
|
if p > Length(s) then
|
|
p := 0;
|
|
q := Posn('@', s, -1);
|
|
if q > Length(s) then
|
|
q := 0;
|
|
if (p = 0) and (q = 0) then
|
|
begin { no user, password or port }
|
|
Host := s;
|
|
Exit;
|
|
end
|
|
else if q < p then
|
|
begin { a port given }
|
|
Port := Copy(s, p + 1, Length(s));
|
|
Host := Copy(s, q + 1, p - q - 1);
|
|
if q = 0 then
|
|
Exit; { no user, password }
|
|
s := Copy(s, 1, q - 1);
|
|
end
|
|
else
|
|
begin
|
|
Host := Copy(s, q + 1, Length(s));
|
|
s := Copy(s, 1, q - 1);
|
|
end;
|
|
p := pos(':', s);
|
|
if p = 0 then
|
|
User := s
|
|
else
|
|
begin
|
|
User := Copy(s, 1, p - 1);
|
|
Pass := Copy(s, p + 1, Length(s));
|
|
end;
|
|
end;
|
|
|
|
//---------------- { THTTPDownload } -------------------------------
|
|
|
|
function THTTPDownload.ParseHeaders( var Header: PHTTPHeader ): Boolean;
|
|
var
|
|
i: Integer; S: KOLstring;
|
|
begin
|
|
Result:= false;
|
|
|
|
if ( not Assigned( fHeaderList ) ) then Exit;
|
|
// HTTP/1.1 200 OK
|
|
Header.ReasonPhrase:= fHeaderList.Items[0];
|
|
Header.HTTPVersion:= Parse( Header.ReasonPhrase, ' ' );
|
|
Header.StatusCode:= Str2Int( Parse(Header.ReasonPhrase, ' ') );
|
|
// avoid curious things if value isn't present in the list
|
|
Header.ContentLength:= -1;
|
|
// begin from second list item
|
|
for i:= 2 to fHeaderList.Count do
|
|
begin
|
|
S:= fHeaderList.Items[i-1];
|
|
// Date: Wed, 09 May 2007 14:31:23 GMT
|
|
if ( Pos('Date: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.ServerDate:= S;
|
|
Continue;
|
|
end;
|
|
// Server: Apache x.x.x (Unix)
|
|
if ( Pos('Server: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.ServerStr:= S;
|
|
Continue;
|
|
end;
|
|
// Last-Modified: Wed, 09 May 2007 14:31:23 GMT
|
|
if ( Pos('Last-Modified: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.LastModified:= S;
|
|
Continue;
|
|
end;
|
|
// Set-Cookie: PHPSESSID=xxxxxxxxx
|
|
if ( Pos('Set-Cookie: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.SetCookie:= S;
|
|
Continue;
|
|
end;
|
|
// Expires: Wed, 10 May 2007 14:31:23 GMT
|
|
if ( Pos('Expires: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.Expires:= S;
|
|
Continue;
|
|
end;
|
|
// Location: foobar.html
|
|
if ( Pos('Location: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.Location:= S;
|
|
Continue;
|
|
end;
|
|
// Content-Length: 12345
|
|
if ( Pos('Content-Length: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.ContentLength:= Str2Int( S );
|
|
Continue;
|
|
end;
|
|
// Transfer-Encoding: chunked
|
|
if ( Pos('Transfer-Encoding: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.TransferEncoding:= S;
|
|
Continue;
|
|
end;
|
|
// Content-Type: application/zip
|
|
if ( Pos('Content-Type: ', S) > 0 ) then
|
|
begin
|
|
Parse(S, ' '); Header.ContentType:= S;
|
|
Continue;
|
|
end;
|
|
end;
|
|
|
|
Result:= true;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure THTTPDownload.SetProxySettings( AProxyServer: string; iProxyPort: Integer = iDefProxyPort );
|
|
begin
|
|
fProxySrv:= AProxyServer;
|
|
fProxyPort:= iProxyPort;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure THTTPDownload.SetAuthInfo( AUserName: string; APassword: string );
|
|
begin
|
|
fUserName:= AUserName;
|
|
fPassword:= APassword;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
{$IFDEF USE_CUSTOMHEADERS}
|
|
procedure THTTPDownload.AddCustomHeader( AHeader: string );
|
|
begin
|
|
if ( Length( AHeader ) > 0 ) then
|
|
begin
|
|
if ( not Assigned( fCHeaderList ) ) then
|
|
begin
|
|
fCHeaderList:= NewStrList;
|
|
fCHeaderList.Add2AutoFree( @Self );
|
|
end; // 'if ( not Assigned( fCHeaderList )'
|
|
fCHeaderList.Add( AHeader );
|
|
end;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure THTTPDownload.ClearCustomHeaders;
|
|
begin
|
|
if Assigned( fCHeaderList ) then fCHeaderList.Clear;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure THTTPDownload.SetCustomHeaders( AHeaderList: PStrList );
|
|
begin
|
|
if Assigned( AHeaderList ) then
|
|
begin
|
|
if ( not Assigned( fCHeaderList ) ) then
|
|
begin
|
|
fCHeaderList:= NewStrList;
|
|
fCHeaderList.Add2AutoFree( @Self );
|
|
end; // 'if ( not Assigned( fCHeaderList )'
|
|
fCHeaderList.Assign( AHeaderList );
|
|
end;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
//-----------------------------------------------------
|
|
function THTTPDownload.CheckConnection( AResourceName: string ): Boolean;
|
|
begin
|
|
Result:= false;
|
|
|
|
// I'm wondering why FLAG_ICC_FORCE_CONNECTION declaration is missing in WinInet.pas
|
|
if ( InternetCheckConnection( PChar( AResourceName ), $00000001 {FLAG_ICC_FORCE_CONNECTION}, 0 ) ) then
|
|
Result:= true
|
|
else
|
|
if Assigned( fOnError ) then fOnError( @Self, GetLastError );
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function THTTPDownload.GetResource( AResourceName: string ): Boolean;
|
|
var
|
|
strPort, strProto: string;
|
|
begin
|
|
Result:= false;
|
|
CancelDownload;
|
|
|
|
if ( not fBusy ) then
|
|
begin
|
|
fResource:= AResourceName;
|
|
// checking request data
|
|
ParseURL( fResource, strProto, fUserName, fPassword, fHostName, strPort, fPath );
|
|
if ( strProto = '' ) then strProto:= 'http';
|
|
if ( ( fHostName = '' ) or ( fPath = '' ) or ( strProto <> 'http' ) ) then
|
|
begin
|
|
if Assigned( fOnError ) then fOnError( @Self, ERROR_INTERNET_INVALID_URL );
|
|
Exit;
|
|
end;
|
|
if ( strPort = '' ) then fPort:= INTERNET_DEFAULT_HTTP_PORT
|
|
else fPort:= Str2Int( strPort );
|
|
|
|
if Assigned( fOnHeaderReceived ) then
|
|
if ( not Assigned( fHeaderList ) ) then
|
|
begin
|
|
fHeaderList:= NewStrList;
|
|
fHeaderList.Add2AutoFree( @Self );
|
|
end;
|
|
|
|
if Assigned( fOnDownload ) then
|
|
begin
|
|
if ( not Assigned( fDataStream ) ) then
|
|
begin
|
|
fDataStream:= NewMemoryStream;
|
|
fDataStream.Add2AutoFree( @Self );
|
|
end
|
|
else fDataStream.Size:= 0;
|
|
end;
|
|
|
|
fBusy:= true;
|
|
fWorker:= NewDownloadWorker( @Self );
|
|
fWorker.StartDownload;
|
|
Result:= true;
|
|
end;
|
|
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure THTTPDownload.CancelDownload;
|
|
begin
|
|
if ( fBusy ) then
|
|
fWorker.StopDownload;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
destructor THTTPDownload.Destroy;
|
|
begin
|
|
CancelDownload;
|
|
fResource:= '';
|
|
fHostName:= '';
|
|
fPath:= '';
|
|
fProxySrv:= '';
|
|
fUserName:= '';
|
|
fPassword:= '';
|
|
inherited;
|
|
end;
|
|
|
|
//---------------- { TDownloadWorker } -------------------------------
|
|
|
|
procedure TDownloadWorker.StartDownload;
|
|
begin
|
|
fWThread:= NewThread;
|
|
fWThread.OnExecute:= On_WatchExecute;
|
|
fWThread.Add2AutoFree( @Self );
|
|
fWThread.Resume;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function TDownloadWorker.On_WatchExecute( Sender: PThread ): Integer;
|
|
begin
|
|
Result:= 0; // stub
|
|
|
|
// create download working thread
|
|
fDLThread:= NewThreadEx( On_DownloadExecute );
|
|
// wait for download thread finished (any way)
|
|
fDLThread.WaitFor;
|
|
// destroy worker object
|
|
Free;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function TDownloadWorker.StopDownload: Integer;
|
|
var
|
|
lpOwner: PHTTPDownload;
|
|
begin
|
|
Result:= 0; // stub
|
|
|
|
lpOwner:= nil; // avoid compiler warning
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if Assigned( fOwner ) then
|
|
begin
|
|
lpOwner:= PHTTPDownload( fOwner );
|
|
fOwner:= nil;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// trying to terminate thread gracefully
|
|
if ( not fDLThread.Terminated ) then fDLThread.WaitForTime( iTimeOutValue );
|
|
// terminate thread forcefully
|
|
if ( not fDLThread.Terminated ) then fDLThread.Terminate;
|
|
|
|
if Assigned( lpOwner ) then
|
|
begin
|
|
// don't keep partially downloaded file
|
|
if Assigned( lpOwner.fDataStream ) then
|
|
lpOwner.fDataStream.Size:= 0;
|
|
lpOwner.fBusy:= false;
|
|
end;
|
|
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
procedure TDownloadWorker.On_UpdateProgress;
|
|
begin
|
|
if Assigned( fOwner ) then
|
|
fOwner.OnProgress( fOwner, iReadCount, iContentLen );
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
function TDownloadWorker.On_DownloadExecute( Sender: PThread ): Integer;
|
|
var
|
|
hSession, hConnect, hRequest: HINTERNET;
|
|
iBufSize, lpdwIndex, iNumRead: Cardinal;
|
|
Buf: PChar; i, iErrorCode: Integer;
|
|
|
|
procedure CloseHandles;
|
|
begin
|
|
InternetCloseHandle( hRequest );
|
|
InternetCloseHandle( hConnect );
|
|
InternetCloseHandle( hSession );
|
|
end;
|
|
|
|
begin
|
|
Result:= 0; // stub
|
|
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if Assigned( fOwner ) then
|
|
begin
|
|
fHostName:= fOwner.fHostName;
|
|
fPath:= fOwner.fPath;
|
|
fPort:= fOwner.fPort;
|
|
fUserName:= fOwner.fUserName;
|
|
fPassword:= fOwner.fPassword;
|
|
fPreConfigProxy:= fOwner.fPreConfigProxy;
|
|
if ( not fPreConfigProxy ) then
|
|
begin
|
|
fProxySrv:= fOwner.fProxySrv;
|
|
fProxyPort:= fOwner.fProxyPort;
|
|
end;
|
|
end // 'if Assigned( fOwner ) then'
|
|
else Exit;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// initializing Wininet, settings some connection parameters
|
|
if ( fPreConfigProxy ) then
|
|
hSession:= InternetOpen( strUserAgent, INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0 )
|
|
else
|
|
begin
|
|
if ( fProxySrv <> '' ) then
|
|
hSession:= InternetOpen( strUserAgent, INTERNET_OPEN_TYPE_PROXY,
|
|
PChar( 'http=' + fProxySrv + ':' + Int2Str( fProxyPort) ), nil, 0 )
|
|
else
|
|
hSession:= InternetOpen( strUserAgent, INTERNET_OPEN_TYPE_DIRECT, nil, nil, 0 );
|
|
end;
|
|
if ( hSession = nil ) then
|
|
begin
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
Exit;
|
|
end;
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then Exit;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// connecting to http-server
|
|
hConnect:= InternetConnect( hSession, PChar( fHostName ), fPort,
|
|
PChar( fUserName ), PChar( fPassword ), INTERNET_SERVICE_HTTP, 0, 0 );
|
|
if ( hConnect = nil ) then
|
|
begin
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// prepare resource request to http-server
|
|
// we're prefer HTTP/1.0 version but this parameter can be ignored by Wininet
|
|
// see KB258425 (http://support.microsoft.com/kb/258425) for more details.
|
|
hRequest:= HttpOpenRequest( hConnect, nil, PChar( fPath ), nil,
|
|
nil, nil, INTERNET_FLAG_NO_UI + INTERNET_FLAG_PRAGMA_NOCACHE, 0);
|
|
if ( hRequest = nil ) then
|
|
begin
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
// adding custom http headers to request
|
|
{$IFDEF USE_CUSTOMHEADERS}
|
|
with ( fOwner^ ) do
|
|
if Assigned( fCHeaderList ) then
|
|
with ( fCHeaderList^ ) do
|
|
if ( Count > 0 ) then
|
|
for i:= 1 to Count do
|
|
HttpAddRequestHeaders( hRequest, PChar( Items[i-1] ), Length( Items[i-1] ), HTTP_ADDREQ_FLAG_ADD );
|
|
{$ENDIF}
|
|
// setting http headers 'connection type' field (don't allow persistent connection)
|
|
if ( fPreConfigProxy or ( fProxySrv <> '' ) ) then
|
|
HttpAddRequestHeaders( hRequest, strProxyConnectType, Length( strProxyConnectType ), HTTP_ADDREQ_FLAG_ADD )
|
|
else
|
|
HttpAddRequestHeaders( hRequest, strConnectType, Length( strConnectType ), HTTP_ADDREQ_FLAG_ADD );
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// send http request to server
|
|
if ( not HttpSendRequest( hRequest, nil, 0, nil, 0 ) ) then
|
|
begin
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// receiving headers (if event assigned)
|
|
if Assigned( fOwner.fOnHeaderReceived ) then
|
|
begin
|
|
lpdwIndex:= 0; Buf:= nil;
|
|
HttpQueryInfo( hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, Buf, iBufSize, lpdwIndex );
|
|
// NB: it's ok when 'unsufficient buffer' message received now
|
|
iErrorCode:= GetLastError;
|
|
|
|
if ( iErrorCode = ERROR_INSUFFICIENT_BUFFER ) then
|
|
begin
|
|
GetMem( Buf, iBufSize );
|
|
lpdwIndex:= 0;
|
|
try
|
|
if ( HttpQueryInfo( hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, Buf, iBufSize, lpdwIndex ) ) then
|
|
with ( fOwner^ ) do
|
|
begin
|
|
fHeaderList.SetText( Buf, false );
|
|
with ( fHeaderList^ ) do
|
|
if ( Items[Count-1] = '' ) then Delete( Count-1 );
|
|
fOnHeaderReceived( fOwner, fHeaderList );
|
|
end // 'if ( HttpQueryInfo( hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, Buf, iBufSize, iReserved ) )'
|
|
else
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
|
|
finally
|
|
FreeMem( Buf );
|
|
end;
|
|
end // 'if ( iErrorCode = ERROR_INSUFFICIENT_BUFFER )'
|
|
else
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, iErrorCode );
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
end; // 'if Assigned( fOnHeaderReceived )'
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// getting http status code
|
|
iBufSize:= 16;
|
|
iErrorCode:= 0;
|
|
lpdwIndex:= 0;
|
|
GetMem( Buf, iBufSize );
|
|
try
|
|
if ( HttpQueryInfo( hRequest, HTTP_QUERY_STATUS_CODE, Buf, iBufSize, lpdwIndex ) ) then
|
|
iErrorCode:= Str2Int( StrPas( Buf ) )
|
|
else
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, GetLastError );
|
|
finally
|
|
FreeMem( Buf );
|
|
end;
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// checking if resource is available
|
|
if ( ( Assigned( fOwner.fOnDownload ) and ( iErrorCode = HTTP_STATUS_OK {HTTP/1.1 200 OK} ) ) ) then
|
|
begin
|
|
iBufSize:= 16;
|
|
lpdwIndex:= 0;
|
|
iContentLen:= 0;
|
|
GetMem( Buf, iBufSize );
|
|
try
|
|
if ( HttpQueryInfo( hRequest, HTTP_QUERY_CONTENT_LENGTH, Buf, iBufSize, lpdwIndex ) ) then
|
|
// getting http content length
|
|
iContentLen:= Str2Int( StrPas( Buf ) );
|
|
// set iContentLen value to '-1' if not present or invalid
|
|
if ( iContentLen <= 0 ) then iContentLen:= -1;
|
|
|
|
iReadCount:= 0;
|
|
GetMem( fDataBuf, iDataBufSize );
|
|
try
|
|
// downloading resource
|
|
with ( fOwner^ ) do
|
|
while ( InternetReadFile( hRequest, fDataBuf, iDataBufSize, iNumRead ) ) do
|
|
if ( iNumRead > 0 ) then
|
|
begin
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then Break;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// write received data to stream
|
|
fDataStream.Write( fDataBuf^, iNumRead );
|
|
Inc( iReadCount, iNumRead );
|
|
// update download progress
|
|
if Assigned( fOnProgress ) then fDLThread.Synchronize( On_UpdateProgress );
|
|
end
|
|
// 'if ( iNumRead > 0 )'
|
|
else Break;
|
|
|
|
// checking if thread must be terminated
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if ( not Assigned( fOwner ) ) then
|
|
begin
|
|
CloseHandles;
|
|
Exit;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
|
|
// download complete
|
|
with ( fOwner^ ) do
|
|
begin
|
|
fDataStream.Position:= 0;
|
|
// call assigned event handler
|
|
fOnDownload( @Self, fDataStream );
|
|
end;
|
|
|
|
finally
|
|
FreeMem( fDataBuf );
|
|
end;
|
|
|
|
finally
|
|
FreeMem( Buf );
|
|
end;
|
|
|
|
end // 'if ( ( Assigned( fOnDownload ) and ( iErrorCode = HTTP_STATUS_OK {HTTP/1.1 200 OK} ) ) )'
|
|
else
|
|
if ( iErrorCode <> HTTP_STATUS_OK { HTTP/1.1 OK } ) then
|
|
with ( fOwner^ ) do
|
|
if Assigned( fOnError ) then fOnError( fOwner, ERROR_INTERNET_EXTENDED_ERROR );
|
|
|
|
CloseHandles;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
destructor TDownloadWorker.Destroy;
|
|
begin
|
|
fDLThread.Free;
|
|
fHostName:= '';
|
|
fPath:= '';
|
|
fUserName:= '';
|
|
fPassword:= '';
|
|
fProxySrv:= '';
|
|
EnterCriticalSection( fCritSection );
|
|
try
|
|
if Assigned( fOwner ) then
|
|
fOwner.fBusy:= false;
|
|
finally
|
|
LeaveCriticalSection( fCritSection );
|
|
end;
|
|
DeleteCriticalSection( fCritSection );
|
|
inherited;
|
|
end;
|
|
|
|
//-----------------------------------------------------
|
|
|
|
|
|
end.
|