BrookHTTPResponse.pas 15 KB

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