{ $Project$ $Workfile$ $Revision$ $DateUTC$ $Id$ This file is part of the Indy (Internet Direct) project, and is offered under the dual-licensing agreement described on the Indy website. (http://www.indyproject.org/) Copyright: (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved. } { $Log$ } { Rev 1.6 2004.10.27 9:17:46 AM czhower For TIdStrings Rev 1.5 10/26/2004 11:08:08 PM JPMugaas Updated refs. Rev 1.4 13.04.2004 12:56:44 ARybin M$ IE behavior Rev 1.3 2004.02.03 5:45:00 PM czhower Name changes Rev 1.2 2004.01.22 6:09:02 PM czhower IdCriticalSection Rev 1.1 1/22/2004 7:09:58 AM JPMugaas Tried to fix AnsiSameText depreciation. Rev 1.0 11/14/2002 02:16:20 PM JPMugaas Mar-31-2001 Doychin Bondzhev - Changes in the class heirarchy to implement Netscape specification[Netscape], RFC 2109[RFC2109] & 2965[RFC2965] Feb-2001 Doychin Bondzhev - Initial release } unit IdCookie; { Implementation of the HTTP State Management Mechanism as specified in RFC 6265. Author: Remy Lebeau (remy@lebeausoftware.org) Copyright: (c) Chad Z. Hower and The Indy Team. TIdCookie - The base code used in all cookies. REFERENCES ------------------- [RFC6265] Barth, A, "HTTP State Management Mechanism", RFC 6265, April 2011. [DRAFT-ORIGIN-01] Pettersen, Y, "Identifying origin server of HTTP Cookies", Internet-Draft, March 07, 2010. http://www.ietf.org/id/draft-pettersen-cookie-origin-01.txt [DRAFT-COOKIEv2-05] Pettersen, Y, "HTTP State Management Mechanism v2", Internet-Draft, March 07, 2010. http://www.ietf.org/id/draft-pettersen-cookie-v2-05.txt } interface {$I IdCompilerDefines.inc} uses Classes, {$IFDEF HAS_UNIT_Generics_Collections} System.Generics.Collections, {$ENDIF} IdGlobal, IdException, IdGlobalProtocols, IdURI, SysUtils; type { Base Cookie class as described in [RFC6265] } TIdCookie = class(TCollectionItem) protected FDomain: String; FExpires: TDateTime; FHttpOnly: Boolean; FName: String; FPath: String; FSecure: Boolean; FValue: String; FCreatedAt: TDateTime; FHostOnly: Boolean; FLastAccessed: TDateTime; FPersistent: Boolean; function GetIsExpired: Boolean; function GetServerCookie: String; virtual; function GetClientCookie: String; virtual; function GetMaxAge: Int64; public constructor Create(ACollection: TCollection); override; destructor Destroy; override; procedure Assign(Source: TPersistent); override; function IsAllowed(AURI: TIdURI; SecureOnly: Boolean): Boolean; virtual; function ParseClientCookie(const ACookieText: String): Boolean; virtual; function ParseServerCookie(const ACookieText: String; AURI: TIdURI): Boolean; virtual; property ClientCookie: String read GetClientCookie; property CookieName: String read FName write FName; property CookieText: String read GetServerCookie; // {$IFDEF HAS_DEPRECATED}deprecated{$IFDEF HAS_DEPECATED_MSG} 'Use ServerCookie property instead'{$ENDIF};{$ENDIF} property Domain: String read FDomain write FDomain; property Expires: TDateTime read FExpires write FExpires; property HttpOnly: Boolean read FHttpOnly write FHttpOnly; property Path: String read FPath write FPath; property Secure: Boolean read FSecure write FSecure; property ServerCookie: String read GetServerCookie; property Value: String read FValue write FValue; property MaxAge: Int64 read GetMaxAge; property CreatedAt: TDateTime read FCreatedAt write FCreatedAt; property IsExpired: Boolean read GetIsExpired; property HostOnly: Boolean read FHostOnly write FHostOnly; property LastAccessed: TDateTime read FLastAccessed write FLastAccessed; property Persistent: Boolean read FPersistent write FPersistent; end; TIdCookieClass = class of TIdCookie; { The Cookie collection } {$IFDEF HAS_GENERICS_TList} TIdCookieList = TList; {$ELSE} TIdCookieList = class(TList) protected function GetCookie(Index: Integer): TIdCookie; procedure SetCookie(Index: Integer; AValue: TIdCookie); public function IndexOfCookie(ACookie: TIdCookie): Integer; property Cookies[Index: Integer]: TIdCookie read GetCookie write SetCookie; default; end; {$ENDIF} TIdCookieAccess = (caRead, caReadWrite); TIdCookies = class(TOwnedCollection) protected FCookieList: TIdCookieList; FRWLock: TMultiReadExclusiveWriteSynchronizer; function GetCookieByNameAndDomain(const AName, ADomain: string): TIdCookie; function GetCookie(Index: Integer): TIdCookie; procedure SetCookie(Index: Integer; const Value: TIdCookie); public constructor Create(AOwner: TPersistent); destructor Destroy; override; function Add: TIdCookie; reintroduce; function AddCookie(ACookie: TIdCookie; AURI: TIdURI; AReplaceOld: Boolean = True): Boolean; function AddClientCookie(const ACookie: string): TIdCookie; procedure AddClientCookies(const ACookie: string); overload; procedure AddClientCookies(const ACookies: TStrings); overload; function AddServerCookie(const ACookie: string; AURI: TIdURI): TIdCookie; procedure AddServerCookies(const ACookies: TStrings; AURI: TIdURI); procedure AddCookies(ASource: TIdCookies); procedure Assign(ASource: TPersistent); override; procedure Clear; reintroduce; function GetCookieIndex(const AName: string; FirstIndex: Integer = 0): Integer; overload; function GetCookieIndex(const AName, ADomain: string; FirstIndex: integer = 0): Integer; overload; function LockCookieList(AAccessType: TIdCookieAccess): TIdCookieList; procedure UnlockCookieList(AAccessType: TIdCookieAccess); property Cookie[const AName, ADomain: string]: TIdCookie read GetCookieByNameAndDomain; property Cookies[Index: Integer]: TIdCookie read GetCookie write SetCookie; Default; end; EIdCookieError = class(EIdException); function IsDomainMatch(const AUriHost, ACookieDomain: String): Boolean; function IsPathMatch(const AUriPath, ACookiePath: String): Boolean; function CanonicalizeHostName(const AHost: String): String; implementation uses {$IFDEF VCL_XE3_OR_ABOVE} System.Types, {$ENDIF} IdAssignedNumbers, IdResourceStringsProtocols; function GetDefaultPath(const AURL: TIdURI): String; var Idx: Integer; begin { Per RFC 6265, Section 5.1.4: The user agent MUST use an algorithm equivalent to the following algorithm to compute the default-path of a cookie: 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise). For example, if the request-uri contains just a path (and optional query string), then the uri-path is that path (without the %x3F ("?") character or query string), and if the request-uri contains a full absoluteURI, the uri-path is the path component of that URI. 2. If the uri-path is empty or if the first character of the uri- path is not a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. 3. If the uri-path contains no more than one %x2F ("/") character, output %x2F ("/") and skip the remaining steps. 4. Output the characters of the uri-path from the first character up to, but not including, the right-most %x2F ("/"). } if TextStartsWith(AURL.Path, '/') then begin {do not localize} Idx := RPos('/', AURL.Path); {do not localize} if Idx > 1 then begin Result := Copy(AURL.Path, 1, Idx-1); Exit; end; end; Result := '/'; {do not localize} end; function CanonicalizeHostName(const AHost: String): String; begin // TODO: implement this { Per RFC 6265 Section 5.1.2: A canonicalized host name is the string generated by the following algorithm: 1. Convert the host name to a sequence of individual domain name labels. 2. Convert each label that is not a Non-Reserved LDH (NR_LDH) label, to an A-label (see Section 2.3.2.1 of [RFC5890] for the fomer and latter), or to a "punycode label" (a label resulting from the "ToASCII" conversion in Section 4 of [RFC3490]), as appropriate (see Section 6.3 of this specification). 3. Concatentate the resulting labels, separated by a %x2E (".") character. } Result := AHost; end; function IsDomainMatch(const AUriHost, ACookieDomain: String): Boolean; var LHost, LDomain: String; begin { Per RFC 6265 Section 5.1.3: A string domain-matches a given domain string if at least one of the following conditions hold: o The domain string and the string are identical. (Note that both the domain string and the string will have been canonicalized to lower case at this point.) o All of the following conditions hold: * The domain string is a suffix of the string. * The last character of the string that is not included in the domain string is a %x2E (".") character. * The string is a host name (i.e., not an IP address). } Result := False; LHost := CanonicalizeHostName(AUriHost); LDomain := CanonicalizeHostName(ACookieDomain); if (LHost <> '') and (LDomain <> '') then begin if TextIsSame(LHost, LDomain) then begin Result := True; end else if TextEndsWith(LHost, LDomain) then begin if TextEndsWith(Copy(LHost, 1, Length(LHost)-Length(LDomain)), '.') then begin Result := IsHostName(LHost); end; end; end; end; function IsPathMatch(const AUriPath, ACookiePath: String): Boolean; begin { Per RFC 6265 Section 5.1.4: A request-path path-matches a given cookie-path if at least one of the following conditions hold: o The cookie-path and the request-path are identical. o The cookie-path is a prefix of the request-path and the last character of the cookie-path is %x2F ("/"). o The cookie-path is a prefix of the request-path and the first character of the request-path that is not included in the cookie- path is a %x2F ("/") character. } Result := TextIsSame(AUriPath, ACookiePath) or ( TextStartsWith(AUriPath, ACookiePath) and ( TextEndsWith(ACookiePath, '/') or CharEquals(AUriPath, Length(ACookiePath)+1, '/') ) ); end; function IsHTTP(const AProtocol: String): Boolean; begin Result := PosInStrArray(AProtocol, ['http', 'https'], False) <> -1; {do not localize} end; { base functions used for construction of Cookie text } procedure AddCookieProperty(var VCookie: String; const AProperty, AValue: String); begin if Length(AValue) > 0 then begin if Length(VCookie) > 0 then begin VCookie := VCookie + '; '; {Do not Localize} end; // TODO: encode illegal characters? VCookie := VCookie + AProperty + '=' + AValue; {Do not Localize} end; end; procedure AddCookieFlag(var VCookie: String; const AFlag: String); begin if Length(VCookie) > 0 then begin VCookie := VCookie + '; '; { Do not Localize } end; VCookie := VCookie + AFlag; end; { TIdCookieList } {$IFNDEF HAS_GENERICS_TList} function TIdCookieList.GetCookie(Index: Integer): TIdCookie; begin Result := TIdCookie(Items[Index]); end; procedure TIdCookieList.SetCookie(Index: Integer; AValue: TIdCookie); begin Items[Index] := AValue; end; function TIdCookieList.IndexOfCookie(ACookie: TIdCookie): Integer; begin for Result := 0 to Count - 1 do begin if GetCookie(Result) = ACookie then begin Exit; end; end; Result := -1; end; {$ENDIF} { TIdCookie } constructor TIdCookie.Create(ACollection: TCollection); begin inherited Create(ACollection); FCreatedAt := Now; FLastAccessed := FCreatedAt; end; destructor TIdCookie.Destroy; var LCookieList: TIdCookieList; begin try if Assigned(Collection) then begin LCookieList := TIdCookies(Collection).LockCookieList(caReadWrite); try LCookieList.Remove(Self); finally TIdCookies(Collection).UnlockCookieList(caReadWrite); end; end; finally inherited Destroy; end; end; procedure TIdCookie.Assign(Source: TPersistent); var LSource: TIdCookie; begin if Source is TIdCookie then begin LSource := TIdCookie(Source); FDomain := LSource.FDomain; FExpires := LSource.FExpires; FHttpOnly := LSource.FHttpOnly; FName := LSource.FName; FPath := LSource.FPath; FSecure := LSource.FSecure; FValue := LSource.FValue; FCreatedAt := LSource.FCreatedAt; FHostOnly := LSource.FHostOnly; FLastAccessed := LSource.FLastAccessed; FPersistent := LSource.FPersistent; end else begin inherited Assign(Source); end; end; function TIdCookie.IsAllowed(AURI: TIdURI; SecureOnly: Boolean): Boolean; function MatchesHost: Boolean; begin if HostOnly then begin Result := TextIsSame(CanonicalizeHostName(AURI.Host), Domain); end else begin Result := IsDomainMatch(AURI.Host, Domain); end; end; begin // using the algorithm defined in RFC 6265 section 5.4... Result := MatchesHost and IsPathMatch(AURI.Path, Path) and ((not Secure) or (Secure and SecureOnly)) and ((not HttpOnly) or (HttpOnly and IsHTTP(AURI.Protocol))); end; {$IFNDEF HAS_TryStrToInt64} // TODO: move this to IdGlobalProtocols... function TryStrToInt64(const S: string; out Value: Int64): Boolean; {$IFDEF USE_INLINE}inline;{$ENDIF} var E: Integer; begin Val(S, Value, E); Result := E = 0; end; {$ENDIF} function TIdCookie.ParseServerCookie(const ACookieText: String; AURI: TIdURI): Boolean; const cTokenSeparators = '()<>@,;:\"/[]?={} '#9; procedure SplitCookieText(const CookieProp: TStringList; const S: string); var LNameValue, LAttrs, LAttr, LName, LValue: String; LSecs: Int64; LExpiryTime: TDateTime; i: Integer; begin I := Pos(';', ACookieText); if I > 0 then begin LNameValue := Copy(ACookieText, 1, I-1); LAttrs := Copy(ACookieText, I, MaxInt); end else begin LNameValue := ACookieText; LAttrs := ''; end; I := Pos('=', LNameValue); if I = 0 then begin Exit; end; LName := Trim(Copy(LNameValue, 1, I-1)); if LName = '' then begin Exit; end; LValue := Trim(Copy(LNameValue, I+1, MaxInt)); if TextStartsWith(LValue, '"') then begin IdDelete(LValue, 1, 1); LNameValue := LValue; LValue := Fetch(LNameValue, '"'); end; CookieProp.Add(LName + '=' + LValue); while LAttrs <> '' do begin IdDelete(LAttrs, 1, 1); I := Pos(';', LAttrs); if I > 0 then begin LAttr := Copy(LAttrs, 1, I-1); LAttrs := Copy(LAttrs, I, MaxInt); end else begin LAttr := LAttrs; LAttrs := ''; end; I := Pos('=', LAttr); if I > 0 then begin LName := Trim(Copy(LAttr, 1, I-1)); LValue := Trim(Copy(LAttr, I+1, MaxInt)); // RLebeau: RFC 6265 does not account for quoted attribute values, // despite several complaints asking for it. We'll do it anyway in // the hopes that the RFC will be updated to "do the right thing"... if TextStartsWith(LValue, '"') then begin IdDelete(LValue, 1, 1); LNameValue := LValue; LValue := Fetch(LNameValue, '"'); end; end else begin LName := Trim(LAttr); LValue := ''; end; case PosInStrArray(LName, ['Expires', 'Max-Age', 'Domain', 'Path', 'Secure', 'HttpOnly'], False) of 0: begin if TryStrToInt64(LValue, LSecs) then begin // Not in the RFCs, but some servers specify Expires as an // integer number in seconds instead of using Max-Age... if LSecs >= 0 then begin // TODO: use SecsPerDay instead: // LExpiryTime := (Now + (LSecs / SecsPerDay)); LExpiryTime := (Now + LSecs * 1000 / MSecsPerDay); end else begin LExpiryTime := EncodeDate(1, 1, 1); end; CookieProp.Add('EXPIRES=' + FloatToStr(LExpiryTime)); end else begin LExpiryTime := CookieStrToLocalDateTime(LValue); if LExpiryTime <> 0.0 then begin CookieProp.Add('EXPIRES=' + FloatToStr(LExpiryTime)); end; end; end; 1: begin if TryStrToInt64(LValue, LSecs) then begin if LSecs >= 0 then begin // TODO: use SecsPerDay instead: // LExpiryTime := (Now + (LSecs / SecsPerDay)); LExpiryTime := (Now + LSecs * 1000 / MSecsPerDay); end else begin LExpiryTime := EncodeDate(1, 1, 1); end; CookieProp.Add('MAX-AGE=' + FloatToStr(LExpiryTime)); end; end; 2: begin if LValue <> '' then begin if TextStartsWith(LValue, '.') then begin {do not localize} LValue := Copy(LValue, 2, MaxInt); end; // RLebeau: have encountered one cookie in the 'Set-Cookie' header that // includes a port number in the domain, though the RFCs do not indicate // this is allowed. RFC 2965 defines an explicit "port" attribute in the // 'Set-Cookie2' header for that purpose instead. We'll just strip it off // here if present... I := Pos(':', LValue); if I > 0 then begin LValue := Copy(S, 1, I-1); end; CookieProp.Add('DOMAIN=' + LowerCase(LValue)); end; end; 3: begin if (LValue = '') or (not TextStartsWith(LValue, '/')) then begin LValue := GetDefaultPath(AURI); end; CookieProp.Add('PATH=' + LValue); end; 4: begin CookieProp.Add('SECURE='); end; 5: begin CookieProp.Add('HTTPONLY='); end; end; end; end; function GetLastValueOf(const CookieProp: TStringList; const AName: String; var VValue: String): Boolean; var I: Integer; begin Result := False; for I := CookieProp.Count-1 downto 0 do begin if TextIsSame(CookieProp.Names[I], AName) then begin VValue := IndyValueFromIndex(CookieProp, I); Result := True; Exit; end; end; end; //Darcy: moved down the variables! Android compiler... bad boy! var CookieProp: TStringList; S: string; begin Result := False; // using the algorithm defined in RFC 6265 section 5.2... CookieProp := TStringList.Create; try SplitCookieText(CookieProp, S); if CookieProp.Count = 0 then begin Exit; end; FName := CookieProp.Names[0]; FValue := IndyValueFromIndex(CookieProp, 0); CookieProp.Delete(0); FCreatedAt := Now; FLastAccessed := FCreatedAt; // using the algorithms defined in RFC 6265 section 5.3... if GetLastValueOf(CookieProp, 'MAX-AGE', S) then begin {Do not Localize} FPersistent := True; FExpires := StrToFloat(S); end else if GetLastValueOf(CookieProp, 'EXPIRES', S) then {Do not Localize} begin FPersistent := True; FExpires := StrToFloat(S); end else begin FPersistent := False; FExpires := EncodeDate(9999, 12, 31) + EncodeTime(23, 59, 59, 999); end; S := ''; if GetLastValueOf(CookieProp, 'DOMAIN', S) then {Do not Localize} begin // TODO { If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix: If the domain-attribute is identical to the canonicalized request-host: Let the domain-attribute be the empty string. Otherwise: Ignore the cookie entirely and abort these steps. NOTE: A "public suffix" is a domain that is controlled by a public registry, such as "com", "co.uk", and "pvt.k12.wy.us". This step is essential for preventing attacker.com from disrupting the integrity of example.com by setting a cookie with a Domain attribute of "com". Unfortunately, the set of public suffixes (also known as "registry controlled domains") changes over time. If feasible, user agents SHOULD use an up-to-date public suffix list, such as the one maintained by the Mozilla project at . } { if RejectPublicSuffixes and IsPublicSuffix(S) then begin if S <> CanonicalizeHostName(AURI.Host) then begin Exit; end; S := ''; end; } end; if Length(S) > 0 then begin if not IsDomainMatch(AURI.Host, S) then begin Exit; end; FHostOnly := False; FDomain := S; end else begin FHostOnly := True; FDomain := CanonicalizeHostName(AURI.Host); end; if GetLastValueOf(CookieProp, 'PATH', S) then begin {Do not Localize} FPath := S; end else begin FPath := GetDefaultPath(AURI); end; FSecure := CookieProp.IndexOfName('SECURE') <> -1; { Do not Localize } FHttpOnly := CookieProp.IndexOfName('HTTPONLY') <> -1; { Do not Localize } if FHttpOnly and (not IsHTTP(AURI.Protocol)) then begin Exit; end; Result := True; finally FreeAndNil(CookieProp); end; end; function TIdCookie.GetIsExpired: Boolean; begin Result := (FExpires <> 0.0) and (FExpires < Now); end; function TIdCookie.GetMaxAge: Int64; begin if FExpires <> 0.0 then begin Result := Trunc((FExpires - Now) * MSecsPerDay / 1000); end else begin Result := -1; end; end; { set-cookie-header = "Set-Cookie:" SP set-cookie-string set-cookie-string = cookie-pair *( ";" SP cookie-av ) cookie-pair = cookie-name "=" cookie-value cookie-name = token cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E ; US-ASCII characters excluding CTLs, ; whitespace DQUOTE, comma, semicolon, ; and backslash token = cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / httponly-av / extension-av expires-av = "Expires=" sane-cookie-date sane-cookie-date = max-age-av = "Max-Age=" non-zero-digit *DIGIT ; In practice, both expires-av and max-age-av ; are limited to dates representable by the ; user agent. non-zero-digit = %x31-39 ; digits 1 through 9 domain-av = "Domain=" domain-value domain-value = ; defined in [RFC1034], Section 3.5, as ; enhanced by [RFC1123], Section 2.1 path-av = "Path=" path-value path-value = secure-av = "Secure" httponly-av = "HttpOnly" extension-av = } function TIdCookie.GetServerCookie: String; var LExpires: TDateTime; LMaxAge: Int64; begin Result := FName + '=' + FValue; {Do not Localize} AddCookieProperty(Result, 'Path', FPath); {Do not Localize} AddCookieProperty(Result, 'Domain', FDomain); {Do not Localize} if FSecure then begin AddCookieFlag(Result, 'Secure'); {Do not Localize} end; if FHttpOnly then begin AddCookieFlag(Result, 'HttpOnly'); {Do not Localize} end; LMaxAge := MaxAge; if LMaxAge >= 0 then begin AddCookieProperty(Result, 'Max-Age', IntToStr(LMaxAge)); {Do not Localize} end; LExpires := Expires; if LExpires <> 0.0 then begin AddCookieProperty(Result, 'Expires', LocalDateTimeToCookieStr(LExpires)); {Do not Localize} end; end; { Cookie: NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ... } function TIdCookie.GetClientCookie: String; begin Result := FName + '=' + FValue; end; { cookie-header = "Cookie:" OWS cookie-string OWS cookie-string = cookie-pair *( ";" SP cookie-pair ) } function TIdCookie.ParseClientCookie(const ACookieText: String): Boolean; var CookieProp: TStringList; procedure SplitCookieText; var LTemp, LName, LValue: String; i: Integer; IsFlag: Boolean; begin LTemp := Trim(ACookieText); while LTemp <> '' do {Do not Localize} begin i := FindFirstOf('=;', LTemp); {Do not Localize} if i = 0 then begin CookieProp.Add(LTemp); Break; end; IsFlag := (LTemp[i] = ';'); {Do not Localize} LName := TrimRight(Copy(LTemp, 1, i-1)); LTemp := TrimLeft(Copy(LTemp, i+1, MaxInt)); LValue := ''; if (not IsFlag) and (LTemp <> '') then begin if TextStartsWith(LTemp, '"') then {Do not Localize} begin IdDelete(LTemp, 1, 1); LValue := Fetch(LTemp, '"'); {Do not Localize} Fetch(LTemp, ';'); {Do not Localize} end else begin LValue := Trim(Fetch(LTemp, ';')); {Do not Localize} end; LTemp := TrimLeft(LTemp); end; if LName <> '' then begin CookieProp.Add(LName + '=' + LValue); {Do not Localize} end; end; end; begin Result := False; CookieProp := TStringList.Create; try SplitCookieText; if CookieProp.Count = 0 then begin Exit; end; FName := CookieProp.Names[0]; FValue := IndyValueFromIndex(CookieProp, 0); Result := True; finally FreeAndNil(CookieProp); end; end; { TIdCookies } constructor TIdCookies.Create(AOwner: TPersistent); begin inherited Create(AOwner, TIdCookie); FRWLock := TMultiReadExclusiveWriteSynchronizer.Create; FCookieList := TIdCookieList.Create; end; destructor TIdCookies.Destroy; begin // This will force the Cookie removing process before we free FCookieList and FRWLock Self.Clear; FreeAndNil(FCookieList); FreeAndNil(FRWLock); inherited Destroy; end; function TIdCookies.Add: TIdCookie; begin Result := TIdCookie(inherited Add); end; function TIdCookies.AddCookie(ACookie: TIdCookie; AURI: TIdURI; AReplaceOld: Boolean = True): Boolean; var LOldCookie: TIdCookie; I: Integer; begin Result := False; LockCookieList(caReadWrite); try if AReplaceOld then begin for I := 0 to FCookieList.Count-1 do begin LOldCookie := FCookieList[I]; if not TextIsSame(LOldCookie.CookieName, ACookie.CookieName) then begin Continue; end; if not TextIsSame(LOldCookie.Domain, ACookie.Domain) then begin Continue; end; if not TextIsSame(LOldCookie.Path, ACookie.Path) then begin Continue; end; if ((AURI <> nil) and (not IsHTTP(AURI.Protocol))) and LOldCookie.HttpOnly then begin Exit; end; ACookie.FCreatedAt := LOldCookie.CreatedAt; FCookieList.Delete(I); LOldCookie.Collection := nil; LOldCookie.Free; Break; end; end; if not ACookie.IsExpired then begin FCookieList.Add(ACookie); Result := True; end; finally UnlockCookieList(caReadWrite); end; end; procedure TIdCookies.Assign(ASource: TPersistent); begin if (ASource = nil) or (ASource is TIdCookies) then begin LockCookieList(caReadWrite); try Clear; AddCookies(TIdCookies(ASource)); finally UnlockCookieList(caReadWrite); end; end else begin inherited Assign(ASource); end; end; function TIdCookies.GetCookie(Index: Integer): TIdCookie; begin Result := inherited GetItem(Index) as TIdCookie; end; procedure TIdCookies.SetCookie(Index: Integer; const Value: TIdCookie); begin inherited SetItem(Index, Value); end; function TIdCookies.AddClientCookie(const ACookie: string): TIdCookie; var LCookie: TIdCookie; begin Result := nil; LCookie := Add; try if LCookie.ParseClientCookie(ACookie) then begin LockCookieList(caReadWrite); try FCookieList.Add(LCookie); Result := LCookie; LCookie := nil; finally UnlockCookieList(caReadWrite); end; end; finally if LCookie <> nil then begin LCookie.Collection := nil; LCookie.Free; end; end; end; procedure TIdCookies.AddClientCookies(const ACookie: string); var Temp: TStringList; LCookie, S: String; I: Integer; begin S := Trim(ACookie); if S <> '' then begin Temp := TStringList.Create; try repeat LCookie := Fetch(S, ';'); if LCookie <> '' then begin Temp.Add(LCookie); end; until S = ''; for I := 0 to Temp.Count-1 do begin AddClientCookie(Temp[I]); end; finally Temp.Free; end; end; end; procedure TIdCookies.AddClientCookies(const ACookies: TStrings); var i: Integer; begin for i := 0 to ACookies.Count - 1 do begin AddClientCookies(ACookies[i]); end; end; function TIdCookies.AddServerCookie(const ACookie: string; AURI: TIdURI): TIdCookie; var LCookie: TIdCookie; begin Result := nil; LCookie := Add; try if LCookie.ParseServerCookie(ACookie, AURI) then begin if AddCookie(LCookie, AURI) then begin Result := LCookie; LCookie := nil; end; end; finally if LCookie <> nil then begin LCookie.Collection := nil; LCookie.Free; end; end; end; procedure TIdCookies.AddServerCookies(const ACookies: TStrings; AURI: TIdURI); var i: Integer; begin for i := 0 to ACookies.Count - 1 do begin AddServerCookie(ACookies[i], AURI); end; end; procedure TIdCookies.AddCookies(ASource: TIdCookies); var LSrcCookies: TIdCookieList; LSrcCookie, LDestCookie: TIdCookie; i: Integer; begin if (ASource <> nil) and (ASource <> Self) then begin LSrcCookies := ASource.LockCookieList(caRead); try LockCookieList(caReadWrite); try for i := 0 to LSrcCookies.Count - 1 do begin LSrcCookie := LSrcCookies[i]; LDestCookie := TIdCookieClass(LSrcCookie.ClassType).Create(Self); try LDestCookie.Assign(LSrcCookie); FCookieList.Add(LDestCookie); except LDestCookie.Collection := nil; LDestCookie.Free; raise; end; end; finally UnlockCookieList(caReadWrite); end; finally ASource.UnlockCookieList(caRead); end; end; end; function TIdCookies.GetCookieByNameAndDomain(const AName, ADomain: string): TIdCookie; var i: Integer; begin i := GetCookieIndex(AName, ADomain); if i = -1 then begin Result := nil; end else begin Result := Cookies[i]; end; end; function TIdCookies.GetCookieIndex(const AName: string; FirstIndex: Integer = 0): Integer; var i: Integer; begin Result := -1; for i := FirstIndex to Count - 1 do begin if TextIsSame(Cookies[i].CookieName, AName) then begin Result := i; Exit; end; end; end; function TIdCookies.GetCookieIndex(const AName, ADomain: string; FirstIndex: Integer = 0): Integer; var LCookie: TIdCookie; i: Integer; begin Result := -1; for i := FirstIndex to Count - 1 do begin LCookie := Cookies[i]; if TextIsSame(LCookie.CookieName, AName) and TextIsSame(CanonicalizeHostName(LCookie.Domain), CanonicalizeHostName(ADomain)) then begin Result := i; Exit; end; end; end; procedure TIdCookies.Clear; begin LockCookieList(caReadWrite); try FCookieList.Clear; inherited Clear; finally UnlockCookieList(caReadWrite); end; end; function TIdCookies.LockCookieList(AAccessType: TIdCookieAccess): TIdCookieList; begin case AAccessType of caRead: begin FRWLock.BeginRead; end; caReadWrite: begin FRWLock.BeginWrite; end; end; Result := FCookieList; end; procedure TIdCookies.UnlockCookieList(AAccessType: TIdCookieAccess); begin case AAccessType of caRead: begin FRWLock.EndRead; end; caReadWrite: begin FRWLock.EndWrite; end; end; end; end.