BrookHTTPCookies.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. (* _ _
  2. * | |__ _ __ ___ ___ | | __
  3. * | '_ \| '__/ _ \ / _ \| |/ /
  4. * | |_) | | | (_) | (_) | <
  5. * |_.__/|_| \___/ \___/|_|\_\
  6. *
  7. * Microframework which helps to develop web Pascal applications.
  8. *
  9. * Copyright (c) 2012-2021 Silvio Clecio <[email protected]>
  10. *
  11. * Brook framework is free software; you can redistribute it and/or
  12. * modify it under the terms of the GNU Lesser General Public
  13. * License as published by the Free Software Foundation; either
  14. * version 2.1 of the License, or (at your option) any later version.
  15. *
  16. * Brook framework is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  19. * Lesser General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Lesser General Public
  22. * License along with Brook framework; if not, write to the Free Software
  23. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  24. *)
  25. { Contains classes which handles server side cookies. }
  26. unit BrookHTTPCookies;
  27. {$I BrookDefines.inc}
  28. interface
  29. uses
  30. SysUtils,
  31. DateUtils,
  32. Classes,
  33. {$IFDEF FPC}
  34. HMAC,
  35. Base64,
  36. {$ELSE}
  37. System.Hash,
  38. System.NetEncoding,
  39. {$ENDIF}
  40. BrookUtility;
  41. const
  42. {$IFNDEF FPC}
  43. {$WRITEABLECONST ON}
  44. {$ENDIF}
  45. { Prefix to identify a signed cookie. }
  46. BROOK_COOKIE_SIG_PREFIX: string = 's:';
  47. { Default cookie name. }
  48. BROOK_COOKIE_NAME_PREFIX: string = 'BrookCookie';
  49. {$IFNDEF FPC}
  50. {$WRITEABLECONST OFF}
  51. {$ENDIF}
  52. resourcestring
  53. { Error message @code('Empty cookie name.'). }
  54. SBrookEmptyCookieName = 'Empty cookie name.';
  55. { Error message @code('Invalid cookie name: <cookie-name>.'). }
  56. SBrookInvalidCookieName = 'Invalid cookie name: %s.';
  57. type
  58. { Handles exceptions related to HTTP cookies classes. }
  59. EBrookHTTPCookie = class(Exception);
  60. { SameSite cookie attribute types. }
  61. TBrookHTTPCookieSameSite = (
  62. { The browser will send cookies with both cross-site requests and same-site
  63. requests. }
  64. ssNone,
  65. { The browser will only send cookies for same-site requests. }
  66. ssStrict,
  67. { Same-site cookies are withheld on cross-site subrequests, but will be
  68. sent when a user navigates to the URL from an external site. }
  69. ssLax
  70. );
  71. { Server side HTTP cookie item. }
  72. TBrookHTTPCookie = class(TCollectionItem)
  73. private
  74. FName: string;
  75. FValue: string;
  76. FOriginalValue: string;
  77. FDomain: string;
  78. FPath: string;
  79. FExpires: TDateTime;
  80. FHttpOnly: Boolean;
  81. FSecure: Boolean;
  82. FMaxAge: Integer;
  83. FSameSite: TBrookHTTPCookieSameSite;
  84. procedure SetMaxAge(AValue: Integer);
  85. procedure SetName(const AValue: string);
  86. procedure SetValue(const AValue: string);
  87. procedure SetPath(const AValue: string);
  88. protected
  89. property OriginalValue: string read FOriginalValue;
  90. public
  91. { Creates an instance of @code(TBrookHTTPCookie).
  92. @param(ACollection[in] Cookies list.) }
  93. constructor Create(ACollection: TCollection); override;
  94. { Copies the properties of the source cookie.
  95. @param(ASource[in] Cookie source to be copied.) }
  96. procedure Assign(ASource: TPersistent); override;
  97. { Signs a cookie value using
  98. @html(<a href="https://en.wikipedia.org/wiki/HMAC">HMAC-SHA1</a>).
  99. @param(ASecret[in] Secret key to sign the cookie value.)
  100. @param(AUnsignedValue[in] Unsigned cookie value to be signed.)
  101. @returns(Signed cookie value.) }
  102. class function Sign(const ASecret,
  103. AUnsignedValue: string): string; overload; static;
  104. { Tries to unsign a cookie value.
  105. @param(ASecret[in] Secret key to unsign the cookie value.)
  106. @param(ASignedValue[out] Signed cookie value.)
  107. @param(AUnsignedValue[out] Unsigned cookie value.)
  108. @returns(@True if cookie value is unsigned successfully.) }
  109. class function TryUnsign(const ASecret, ASignedValue: string;
  110. out AUnsignedValue: string): Boolean; overload; static;
  111. { Unsigns a cookie value.
  112. @param(ASecret[in] Secret key to unsign the cookie value.)
  113. @param(ASignedValue[in] Signed cookie value.)
  114. @returns(Unsigned cookie value.) }
  115. class function Unsign(const ASecret,
  116. ASignedValue: string): string; overload; static;
  117. {$IFNDEF DEBUG}inline;{$ENDIF}
  118. { Checks if a cookie value is signed.
  119. @param(ASignedValue[out] Signed cookie value.)
  120. @returns(@True if cookie value is signed.) }
  121. class function IsSigned(
  122. const ASignedValue: string): Boolean; overload; static;
  123. {$IFNDEF DEBUG}inline;{$ENDIF}
  124. { Checks if a cookie is signed.
  125. @returns(@True if cookie is signed.) }
  126. function IsSigned: Boolean; overload; virtual;
  127. { Signs a cookie value using
  128. @html(<a href="https://en.wikipedia.org/wiki/HMAC">HMAC-SHA1</a>).
  129. @param(ASecret[in] Secret key to sign the cookie value.) }
  130. procedure Sign(const ASecret: string); overload; virtual;
  131. { Tries to unsign a cookie.
  132. @param(ASecret[in] Secret key to unsign the cookie value.)
  133. @returns(@True if cookie is unsigned successfully.) }
  134. function TryUnsign(const ASecret: string): Boolean; overload; virtual;
  135. { Unsigns a cookie.
  136. @param(ASecret[in] Secret key to unsign the cookie value.) }
  137. procedure Unsign(const ASecret: string); overload; virtual;
  138. { Gets the cookie as string. }
  139. function ToString: string; override;
  140. { Clears the cookie properties. }
  141. procedure Clear; virtual;
  142. { Expires the cookie. }
  143. procedure Expire; virtual;
  144. { Persists a cookie to live as long as it can. }
  145. procedure Persist; virtual;
  146. { Cookie name. }
  147. property Name: string read FName write SetName;
  148. { Cookie value. }
  149. property Value: string read FValue write SetValue;
  150. { Allowed domain to receive the cookie. }
  151. property Domain: string read FDomain write FDomain;
  152. { Path that must exist in the URL to receive the cookie. }
  153. property Path: string read FPath write SetPath;
  154. { Expiration date/time. }
  155. property Expires: TDateTime read FExpires write FExpires;
  156. { @True prevents the cookie to be accessed through JavaScript. }
  157. property HttpOnly: Boolean read FHttpOnly write FHttpOnly;
  158. { @True indicates cookie sent only through HTTPS protocol. }
  159. property Secure: Boolean read FSecure write FSecure;
  160. { Sets an expiration expressed in number of seconds. }
  161. property MaxAge: Integer read FMaxAge write SetMaxAge;
  162. { @True indicates that a cookie shouldn't be sent with cross-site requests. }
  163. property SameSite: TBrookHTTPCookieSameSite read FSameSite write FSameSite;
  164. end;
  165. { Class-reference for @code(TBrookHTTPCookie). }
  166. TBrookHTTPCookieClass = class of TBrookHTTPCookie;
  167. { List enumerator for @code(TBrookHTTPCookies). }
  168. TBrookHTTPCookiesEnumerator = class(TCollectionEnumerator)
  169. public
  170. { Get current cookie item. }
  171. function GetCurrent: TBrookHTTPCookie;
  172. { Current cookie item. }
  173. property Current: TBrookHTTPCookie read GetCurrent;
  174. end;
  175. { Server side HTTP cookie list. }
  176. TBrookHTTPCookies = class(TOwnedCollection)
  177. protected
  178. function GetItem(AIndex: Integer): TBrookHTTPCookie; virtual;
  179. procedure SetItem(AIndex: Integer; AValue: TBrookHTTPCookie); virtual;
  180. public
  181. { Creates an instance of @code(TBrookHTTPCookies).
  182. @param(AOwner[in] Cookies persistent.) }
  183. constructor Create(AOwner: TPersistent); virtual;
  184. { Gets the default class for cookie item creation. }
  185. class function GetCookieClass: TBrookHTTPCookieClass; virtual;
  186. { Copies the items of the source cookies.
  187. @param(ASource[in] Cookies source to be copied.) }
  188. procedure Assign(ASource: TPersistent); override;
  189. { Creates an enumerator to iterate the cookies though @code(for..in). }
  190. function GetEnumerator: TBrookHTTPCookiesEnumerator;
  191. { Adds a new cookie to the cookies list. }
  192. function Add: TBrookHTTPCookie; virtual;
  193. { Removes a cookie from the cookies list by its name.
  194. @param(AName[in] Cookie name.) }
  195. function Remove(const AName: string): Boolean; virtual;
  196. { Gets the cookie index by its name. }
  197. function IndexOf(const AName: string): Integer; virtual;
  198. { Finds a cookie in the cookies list by its name.
  199. @param(AName[in] Cookie name.) }
  200. function Find(const AName: string): TBrookHTTPCookie; virtual;
  201. { Gets the first cookie in the cookies list. }
  202. function First: TBrookHTTPCookie; virtual;
  203. { Gets the last cookie in the cookies list. }
  204. function Last: TBrookHTTPCookie; virtual;
  205. { Gets/sets a cookie from/to the cookies list by its index. }
  206. property Items[AIndex: Integer]: TBrookHTTPCookie read GetItem
  207. write SetItem; default;
  208. end;
  209. implementation
  210. { TBrookHTTPCookie }
  211. constructor TBrookHTTPCookie.Create(ACollection: TCollection);
  212. begin
  213. inherited Create(ACollection);
  214. FName := BROOK_COOKIE_NAME_PREFIX;
  215. if Assigned(ACollection) then
  216. FName := Concat(FName, Succ(ID).ToString);
  217. FExpires := -1;
  218. FMaxAge := -1;
  219. FPath := '/';
  220. end;
  221. class function TBrookHTTPCookie.IsSigned(const ASignedValue: string): Boolean;
  222. begin
  223. Result := (Length(ASignedValue) > 0) and CompareMem(@ASignedValue[1],
  224. @BROOK_COOKIE_SIG_PREFIX[1], Length(BROOK_COOKIE_SIG_PREFIX) * SizeOf(Char));
  225. end;
  226. class function TBrookHTTPCookie.Sign(const ASecret,
  227. AUnsignedValue: string): string;
  228. var
  229. {$IFDEF FPC}
  230. VEncoder: TBase64EncodingStream;
  231. VStream: TStringStream;
  232. VDigest: THMACSHA1Digest;
  233. {$ELSE}
  234. VEncoder: TBase64Encoding;
  235. {$ENDIF}
  236. VPos: Integer;
  237. begin
  238. if IsSigned(AUnsignedValue) then
  239. Exit(AUnsignedValue);
  240. {$IFDEF FPC}
  241. VStream := TStringStream.Create('');
  242. try
  243. VEncoder := TBase64EncodingStream.Create(VStream);
  244. try
  245. VDigest := HMACSHA1Digest(ASecret, AUnsignedValue);
  246. VEncoder.Write(VDigest[0], Length(VDigest));
  247. finally
  248. VEncoder.Destroy;
  249. end;
  250. Result := VStream.DataString;
  251. finally
  252. VStream.Destroy;
  253. end
  254. {$ELSE}
  255. VEncoder := TBase64Encoding.Create(0, '');
  256. try
  257. Result := VEncoder.EncodeBytesToString(
  258. THashSHA1.GetHMACAsBytes(AUnsignedValue, ASecret))
  259. finally
  260. VEncoder.Free;
  261. end;
  262. {$ENDIF};
  263. VPos := Pos('=', Result);
  264. if VPos > 0 then
  265. System.Delete(Result, VPos, MaxInt);
  266. Result := Concat(BROOK_COOKIE_SIG_PREFIX, AUnsignedValue, '.', Result);
  267. end;
  268. class function TBrookHTTPCookie.TryUnsign(const ASecret, ASignedValue: string;
  269. out AUnsignedValue: string): Boolean;
  270. var
  271. VPos: Integer;
  272. begin
  273. if not IsSigned(ASignedValue) then
  274. Exit(False);
  275. AUnsignedValue := ASignedValue;
  276. System.Delete(AUnsignedValue, 1, Length(BROOK_COOKIE_SIG_PREFIX));
  277. VPos := Pos('.', AUnsignedValue);
  278. if VPos > 0 then
  279. begin
  280. AUnsignedValue := Copy(AUnsignedValue, 1, Pred(VPos));
  281. if (Length(AUnsignedValue) > 0) and
  282. (CompareStr(Brook.Sha1(Sign(ASecret, AUnsignedValue)),
  283. Brook.Sha1(ASignedValue)) = 0) then
  284. Exit(True);
  285. end;
  286. Result := False;
  287. end;
  288. class function TBrookHTTPCookie.Unsign(const ASecret,
  289. ASignedValue: string): string;
  290. begin
  291. if not TryUnsign(ASecret, ASignedValue, Result) then
  292. Result := EmptyStr;
  293. end;
  294. procedure TBrookHTTPCookie.Assign(ASource: TPersistent);
  295. var
  296. VSrc: TBrookHTTPCookie;
  297. begin
  298. if ASource is TBrookHTTPCookie then
  299. begin
  300. VSrc := ASource as TBrookHTTPCookie;
  301. FName := VSrc.Name;
  302. FValue := VSrc.Value;
  303. FDomain := VSrc.Domain;
  304. FPath := VSrc.Path;
  305. FExpires := VSrc.Expires;
  306. FHttpOnly := VSrc.HttpOnly;
  307. FSecure := VSrc.Secure;
  308. FMaxAge := VSrc.MaxAge;
  309. FSameSite := VSrc.SameSite;
  310. end
  311. else
  312. inherited Assign(ASource);
  313. end;
  314. function TBrookHTTPCookie.IsSigned: Boolean;
  315. begin
  316. Result := IsSigned(FValue);
  317. end;
  318. procedure TBrookHTTPCookie.Sign(const ASecret: string);
  319. begin
  320. FValue := Sign(ASecret, FValue);
  321. end;
  322. function TBrookHTTPCookie.TryUnsign(const ASecret: string): Boolean;
  323. var
  324. R: string;
  325. begin
  326. Result := TryUnsign(ASecret, FValue, R);
  327. if Result then
  328. FValue := R;
  329. end;
  330. procedure TBrookHTTPCookie.Unsign(const ASecret: string);
  331. begin
  332. FValue := Unsign(ASecret, FValue);
  333. end;
  334. function TBrookHTTPCookie.ToString: string;
  335. begin
  336. Result := Concat(FName, '=');
  337. if IsSigned then
  338. Result := Concat(Result, BROOK_COOKIE_SIG_PREFIX, FOriginalValue,
  339. FValue.SubString(BROOK_COOKIE_SIG_PREFIX.Length + FOriginalValue.Length))
  340. else
  341. Result := Concat(Result, FValue);
  342. if FMaxAge > -1 then
  343. Result := Concat(Result, '; Max-Age=', IntToStr(FMaxAge));
  344. if Length(FDomain) > 0 then
  345. Result := Concat(Result, '; Domain=', FDomain);
  346. if Length(FPath) > 0 then
  347. Result := Concat(Result, '; Path=', FPath);
  348. if FExpires > -1 then
  349. Result := Concat(Result, '; Expires=', Brook.DateTimeToGmt(FExpires));
  350. if FHttpOnly then
  351. Result := Concat(Result, '; HttpOnly');
  352. if FSecure then
  353. Result := Concat(Result, '; Secure');
  354. case FSameSite of
  355. ssStrict: Result := Concat(Result, '; SameSite=Strict');
  356. ssLax: Result := Concat(Result, '; SameSite=Lax');
  357. ssNone: ;
  358. end;
  359. end;
  360. procedure TBrookHTTPCookie.SetMaxAge(AValue: Integer);
  361. begin
  362. if AValue = FMaxAge then
  363. Exit;
  364. FMaxAge := AValue;
  365. if AValue > 0 then
  366. FExpires := Brook.DateTimeToUTC(IncSecond(Now, AValue))
  367. else
  368. Expire;
  369. end;
  370. procedure TBrookHTTPCookie.SetName(const AValue: string);
  371. begin
  372. if AValue = FName then
  373. Exit;
  374. if AValue.IsEmpty then
  375. raise EBrookHTTPCookie.Create(SBrookEmptyCookieName);
  376. if not IsValidIdent(AValue) then
  377. raise EBrookHTTPCookie.CreateFmt(SBrookInvalidCookieName, [AValue]);
  378. FName := AValue;
  379. end;
  380. procedure TBrookHTTPCookie.SetValue(const AValue: string);
  381. begin
  382. if AValue = FValue then
  383. Exit;
  384. FValue := AValue;
  385. FOriginalValue := FValue;
  386. end;
  387. procedure TBrookHTTPCookie.SetPath(const AValue: string);
  388. begin
  389. if AValue <> FPath then
  390. FPath := Brook.FixPath(AValue);
  391. end;
  392. procedure TBrookHTTPCookie.Clear;
  393. begin
  394. FValue := '';
  395. FMaxAge := -1;
  396. FDomain := '';
  397. FPath := '';
  398. FExpires := -1;
  399. FHTTPOnly := False;
  400. FSecure := False;
  401. FSameSite := ssNone;
  402. end;
  403. procedure TBrookHTTPCookie.Expire;
  404. begin
  405. FExpires := EncodeDate(1970, 1, 1);
  406. end;
  407. procedure TBrookHTTPCookie.Persist;
  408. begin
  409. FExpires := EncodeDate(9999, 12, 31) + EncodeTime(23, 59, 59, 999);
  410. end;
  411. { TBrookHTTPCookiesEnumerator }
  412. function TBrookHTTPCookiesEnumerator.GetCurrent: TBrookHTTPCookie;
  413. begin
  414. Result := TBrookHTTPCookie(inherited GetCurrent);
  415. end;
  416. { TBrookHTTPCookies }
  417. constructor TBrookHTTPCookies.Create(AOwner: TPersistent);
  418. begin
  419. inherited Create(AOwner, GetCookieClass);
  420. end;
  421. class function TBrookHTTPCookies.GetCookieClass: TBrookHTTPCookieClass;
  422. begin
  423. Result := TBrookHTTPCookie;
  424. end;
  425. function TBrookHTTPCookies.GetEnumerator: TBrookHTTPCookiesEnumerator;
  426. begin
  427. Result := TBrookHTTPCookiesEnumerator.Create(Self);
  428. end;
  429. procedure TBrookHTTPCookies.Assign(ASource: TPersistent);
  430. var
  431. C: TBrookHTTPCookie;
  432. begin
  433. if ASource is TBrookHTTPCookies then
  434. begin
  435. Clear;
  436. for C in (ASource as TBrookHTTPCookies) do
  437. Add.Assign(C);
  438. end
  439. else
  440. inherited Assign(ASource);
  441. end;
  442. function TBrookHTTPCookies.GetItem(AIndex: Integer): TBrookHTTPCookie;
  443. begin
  444. Result := TBrookHTTPCookie(inherited GetItem(AIndex));
  445. end;
  446. procedure TBrookHTTPCookies.SetItem(AIndex: Integer;
  447. AValue: TBrookHTTPCookie);
  448. begin
  449. inherited SetItem(AIndex, AValue);
  450. end;
  451. function TBrookHTTPCookies.Add: TBrookHTTPCookie;
  452. begin
  453. Result := TBrookHTTPCookie(inherited Add);
  454. end;
  455. function TBrookHTTPCookies.Remove(const AName: string): Boolean;
  456. var
  457. I: Integer;
  458. begin
  459. I := IndexOf(AName);
  460. Result := I > -1;
  461. if Result then
  462. inherited Delete(I);
  463. end;
  464. function TBrookHTTPCookies.IndexOf(const AName: string): Integer;
  465. begin
  466. for Result := 0 to Pred(Count) do
  467. if SameText(GetItem(Result).Name, AName) then
  468. Exit;
  469. Result := -1;
  470. end;
  471. function TBrookHTTPCookies.Find(const AName: string): TBrookHTTPCookie;
  472. var
  473. C: TBrookHTTPCookie;
  474. begin
  475. for C in Self do
  476. if SameText(C.Name, AName) then
  477. Exit(C);
  478. Result := nil;
  479. end;
  480. function TBrookHTTPCookies.First: TBrookHTTPCookie;
  481. begin
  482. if Count = 0 then
  483. Exit(nil);
  484. Result := GetItem(0);
  485. end;
  486. function TBrookHTTPCookies.Last: TBrookHTTPCookie;
  487. begin
  488. if Count = 0 then
  489. Exit(nil);
  490. Result := GetItem(Pred(Count));
  491. end;
  492. end.