BrookHTTPResponse.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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 class which dispatches data to the client. }
  26. unit BrookHTTPResponse;
  27. {$I BrookDefines.inc}
  28. interface
  29. uses
  30. RTLConsts,
  31. SysConst,
  32. SysUtils,
  33. Classes,
  34. Platform,
  35. Marshalling,
  36. libsagui,
  37. BrookHandledClasses,
  38. BrookStringMap,
  39. BrookHTTPCookies;
  40. resourcestring
  41. { Error message @code('Invalid status code: <code>.'). }
  42. SBrookInvalidHTTPStatus = 'Invalid status code: %d.';
  43. { Error message @code('Response already sent.'). }
  44. SBrookResponseAlreadySent = 'Response already sent.';
  45. { Error message @code('Generic ZLib error.'). }
  46. SBrookZLibError = 'Generic ZLib error.';
  47. type
  48. { Handles exceptions related to response class. }
  49. EBrookHTTPResponse = class(Exception);
  50. { Class which dispatches headers, contents, binaries, files and other data to
  51. the client. }
  52. TBrookHTTPResponse = class(TBrookHandledPersistent)
  53. private
  54. FCookies: TBrookHTTPCookies;
  55. FHeaders: TBrookStringMap;
  56. FHandle: Psg_httpres;
  57. FCompressed: Boolean;
  58. procedure SetCookies(AValue: TBrookHTTPCookies);
  59. protected
  60. class function DoStreamRead(Acls: Pcvoid; Aoffset: cuint64_t; Abuf: Pcchar;
  61. Asize: csize_t): cssize_t; cdecl; static;
  62. class procedure DoStreamFree(Acls: Pcvoid); cdecl; static;
  63. class procedure CheckStatus(AStatus: Word); static;
  64. {$IFNDEF DEBUG}inline;{$ENDIF}
  65. class procedure CheckStream(AStream: TStream); static;
  66. {$IFNDEF DEBUG}inline;{$ENDIF}
  67. function CreateHeaders(AHandle: Pointer): TBrookStringMap; virtual;
  68. function CreateCookies(AOwner: TPersistent): TBrookHTTPCookies; virtual;
  69. function GetHandle: Pointer; override;
  70. procedure CheckAlreadySent(Aret: cint); {$IFNDEF DEBUG}inline;{$ENDIF}
  71. procedure CheckZLib(Aret: cint); {$IFNDEF DEBUG}inline;{$ENDIF}
  72. public
  73. { Creates an instance of @code(TBrookHTTPResponse).
  74. @param(AHandle[in] Request handle.) }
  75. constructor Create(AHandle: Pointer); virtual;
  76. { Frees an instance of @code(TBrookHTTPResponse). }
  77. destructor Destroy; override;
  78. { Sets server cookie to the response handle.
  79. @param(AName[in] Cookie name.)
  80. @param(AValue[in] Cookie value.) }
  81. procedure SetCookie(const AName, AValue: string); virtual;
  82. { Sends a string content to the client.
  83. @param(AValue[in] String to be sent.)
  84. @param(AContentType[in] Content type.)
  85. @param(AStatus[in] HTTP status code.) }
  86. procedure Send(const AValue, AContentType: string; AStatus: Word); virtual;
  87. { Sends a formatted string content to the client.
  88. @param(AFmt[in] Format string.)
  89. @param(AArgs[in] Arguments to compose the formatted string.)
  90. @param(AContentType[in] Content type.)
  91. @param(AStatus[in] HTTP status code.) }
  92. procedure SendFmt(const AFormat: string; const AArgs: array of const;
  93. const AContentType: string; AStatus: Word); virtual;
  94. { Sends a binary content to the client.
  95. @param(ABinary[in] Binary content to be sent.)
  96. @param(ASize[in] Content size.)
  97. @param(AContentType[in] Content type.)
  98. @param(AStatus[in] HTTP status code.) }
  99. procedure SendBinary(ABuffer: Pointer; ASize: NativeUInt;
  100. const AContentType: string; AStatus: Word); virtual;
  101. { Sends an array of Bytes content to the client.
  102. @param(ABytes[in] Array of Bytes to be sent.)
  103. @param(ASize[in] Content size.)
  104. @param(AContentType[in] Content type.)
  105. @param(AStatus[in] HTTP status code.) }
  106. procedure SendBytes(const ABytes: TBytes; ASize: NativeUInt;
  107. const AContentType: string; AStatus: Word); virtual;
  108. { Sends a file to the client.
  109. @param(ASize[in] Size of the file to be sent. Use zero to calculate
  110. automatically.)
  111. @param(AMaxSize[in] Maximum allowed file size. Use zero for no limit.)
  112. @param(AOffset[in] Offset to start reading from in the file to be sent.)
  113. @param(AFileName[in] Path of the file to be sent.)
  114. @param(ADownloaded[in] If @True it offers the file as download.)
  115. @param(AStatus[in] HTTP status code.) }
  116. procedure SendFile(ASize: NativeUInt; AMaxSize, AOffset: UInt64;
  117. const AFileName: TFileName; ADownloaded: Boolean; AStatus: Word); virtual;
  118. { Sends a stream to the client.
  119. @param(AStream[in] Stream to be sent.)
  120. @param(AFreed[in] @True frees the stream automatically as soon as it
  121. is sent.)
  122. @param(AStatus[in] HTTP status code.) }
  123. procedure SendStream(AStream: TStream; AFreed: Boolean;
  124. AStatus: Word); overload; virtual;
  125. { Sends a stream to the client. The stream is freed automatically as soon as
  126. it is sent.
  127. @param(AStream[in] Stream to be sent.)
  128. @param(AStatus[in] HTTP status code.) }
  129. procedure SendStream(AStream: TStream; AStatus: Word); overload; virtual;
  130. { Sends an HTTP status 204 to the client indicating the server has fulfilled
  131. the request, but does not need to return a content.
  132. @param(AContentType[in] Content type.) }
  133. procedure SendEmpty(const AContentType: string); overload; virtual;
  134. { Sends an HTTP status 204 to the client indicating the server has fulfilled
  135. the request, but does not need to return a content. }
  136. procedure SendEmpty; overload; virtual;
  137. { Sends a string content to the client and redirects it to a new location.
  138. @param(AValue[in] String to be sent.)
  139. @param(ADestination[in] Destination to which it will be redirected as soon
  140. as the content is sent.)
  141. @param(AContentType[in] Content type.)
  142. @param(AStatus[in] HTTP status code (must be >=300 and <=307).) }
  143. procedure SendAndRedirect(const AValue, ADestination, AContentType: string;
  144. AStatus: Word); overload; virtual;
  145. { Sends a string content to the client with HTTP status 302 and redirects it
  146. to a new location.
  147. @param(AValue[in] String to be sent.)
  148. @param(ADestination[in] Destination to which it will be redirected as soon
  149. as the content is sent.)
  150. @param(AContentType[in] Content type.) }
  151. procedure SendAndRedirect(const AValue, ADestination,
  152. AContentType: string); overload; virtual;
  153. { Offers a file as download.
  154. @param(AFileName[in] Path of the file to be sent.)
  155. @param(AStatus[in] HTTP status code.) }
  156. procedure Download(const AFileName: TFileName;
  157. AStatus: Word); overload; virtual;
  158. { Sends a file to be rendered.
  159. @param(AFileName[in] Path of the file to be sent.)
  160. @param(AStatus[in] HTTP status code.) }
  161. procedure Render(const AFileName: TFileName;
  162. AStatus: Word); overload; virtual;
  163. { Offers a file as download.
  164. @param(AFileName[in] Path of the file to be sent.) }
  165. procedure Download(const AFileName: TFileName); overload; virtual;
  166. { Sends a file to be rendered.
  167. @param(AFileName[in] Path of the file to be sent.) }
  168. procedure Render(const AFileName: TFileName); overload; virtual;
  169. { Resets status and internal buffers of the response handle preserving all
  170. headers and cookies. }
  171. procedure Reset; virtual;
  172. { Clears all headers, cookies, status and internal buffers of the response
  173. object. }
  174. procedure Clear; virtual;
  175. { Checks if the response is empty. }
  176. function IsEmpty: Boolean;
  177. { Determines if the content must be compressed while sending.
  178. The compression is done by the ZLib library using the DEFLATE compression
  179. algorithm. It uses the Gzip format when the content is a file. }
  180. property Compressed: Boolean read FCompressed write FCompressed;
  181. { Hash table containing the headers to be sent to the client. }
  182. property Headers: TBrookStringMap read FHeaders;
  183. { Cookies to be sent to the client. }
  184. property Cookies: TBrookHTTPCookies read FCookies write SetCookies;
  185. { Determines if the response is empty. }
  186. property Empty: Boolean read IsEmpty; //FI:C110
  187. end;
  188. implementation
  189. constructor TBrookHTTPResponse.Create(AHandle: Pointer);
  190. begin
  191. inherited Create;
  192. FHandle := AHandle;
  193. FHeaders := CreateHeaders(sg_httpres_headers(FHandle));
  194. FCookies := CreateCookies(Self);
  195. end;
  196. destructor TBrookHTTPResponse.Destroy;
  197. var
  198. C: TBrookHTTPCookie;
  199. begin
  200. for C in FCookies do
  201. FHeaders.Add('Set-Cookie', C.ToString);
  202. FCookies.Free;
  203. FHeaders.Free;
  204. inherited Destroy;
  205. end;
  206. function TBrookHTTPResponse.GetHandle: Pointer;
  207. begin
  208. Result := FHandle;
  209. end;
  210. procedure TBrookHTTPResponse.CheckAlreadySent(Aret: cint);
  211. begin
  212. if Aret = EALREADY then
  213. raise EBrookHTTPResponse.Create(SBrookResponseAlreadySent);
  214. end;
  215. procedure TBrookHTTPResponse.CheckZLib(Aret: cint);
  216. begin
  217. if Aret < 0 then
  218. raise EBrookHTTPResponse.Create(SBrookZLibError);
  219. end;
  220. class procedure TBrookHTTPResponse.CheckStatus(AStatus: Word);
  221. begin
  222. if (AStatus < 100) or (AStatus > 599) then
  223. raise EArgumentException.CreateFmt(SBrookInvalidHTTPStatus, [AStatus]);
  224. end;
  225. class procedure TBrookHTTPResponse.CheckStream(AStream: TStream);
  226. begin
  227. if not Assigned(AStream) then
  228. raise EArgumentNilException.CreateFmt(SParamIsNil, ['AStream']);
  229. end;
  230. function TBrookHTTPResponse.CreateHeaders(AHandle: Pointer): TBrookStringMap;
  231. begin
  232. Result := TBrookStringMap.Create(AHandle);
  233. Result.ClearOnDestroy := False;
  234. end;
  235. function TBrookHTTPResponse.CreateCookies(AOwner: TPersistent): TBrookHTTPCookies;
  236. begin
  237. Result := TBrookHTTPCookies.Create(AOwner);
  238. end;
  239. {$IFDEF FPC}
  240. {$PUSH}{$WARN 5024 OFF}
  241. {$ENDIF}
  242. class function TBrookHTTPResponse.DoStreamRead(
  243. Acls: Pcvoid; Aoffset: cuint64_t; //FI:O804
  244. Abuf: Pcchar; Asize: csize_t): cssize_t;
  245. begin
  246. Result := TStream(Acls).Read(Abuf^, Asize);
  247. if Result = 0 then
  248. Exit(sg_eor(False));
  249. if Result < 0 then
  250. Result := sg_eor(True);
  251. end;
  252. {$IFDEF FPC}
  253. {$POP}
  254. {$ENDIF}
  255. class procedure TBrookHTTPResponse.DoStreamFree(Acls: Pcvoid);
  256. begin
  257. TStream(Acls).Free;
  258. end;
  259. procedure TBrookHTTPResponse.SetCookies(AValue: TBrookHTTPCookies);
  260. begin
  261. if AValue = FCookies then
  262. Exit;
  263. if Assigned(AValue) then
  264. FCookies.Assign(AValue)
  265. else
  266. FCookies.Clear;
  267. end;
  268. procedure TBrookHTTPResponse.SetCookie(const AName, AValue: string);
  269. var
  270. M: TMarshaller;
  271. begin
  272. SgLib.Check;
  273. SgLib.CheckLastError(sg_httpres_set_cookie(FHandle, M.ToCString(AName),
  274. M.ToCString(AValue)));
  275. end;
  276. procedure TBrookHTTPResponse.Send(const AValue, AContentType: string;
  277. AStatus: Word);
  278. var
  279. M: TMarshaller;
  280. R: cint;
  281. begin
  282. SgLib.Check;
  283. if FCompressed then
  284. begin
  285. R := sg_httpres_zsendbinary(FHandle, M.ToCString(AValue), Length(AValue),
  286. M.ToCString(AContentType), AStatus);
  287. CheckZLib(R);
  288. end
  289. else
  290. R := sg_httpres_sendbinary(FHandle, M.ToCString(AValue), M.Length(AValue),
  291. M.ToCString(AContentType), AStatus);
  292. CheckAlreadySent(R);
  293. SgLib.CheckLastError(R);
  294. end;
  295. procedure TBrookHTTPResponse.SendFmt(const AFormat: string;
  296. const AArgs: array of const; const AContentType: string; AStatus: Word);
  297. begin
  298. Send(Format(AFormat, AArgs), AContentType, AStatus);
  299. end;
  300. procedure TBrookHTTPResponse.SendBinary(ABuffer: Pointer; ASize: NativeUInt;
  301. const AContentType: string; AStatus: Word);
  302. var
  303. M: TMarshaller;
  304. R: cint;
  305. begin
  306. CheckStatus(AStatus);
  307. SgLib.Check;
  308. if FCompressed then
  309. begin
  310. R := sg_httpres_zsendbinary(FHandle, ABuffer, ASize,
  311. M.ToCString(AContentType), AStatus);
  312. CheckZLib(R);
  313. end
  314. else
  315. R := sg_httpres_sendbinary(FHandle, ABuffer, ASize,
  316. M.ToCString(AContentType), AStatus);
  317. CheckAlreadySent(R);
  318. SgLib.CheckLastError(R);
  319. end;
  320. procedure TBrookHTTPResponse.SendBytes(const ABytes: TBytes; ASize: NativeUInt;
  321. const AContentType: string; AStatus: Word);
  322. var
  323. WorkAround: TBytes;
  324. begin
  325. if Length(ABytes) > 0 then
  326. SendBinary(@ABytes[0], ASize, AContentType, AStatus)
  327. else
  328. begin
  329. SetLength(WorkAround, 1);
  330. SendBinary(@WorkAround[0], ASize, AContentType, AStatus)
  331. end;
  332. end;
  333. procedure TBrookHTTPResponse.SendFile(ASize: NativeUInt; AMaxSize,
  334. AOffset: UInt64; const AFileName: TFileName; ADownloaded: Boolean;
  335. AStatus: Word);
  336. var
  337. M: TMarshaller;
  338. R: cint;
  339. begin
  340. CheckStatus(AStatus);
  341. SgLib.Check;
  342. if FCompressed then
  343. begin
  344. R := sg_httpres_zsendfile(FHandle, ASize, AMaxSize, AOffset,
  345. M.ToCString(AFileName), ADownloaded, AStatus);
  346. CheckZLib(R);
  347. end
  348. else
  349. R := sg_httpres_sendfile(FHandle, ASize, AMaxSize, AOffset,
  350. M.ToCString(AFileName), ADownloaded, AStatus);
  351. CheckAlreadySent(R);
  352. if R = ENOENT then
  353. raise EFileNotFoundException.Create(SFileNotFound);
  354. SgLib.CheckLastError(R);
  355. end;
  356. procedure TBrookHTTPResponse.SendStream(AStream: TStream; AFreed: Boolean;
  357. AStatus: Word);
  358. var
  359. FCb: sg_free_cb;
  360. R: cint;
  361. begin
  362. CheckStream(AStream);
  363. CheckStatus(AStatus);
  364. if AFreed and (not SgLib.IsLoaded) then
  365. AStream.Free;
  366. SgLib.Check;
  367. if AFreed then
  368. FCb := DoStreamFree
  369. else
  370. FCb := nil;
  371. if FCompressed then
  372. begin
  373. R := sg_httpres_zsendstream(FHandle, DoStreamRead, AStream, FCb, AStatus);
  374. CheckZLib(R);
  375. end
  376. else
  377. R := sg_httpres_sendstream(FHandle, 0, DoStreamRead, AStream, FCb, AStatus);
  378. CheckAlreadySent(R);
  379. SgLib.CheckLastError(R);
  380. end;
  381. procedure TBrookHTTPResponse.SendStream(AStream: TStream; AStatus: Word);
  382. begin
  383. SendStream(AStream, True, AStatus);
  384. end;
  385. procedure TBrookHTTPResponse.SendEmpty(const AContentType: string);
  386. begin
  387. Reset;
  388. Send('', AContentType, 204);
  389. end;
  390. procedure TBrookHTTPResponse.SendEmpty;
  391. begin
  392. Reset;
  393. Send('', '', 204);
  394. end;
  395. procedure TBrookHTTPResponse.SendAndRedirect(const AValue, ADestination,
  396. AContentType: string; AStatus: Word);
  397. begin
  398. if (AStatus < 300) or (AStatus > 307) then
  399. raise EBrookHTTPResponse.CreateFmt(SBrookInvalidHTTPStatus, [AStatus]);
  400. FHeaders.AddOrSet('Location', ADestination);
  401. Send(AValue, AContentType, AStatus);
  402. end;
  403. procedure TBrookHTTPResponse.SendAndRedirect(const AValue, ADestination,
  404. AContentType: string);
  405. begin
  406. SendAndRedirect(AValue, ADestination, AContentType, 302);
  407. end;
  408. procedure TBrookHTTPResponse.Download(const AFileName: TFileName;
  409. AStatus: Word);
  410. var
  411. M: TMarshaller;
  412. R: cint;
  413. begin
  414. SgLib.Check;
  415. if FCompressed then
  416. begin
  417. R := sg_httpres_zdownload(FHandle, M.ToCString(AFileName), AStatus);
  418. CheckZLib(R);
  419. end
  420. else
  421. R := sg_httpres_download(FHandle, M.ToCString(AFileName), AStatus);
  422. CheckAlreadySent(R);
  423. if R = ENOENT then
  424. raise EFileNotFoundException.Create(SFileNotFound);
  425. SgLib.CheckLastError(R);
  426. end;
  427. procedure TBrookHTTPResponse.Download(const AFileName: TFileName);
  428. begin
  429. Download(AFileName, 200);
  430. end;
  431. procedure TBrookHTTPResponse.Render(const AFileName: TFileName;
  432. AStatus: Word);
  433. var
  434. M: TMarshaller;
  435. R: cint;
  436. begin
  437. SgLib.Check;
  438. if FCompressed then
  439. begin
  440. R := sg_httpres_zrender(FHandle, M.ToCString(AFileName), AStatus);
  441. CheckZLib(R);
  442. end
  443. else
  444. R := sg_httpres_render(FHandle, M.ToCString(AFileName), AStatus);
  445. CheckAlreadySent(R);
  446. if R = ENOENT then
  447. raise EFileNotFoundException.Create(SFileNotFound);
  448. SgLib.CheckLastError(R);
  449. end;
  450. procedure TBrookHTTPResponse.Render(const AFileName: TFileName);
  451. begin
  452. Render(AFileName, 200);
  453. end;
  454. procedure TBrookHTTPResponse.Reset;
  455. begin
  456. SgLib.Check;
  457. SgLib.CheckLastError(sg_httpres_reset(FHandle));
  458. end;
  459. procedure TBrookHTTPResponse.Clear;
  460. begin
  461. SgLib.Check;
  462. SgLib.CheckLastError(sg_httpres_clear(FHandle));
  463. end;
  464. function TBrookHTTPResponse.IsEmpty: Boolean;
  465. begin
  466. SgLib.Check;
  467. Result := sg_httpres_is_empty(FHandle);
  468. end;
  469. end.