BrookHTTPResponse.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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. begin
  323. SendBinary(@ABytes[0], ASize, AContentType, AStatus)
  324. end;
  325. procedure TBrookHTTPResponse.SendFile(ASize: NativeUInt; AMaxSize,
  326. AOffset: UInt64; const AFileName: TFileName; ADownloaded: Boolean;
  327. AStatus: Word);
  328. var
  329. M: TMarshaller;
  330. R: cint;
  331. begin
  332. CheckStatus(AStatus);
  333. SgLib.Check;
  334. if FCompressed then
  335. begin
  336. R := sg_httpres_zsendfile(FHandle, ASize, AMaxSize, AOffset,
  337. M.ToCString(AFileName), ADownloaded, AStatus);
  338. CheckZLib(R);
  339. end
  340. else
  341. R := sg_httpres_sendfile(FHandle, ASize, AMaxSize, AOffset,
  342. M.ToCString(AFileName), ADownloaded, AStatus);
  343. CheckAlreadySent(R);
  344. if R = ENOENT then
  345. raise EFileNotFoundException.Create(SFileNotFound);
  346. SgLib.CheckLastError(R);
  347. end;
  348. procedure TBrookHTTPResponse.SendStream(AStream: TStream; AFreed: Boolean;
  349. AStatus: Word);
  350. var
  351. FCb: sg_free_cb;
  352. R: cint;
  353. begin
  354. CheckStream(AStream);
  355. CheckStatus(AStatus);
  356. if AFreed and (not SgLib.IsLoaded) then
  357. AStream.Free;
  358. SgLib.Check;
  359. if AFreed then
  360. FCb := DoStreamFree
  361. else
  362. FCb := nil;
  363. if FCompressed then
  364. begin
  365. R := sg_httpres_zsendstream(FHandle, DoStreamRead, AStream, FCb, AStatus);
  366. CheckZLib(R);
  367. end
  368. else
  369. R := sg_httpres_sendstream(FHandle, 0, DoStreamRead, AStream, FCb, AStatus);
  370. CheckAlreadySent(R);
  371. SgLib.CheckLastError(R);
  372. end;
  373. procedure TBrookHTTPResponse.SendStream(AStream: TStream; AStatus: Word);
  374. begin
  375. SendStream(AStream, True, AStatus);
  376. end;
  377. procedure TBrookHTTPResponse.SendEmpty(const AContentType: string);
  378. begin
  379. Reset;
  380. Send('', AContentType, 204);
  381. end;
  382. procedure TBrookHTTPResponse.SendEmpty;
  383. begin
  384. Reset;
  385. Send('', '', 204);
  386. end;
  387. procedure TBrookHTTPResponse.SendAndRedirect(const AValue, ADestination,
  388. AContentType: string; AStatus: Word);
  389. begin
  390. if (AStatus < 300) or (AStatus > 307) then
  391. raise EBrookHTTPResponse.CreateFmt(SBrookInvalidHTTPStatus, [AStatus]);
  392. FHeaders.AddOrSet('Location', ADestination);
  393. Send(AValue, AContentType, AStatus);
  394. end;
  395. procedure TBrookHTTPResponse.SendAndRedirect(const AValue, ADestination,
  396. AContentType: string);
  397. begin
  398. SendAndRedirect(AValue, ADestination, AContentType, 302);
  399. end;
  400. procedure TBrookHTTPResponse.Download(const AFileName: TFileName;
  401. AStatus: Word);
  402. var
  403. M: TMarshaller;
  404. R: cint;
  405. begin
  406. SgLib.Check;
  407. if FCompressed then
  408. begin
  409. R := sg_httpres_zdownload(FHandle, M.ToCString(AFileName), AStatus);
  410. CheckZLib(R);
  411. end
  412. else
  413. R := sg_httpres_download(FHandle, M.ToCString(AFileName), AStatus);
  414. CheckAlreadySent(R);
  415. if R = ENOENT then
  416. raise EFileNotFoundException.Create(SFileNotFound);
  417. SgLib.CheckLastError(R);
  418. end;
  419. procedure TBrookHTTPResponse.Download(const AFileName: TFileName);
  420. begin
  421. Download(AFileName, 200);
  422. end;
  423. procedure TBrookHTTPResponse.Render(const AFileName: TFileName;
  424. AStatus: Word);
  425. var
  426. M: TMarshaller;
  427. R: cint;
  428. begin
  429. SgLib.Check;
  430. if FCompressed then
  431. begin
  432. R := sg_httpres_zrender(FHandle, M.ToCString(AFileName), AStatus);
  433. CheckZLib(R);
  434. end
  435. else
  436. R := sg_httpres_render(FHandle, M.ToCString(AFileName), AStatus);
  437. CheckAlreadySent(R);
  438. if R = ENOENT then
  439. raise EFileNotFoundException.Create(SFileNotFound);
  440. SgLib.CheckLastError(R);
  441. end;
  442. procedure TBrookHTTPResponse.Render(const AFileName: TFileName);
  443. begin
  444. Render(AFileName, 200);
  445. end;
  446. procedure TBrookHTTPResponse.Reset;
  447. begin
  448. SgLib.Check;
  449. SgLib.CheckLastError(sg_httpres_reset(FHandle));
  450. end;
  451. procedure TBrookHTTPResponse.Clear;
  452. begin
  453. SgLib.Check;
  454. SgLib.CheckLastError(sg_httpres_clear(FHandle));
  455. end;
  456. function TBrookHTTPResponse.IsEmpty: Boolean;
  457. begin
  458. SgLib.Check;
  459. Result := sg_httpres_is_empty(FHandle);
  460. end;
  461. end.