kolmck/Addons/KOLHTTPDownload.pas

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.