| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- (* _ _
- * | |__ _ __ ___ ___ | | __
- * | '_ \| '__/ _ \ / _ \| |/ /
- * | |_) | | | (_) | (_) | <
- * |_.__/|_| \___/ \___/|_|\_\
- *
- * Microframework which helps to develop web Pascal applications.
- *
- * Copyright (c) 2012-2021 Silvio Clecio <[email protected]>
- *
- * Brook framework is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * Brook framework is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with Brook framework; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- *)
- { Contains classes which handles server side cookies. }
- unit BrookHTTPCookies;
- {$I BrookDefines.inc}
- interface
- uses
- SysUtils,
- DateUtils,
- Classes,
- {$IFDEF FPC}
- HMAC,
- Base64,
- {$ELSE}
- System.Hash,
- System.NetEncoding,
- {$ENDIF}
- BrookUtility;
- const
- {$IFNDEF FPC}
- {$WRITEABLECONST ON}
- {$ENDIF}
- { Prefix to identify a signed cookie. }
- BROOK_COOKIE_SIG_PREFIX: string = 's:';
- { Default cookie name. }
- BROOK_COOKIE_NAME_PREFIX: string = 'BrookCookie';
- {$IFNDEF FPC}
- {$WRITEABLECONST OFF}
- {$ENDIF}
- resourcestring
- { Error message @code('Empty cookie name.'). }
- SBrookEmptyCookieName = 'Empty cookie name.';
- { Error message @code('Invalid cookie name: <cookie-name>.'). }
- SBrookInvalidCookieName = 'Invalid cookie name: %s.';
- type
- { Handles exceptions related to HTTP cookies classes. }
- EBrookHTTPCookie = class(Exception);
- { SameSite cookie attribute types. }
- TBrookHTTPCookieSameSite = (
- { The browser will send cookies with both cross-site requests and same-site
- requests. }
- ssNone,
- { The browser will only send cookies for same-site requests. }
- ssStrict,
- { Same-site cookies are withheld on cross-site subrequests, but will be
- sent when a user navigates to the URL from an external site. }
- ssLax
- );
- { Server side HTTP cookie item. }
- TBrookHTTPCookie = class(TCollectionItem)
- private
- FName: string;
- FValue: string;
- FOriginalValue: string;
- FDomain: string;
- FPath: string;
- FExpires: TDateTime;
- FHttpOnly: Boolean;
- FSecure: Boolean;
- FMaxAge: Integer;
- FSameSite: TBrookHTTPCookieSameSite;
- procedure SetMaxAge(AValue: Integer);
- procedure SetName(const AValue: string);
- procedure SetValue(const AValue: string);
- procedure SetPath(const AValue: string);
- protected
- property OriginalValue: string read FOriginalValue;
- public
- { Creates an instance of @code(TBrookHTTPCookie).
- @param(ACollection[in] Cookies list.) }
- constructor Create(ACollection: TCollection); override;
- { Copies the properties of the source cookie.
- @param(ASource[in] Cookie source to be copied.) }
- procedure Assign(ASource: TPersistent); override;
- { Signs a cookie value using
- @html(<a href="https://en.wikipedia.org/wiki/HMAC">HMAC-SHA1</a>).
- @param(ASecret[in] Secret key to sign the cookie value.)
- @param(AUnsignedValue[in] Unsigned cookie value to be signed.)
- @returns(Signed cookie value.) }
- class function Sign(const ASecret,
- AUnsignedValue: string): string; overload; static;
- { Tries to unsign a cookie value.
- @param(ASecret[in] Secret key to unsign the cookie value.)
- @param(ASignedValue[out] Signed cookie value.)
- @param(AUnsignedValue[out] Unsigned cookie value.)
- @returns(@True if cookie value is unsigned successfully.) }
- class function TryUnsign(const ASecret, ASignedValue: string;
- out AUnsignedValue: string): Boolean; overload; static;
- { Unsigns a cookie value.
- @param(ASecret[in] Secret key to unsign the cookie value.)
- @param(ASignedValue[in] Signed cookie value.)
- @returns(Unsigned cookie value.) }
- class function Unsign(const ASecret,
- ASignedValue: string): string; overload; static;
- {$IFNDEF DEBUG}inline;{$ENDIF}
- { Checks if a cookie value is signed.
- @param(ASignedValue[out] Signed cookie value.)
- @returns(@True if cookie value is signed.) }
- class function IsSigned(
- const ASignedValue: string): Boolean; overload; static;
- {$IFNDEF DEBUG}inline;{$ENDIF}
- { Checks if a cookie is signed.
- @returns(@True if cookie is signed.) }
- function IsSigned: Boolean; overload; virtual;
- { Signs a cookie value using
- @html(<a href="https://en.wikipedia.org/wiki/HMAC">HMAC-SHA1</a>).
- @param(ASecret[in] Secret key to sign the cookie value.) }
- procedure Sign(const ASecret: string); overload; virtual;
- { Tries to unsign a cookie.
- @param(ASecret[in] Secret key to unsign the cookie value.)
- @returns(@True if cookie is unsigned successfully.) }
- function TryUnsign(const ASecret: string): Boolean; overload; virtual;
- { Unsigns a cookie.
- @param(ASecret[in] Secret key to unsign the cookie value.) }
- procedure Unsign(const ASecret: string); overload; virtual;
- { Gets the cookie as string. }
- function ToString: string; override;
- { Clears the cookie properties. }
- procedure Clear; virtual;
- { Expires the cookie. }
- procedure Expire; virtual;
- { Persists a cookie to live as long as it can. }
- procedure Persist; virtual;
- { Cookie name. }
- property Name: string read FName write SetName;
- { Cookie value. }
- property Value: string read FValue write SetValue;
- { Allowed domain to receive the cookie. }
- property Domain: string read FDomain write FDomain;
- { Path that must exist in the URL to receive the cookie. }
- property Path: string read FPath write SetPath;
- { Expiration date/time. }
- property Expires: TDateTime read FExpires write FExpires;
- { @True prevents the cookie to be accessed through JavaScript. }
- property HttpOnly: Boolean read FHttpOnly write FHttpOnly;
- { @True indicates cookie sent only through HTTPS protocol. }
- property Secure: Boolean read FSecure write FSecure;
- { Sets an expiration expressed in number of seconds. }
- property MaxAge: Integer read FMaxAge write SetMaxAge;
- { @True indicates that a cookie shouldn't be sent with cross-site requests. }
- property SameSite: TBrookHTTPCookieSameSite read FSameSite write FSameSite;
- end;
- { Class-reference for @code(TBrookHTTPCookie). }
- TBrookHTTPCookieClass = class of TBrookHTTPCookie;
- { List enumerator for @code(TBrookHTTPCookies). }
- TBrookHTTPCookiesEnumerator = class(TCollectionEnumerator)
- public
- { Get current cookie item. }
- function GetCurrent: TBrookHTTPCookie;
- { Current cookie item. }
- property Current: TBrookHTTPCookie read GetCurrent;
- end;
- { Server side HTTP cookie list. }
- TBrookHTTPCookies = class(TOwnedCollection)
- protected
- function GetItem(AIndex: Integer): TBrookHTTPCookie; virtual;
- procedure SetItem(AIndex: Integer; AValue: TBrookHTTPCookie); virtual;
- public
- { Creates an instance of @code(TBrookHTTPCookies).
- @param(AOwner[in] Cookies persistent.) }
- constructor Create(AOwner: TPersistent); virtual;
- { Gets the default class for cookie item creation. }
- class function GetCookieClass: TBrookHTTPCookieClass; virtual;
- { Copies the items of the source cookies.
- @param(ASource[in] Cookies source to be copied.) }
- procedure Assign(ASource: TPersistent); override;
- { Creates an enumerator to iterate the cookies though @code(for..in). }
- function GetEnumerator: TBrookHTTPCookiesEnumerator;
- { Adds a new cookie to the cookies list. }
- function Add: TBrookHTTPCookie; virtual;
- { Removes a cookie from the cookies list by its name.
- @param(AName[in] Cookie name.) }
- function Remove(const AName: string): Boolean; virtual;
- { Gets the cookie index by its name. }
- function IndexOf(const AName: string): Integer; virtual;
- { Finds a cookie in the cookies list by its name.
- @param(AName[in] Cookie name.) }
- function Find(const AName: string): TBrookHTTPCookie; virtual;
- { Gets the first cookie in the cookies list. }
- function First: TBrookHTTPCookie; virtual;
- { Gets the last cookie in the cookies list. }
- function Last: TBrookHTTPCookie; virtual;
- { Gets/sets a cookie from/to the cookies list by its index. }
- property Items[AIndex: Integer]: TBrookHTTPCookie read GetItem
- write SetItem; default;
- end;
- implementation
- { TBrookHTTPCookie }
- constructor TBrookHTTPCookie.Create(ACollection: TCollection);
- begin
- inherited Create(ACollection);
- FName := BROOK_COOKIE_NAME_PREFIX;
- if Assigned(ACollection) then
- FName := Concat(FName, Succ(ID).ToString);
- FExpires := -1;
- FMaxAge := -1;
- FPath := '/';
- end;
- class function TBrookHTTPCookie.IsSigned(const ASignedValue: string): Boolean;
- begin
- Result := (Length(ASignedValue) > 0) and CompareMem(@ASignedValue[1],
- @BROOK_COOKIE_SIG_PREFIX[1], Length(BROOK_COOKIE_SIG_PREFIX) * SizeOf(Char));
- end;
- class function TBrookHTTPCookie.Sign(const ASecret,
- AUnsignedValue: string): string;
- var
- {$IFDEF FPC}
- VEncoder: TBase64EncodingStream;
- VStream: TStringStream;
- VDigest: THMACSHA1Digest;
- {$ELSE}
- VEncoder: TBase64Encoding;
- {$ENDIF}
- VPos: Integer;
- begin
- if IsSigned(AUnsignedValue) then
- Exit(AUnsignedValue);
- {$IFDEF FPC}
- VStream := TStringStream.Create('');
- try
- VEncoder := TBase64EncodingStream.Create(VStream);
- try
- VDigest := HMACSHA1Digest(ASecret, AUnsignedValue);
- VEncoder.Write(VDigest[0], Length(VDigest));
- finally
- VEncoder.Destroy;
- end;
- Result := VStream.DataString;
- finally
- VStream.Destroy;
- end
- {$ELSE}
- VEncoder := TBase64Encoding.Create(0, '');
- try
- Result := VEncoder.EncodeBytesToString(
- THashSHA1.GetHMACAsBytes(AUnsignedValue, ASecret))
- finally
- VEncoder.Free;
- end;
- {$ENDIF};
- VPos := Pos('=', Result);
- if VPos > 0 then
- System.Delete(Result, VPos, MaxInt);
- Result := Concat(BROOK_COOKIE_SIG_PREFIX, AUnsignedValue, '.', Result);
- end;
- class function TBrookHTTPCookie.TryUnsign(const ASecret, ASignedValue: string;
- out AUnsignedValue: string): Boolean;
- var
- VPos: Integer;
- begin
- if not IsSigned(ASignedValue) then
- Exit(False);
- AUnsignedValue := ASignedValue;
- System.Delete(AUnsignedValue, 1, Length(BROOK_COOKIE_SIG_PREFIX));
- VPos := Pos('.', AUnsignedValue);
- if VPos > 0 then
- begin
- AUnsignedValue := Copy(AUnsignedValue, 1, Pred(VPos));
- if (Length(AUnsignedValue) > 0) and
- (CompareStr(Brook.Sha1(Sign(ASecret, AUnsignedValue)),
- Brook.Sha1(ASignedValue)) = 0) then
- Exit(True);
- end;
- Result := False;
- end;
- class function TBrookHTTPCookie.Unsign(const ASecret,
- ASignedValue: string): string;
- begin
- if not TryUnsign(ASecret, ASignedValue, Result) then
- Result := EmptyStr;
- end;
- procedure TBrookHTTPCookie.Assign(ASource: TPersistent);
- var
- VSrc: TBrookHTTPCookie;
- begin
- if ASource is TBrookHTTPCookie then
- begin
- VSrc := ASource as TBrookHTTPCookie;
- FName := VSrc.Name;
- FValue := VSrc.Value;
- FDomain := VSrc.Domain;
- FPath := VSrc.Path;
- FExpires := VSrc.Expires;
- FHttpOnly := VSrc.HttpOnly;
- FSecure := VSrc.Secure;
- FMaxAge := VSrc.MaxAge;
- FSameSite := VSrc.SameSite;
- end
- else
- inherited Assign(ASource);
- end;
- function TBrookHTTPCookie.IsSigned: Boolean;
- begin
- Result := IsSigned(FValue);
- end;
- procedure TBrookHTTPCookie.Sign(const ASecret: string);
- begin
- FValue := Sign(ASecret, FValue);
- end;
- function TBrookHTTPCookie.TryUnsign(const ASecret: string): Boolean;
- var
- R: string;
- begin
- Result := TryUnsign(ASecret, FValue, R);
- if Result then
- FValue := R;
- end;
- procedure TBrookHTTPCookie.Unsign(const ASecret: string);
- begin
- FValue := Unsign(ASecret, FValue);
- end;
- function TBrookHTTPCookie.ToString: string;
- begin
- Result := Concat(FName, '=');
- if IsSigned then
- Result := Concat(Result, BROOK_COOKIE_SIG_PREFIX, FOriginalValue,
- FValue.SubString(BROOK_COOKIE_SIG_PREFIX.Length + FOriginalValue.Length))
- else
- Result := Concat(Result, FValue);
- if FMaxAge > -1 then
- Result := Concat(Result, '; Max-Age=', IntToStr(FMaxAge));
- if Length(FDomain) > 0 then
- Result := Concat(Result, '; Domain=', FDomain);
- if Length(FPath) > 0 then
- Result := Concat(Result, '; Path=', FPath);
- if FExpires > -1 then
- Result := Concat(Result, '; Expires=', Brook.DateTimeToGmt(FExpires));
- if FHttpOnly then
- Result := Concat(Result, '; HttpOnly');
- if FSecure then
- Result := Concat(Result, '; Secure');
- case FSameSite of
- ssStrict: Result := Concat(Result, '; SameSite=Strict');
- ssLax: Result := Concat(Result, '; SameSite=Lax');
- ssNone: ;
- end;
- end;
- procedure TBrookHTTPCookie.SetMaxAge(AValue: Integer);
- begin
- if AValue = FMaxAge then
- Exit;
- FMaxAge := AValue;
- if AValue > 0 then
- FExpires := Brook.DateTimeToUTC(IncSecond(Now, AValue))
- else
- Expire;
- end;
- procedure TBrookHTTPCookie.SetName(const AValue: string);
- begin
- if AValue = FName then
- Exit;
- if AValue.IsEmpty then
- raise EBrookHTTPCookie.Create(SBrookEmptyCookieName);
- if not IsValidIdent(AValue) then
- raise EBrookHTTPCookie.CreateFmt(SBrookInvalidCookieName, [AValue]);
- FName := AValue;
- end;
- procedure TBrookHTTPCookie.SetValue(const AValue: string);
- begin
- if AValue = FValue then
- Exit;
- FValue := AValue;
- FOriginalValue := FValue;
- end;
- procedure TBrookHTTPCookie.SetPath(const AValue: string);
- begin
- if AValue <> FPath then
- FPath := Brook.FixPath(AValue);
- end;
- procedure TBrookHTTPCookie.Clear;
- begin
- FValue := '';
- FMaxAge := -1;
- FDomain := '';
- FPath := '';
- FExpires := -1;
- FHTTPOnly := False;
- FSecure := False;
- FSameSite := ssNone;
- end;
- procedure TBrookHTTPCookie.Expire;
- begin
- FExpires := EncodeDate(1970, 1, 1);
- end;
- procedure TBrookHTTPCookie.Persist;
- begin
- FExpires := EncodeDate(9999, 12, 31) + EncodeTime(23, 59, 59, 999);
- end;
- { TBrookHTTPCookiesEnumerator }
- function TBrookHTTPCookiesEnumerator.GetCurrent: TBrookHTTPCookie;
- begin
- Result := TBrookHTTPCookie(inherited GetCurrent);
- end;
- { TBrookHTTPCookies }
- constructor TBrookHTTPCookies.Create(AOwner: TPersistent);
- begin
- inherited Create(AOwner, GetCookieClass);
- end;
- class function TBrookHTTPCookies.GetCookieClass: TBrookHTTPCookieClass;
- begin
- Result := TBrookHTTPCookie;
- end;
- function TBrookHTTPCookies.GetEnumerator: TBrookHTTPCookiesEnumerator;
- begin
- Result := TBrookHTTPCookiesEnumerator.Create(Self);
- end;
- procedure TBrookHTTPCookies.Assign(ASource: TPersistent);
- var
- C: TBrookHTTPCookie;
- begin
- if ASource is TBrookHTTPCookies then
- begin
- Clear;
- for C in (ASource as TBrookHTTPCookies) do
- Add.Assign(C);
- end
- else
- inherited Assign(ASource);
- end;
- function TBrookHTTPCookies.GetItem(AIndex: Integer): TBrookHTTPCookie;
- begin
- Result := TBrookHTTPCookie(inherited GetItem(AIndex));
- end;
- procedure TBrookHTTPCookies.SetItem(AIndex: Integer;
- AValue: TBrookHTTPCookie);
- begin
- inherited SetItem(AIndex, AValue);
- end;
- function TBrookHTTPCookies.Add: TBrookHTTPCookie;
- begin
- Result := TBrookHTTPCookie(inherited Add);
- end;
- function TBrookHTTPCookies.Remove(const AName: string): Boolean;
- var
- I: Integer;
- begin
- I := IndexOf(AName);
- Result := I > -1;
- if Result then
- inherited Delete(I);
- end;
- function TBrookHTTPCookies.IndexOf(const AName: string): Integer;
- begin
- for Result := 0 to Pred(Count) do
- if SameText(GetItem(Result).Name, AName) then
- Exit;
- Result := -1;
- end;
- function TBrookHTTPCookies.Find(const AName: string): TBrookHTTPCookie;
- var
- C: TBrookHTTPCookie;
- begin
- for C in Self do
- if SameText(C.Name, AName) then
- Exit(C);
- Result := nil;
- end;
- function TBrookHTTPCookies.First: TBrookHTTPCookie;
- begin
- if Count = 0 then
- Exit(nil);
- Result := GetItem(0);
- end;
- function TBrookHTTPCookies.Last: TBrookHTTPCookie;
- begin
- if Count = 0 then
- Exit(nil);
- Result := GetItem(Pred(Count));
- end;
- end.
|