Quick.HttpServer.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. { ***************************************************************************
  2. Copyright (c) 2016-2020 Kike Pérez
  3. Unit : Quick.HttpServer
  4. Description : Http Server
  5. Author : Kike Pérez
  6. Version : 1.8
  7. Created : 30/08/2019
  8. Modified : 11/06/2020
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.HttpServer;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. {$IFDEF DEBUG_HTTPSERVER}
  26. Quick.Debug.Utils,
  27. {$ENDIF}
  28. SysUtils,
  29. Classes,
  30. IdHTTPServer,
  31. IdCustomHTTPServer,
  32. IdSSLOpenSSL,
  33. IdContext,
  34. Quick.Commons,
  35. Quick.Value,
  36. Quick.Logger.Intf,
  37. Quick.HttpServer.Types,
  38. Quick.HttpServer.Request,
  39. Quick.HttpServer.Response;
  40. type
  41. EHttpProtocolError = class(Exception);
  42. TRequestEvent = procedure(aRequest : IHttpRequest; aResponse : IHttpResponse) of object;
  43. TOnConnectEvent = procedure of object;
  44. TOnDisconnectEvent = procedure of object;
  45. TCustomErrorPages = class
  46. private
  47. fPath : string;
  48. fDynamicErrorPage : Boolean;
  49. fEnabled : Boolean;
  50. public
  51. property Path : string read fPath write fPath;
  52. property DynamicErrorPage : Boolean read fDynamicErrorPage write fDynamicErrorPage;
  53. property Enabled : Boolean read fEnabled write fEnabled;
  54. end;
  55. IHttpServer = interface
  56. ['{3B48198A-49F7-40A5-BBFD-39C78B6FA1EA}']
  57. procedure SetOnRequest(aRequestEvent : TRequestEvent);
  58. function GetOnRequest : TRequestEvent;
  59. function GetCustomErrorPages: TCustomErrorPages;
  60. procedure SetCustomErrorPages(const Value: TCustomErrorPages);
  61. function GetHost: string;
  62. function GetPort: Integer;
  63. property OnNewRequest : TRequestEvent read GetOnRequest write SetOnRequest;
  64. property CustomErrorPages : TCustomErrorPages read GetCustomErrorPages write SetCustomErrorPages;
  65. property Host : string read GetHost;
  66. property Port : Integer read GetPort;
  67. function Logger : ILogger;
  68. procedure Start;
  69. procedure Stop;
  70. end;
  71. TCustomHttpServer = class(TInterfacedObject,IHttpServer)
  72. private
  73. fLogger : ILogger;
  74. fOnConnect : TOnConnectEvent;
  75. fOnDisconnect : TOnDisconnectEvent;
  76. fCustomErrorPages : TCustomErrorPages;
  77. procedure SetOnRequest(aRequestEvent : TRequestEvent);
  78. function GetOnRequest : TRequestEvent;
  79. function GetCustomErrorPages: TCustomErrorPages;
  80. procedure SetCustomErrorPages(const Value: TCustomErrorPages);
  81. function GetHost: string;
  82. function GetPort: Integer;
  83. protected
  84. fOnRequest : TRequestEvent;
  85. fHost : string;
  86. fPort : Integer;
  87. fSSLSecured : Boolean;
  88. procedure GetErrorPage(const aURL : string; aResponse : IHttpResponse); virtual;
  89. public
  90. constructor Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil); virtual;
  91. destructor Destroy; override;
  92. property Host : string read GetHost;
  93. property Port : Integer read GetPort;
  94. property CustomErrorPages : TCustomErrorPages read GetCustomErrorPages write SetCustomErrorPages;
  95. property OnNewRequest : TRequestEvent read GetOnRequest write SetOnRequest;
  96. property OnConnect : TOnConnectEvent read fOnConnect write fOnConnect;
  97. property OnDisconnect : TOnDisconnectEvent read fOnDisconnect write fOnDisconnect;
  98. function Logger : ILogger;
  99. procedure Start; virtual; abstract;
  100. procedure Stop; virtual; abstract;
  101. end;
  102. THttpServer = class(TCustomHttpServer)
  103. private
  104. fHTTPServer : TidHTTPServer;
  105. procedure OnGetRequest(aContext: TIdContext; aRequestInfo: TIdHTTPRequestInfo; aResponseInfo: TIdHTTPResponseInfo);
  106. function GetSSLIOHandler : TIdServerIOHandlerSSLOpenSSL;
  107. function OnVerifyPeer(aCertificate: TIdX509; aOk: Boolean; aDepth, aError: Integer): Boolean;
  108. function GetRequestInfo(aRequestInfo : TIdHTTPRequestInfo) : THttpRequest;
  109. procedure SetResponseInfo(aResponseInfo : TIdHTTPResponseInfo; aResponse : IHttpResponse);
  110. procedure DoOnQuerySSLPort(aPort: Word; var vUseSSL: Boolean);
  111. procedure DoConnect(aContext: TIdContext);
  112. procedure DoDisconnect(aContext: TIdContext);
  113. protected
  114. procedure ProcessRequest(aRequest: IHttpRequest; aResponse: IHttpResponse); virtual;
  115. public
  116. constructor Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil); override;
  117. destructor Destroy; override;
  118. procedure Start; override;
  119. procedure Stop; override;
  120. end;
  121. implementation
  122. { TCustomHttpServer }
  123. constructor TCustomHttpServer.Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil);
  124. begin
  125. fCustomErrorPages := TCustomErrorPages.Create;
  126. fCustomErrorPages.Path := '.';
  127. fCustomErrorPages.DynamicErrorPage := False;
  128. fCustomErrorPages.Enabled := False;
  129. if aHost.IsEmpty then fHost := '127.0.0.1'
  130. else fHost := aHost;
  131. {$IFDEF DELPHILINUX}
  132. if fHost = '127.0.0.1' then fHost := '0.0.0.0';
  133. {$ENDIF}
  134. fPort := aPort;
  135. if aLogger = nil then
  136. begin
  137. fLogger := TNullLogger.Create;
  138. end
  139. else fLogger := aLogger;
  140. fSSLSecured := aSSLEnabled;
  141. end;
  142. destructor TCustomHttpServer.Destroy;
  143. begin
  144. fCustomErrorPages.Free;
  145. inherited;
  146. end;
  147. function TCustomHttpServer.GetCustomErrorPages: TCustomErrorPages;
  148. begin
  149. Result := fCustomErrorPages;
  150. end;
  151. procedure TCustomHttpServer.GetErrorPage(const aURL : string; aResponse : IHttpResponse);
  152. var
  153. filestream : TFileStream;
  154. pagestream : TStringStream;
  155. pagefilename : string;
  156. found : Boolean;
  157. content : string;
  158. begin
  159. content := '';
  160. found := False;
  161. if (fCustomErrorPages.Enabled) then
  162. begin
  163. pagestream := TStringStream.Create;
  164. try
  165. //get specific error filename
  166. pagefilename := Format('%s\%d.html',[fCustomErrorPages.Path,aResponse.StatusCode]);
  167. found := FileExists(pagefilename);
  168. //get generic error type filanema
  169. if not found then
  170. begin
  171. pagefilename := Format('%s\%sxx.html',[fCustomErrorPages.Path,(aResponse.StatusCode).ToString[Low(string)]]);
  172. found := FileExists(pagefilename);
  173. end;
  174. //get generic error filename
  175. if not found then
  176. begin
  177. pagefilename := Format('%s\error.html',[fCustomErrorPages.Path]);
  178. found := FileExists(pagefilename);
  179. end;
  180. if found then
  181. begin
  182. filestream := TFileStream.Create(pagefilename,fmShareDenyNone);
  183. try
  184. pagestream.CopyFrom(filestream,filestream.Size);
  185. finally
  186. filestream.Free;
  187. end;
  188. content := pagestream.DataString;
  189. if fCustomErrorPages.DynamicErrorPage then
  190. begin
  191. content := StringReplace(content,'{{URL}}',aURL,[rfReplaceAll,rfIgnoreCase]);
  192. content := StringReplace(content,'{{STATUSCODE}}',aResponse.StatusCode.ToString,[rfReplaceAll,rfIgnoreCase]);
  193. content := StringReplace(content,'{{STATUSTEXT}}',aResponse.StatusText,[rfReplaceAll,rfIgnoreCase]);
  194. content := StringReplace(content,'{{CONTENT}}',aResponse.ContentText,[rfReplaceAll,rfIgnoreCase]);
  195. end;
  196. end;
  197. finally
  198. pagestream.Free;
  199. end;
  200. end;
  201. if not found then
  202. begin
  203. aResponse.ContentText := Format('<h2>%d Error: %s</h2>',[aResponse.StatusCode,aResponse.StatusText])
  204. + Format('<h4>Message: %s</h4>',[aResponse.ContentText]);
  205. end
  206. else aResponse.ContentText := content;
  207. end;
  208. function TCustomHttpServer.GetHost: string;
  209. begin
  210. Result := fHost;
  211. end;
  212. function TCustomHttpServer.GetOnRequest: TRequestEvent;
  213. begin
  214. Result := fOnRequest;
  215. end;
  216. function TCustomHttpServer.GetPort: Integer;
  217. begin
  218. Result := fPort;
  219. end;
  220. procedure TCustomHttpServer.SetCustomErrorPages(const Value: TCustomErrorPages);
  221. begin
  222. fCustomErrorPages := Value;
  223. end;
  224. procedure TCustomHttpServer.SetOnRequest(aRequestEvent: TRequestEvent);
  225. begin
  226. fOnRequest := aRequestEvent;
  227. end;
  228. function TCustomHttpServer.Logger: ILogger;
  229. begin
  230. Result := fLogger;
  231. end;
  232. { THTTPServer }
  233. constructor THTTPServer.Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil);
  234. begin
  235. inherited Create(aHost, aPort, aSSLEnabled, aLogger);
  236. Logger.Info('HTTPServer: Indy');
  237. fHTTPServer := TIdHTTPServer.Create(nil);
  238. fHTTPServer.Bindings.Clear; //make sure there's no other bindings
  239. with fHTTPServer.Bindings.Add do
  240. begin
  241. IP := fHost;
  242. Port := fPort;
  243. end;
  244. if fSSLSecured then fHTTPServer.IOHandler := GetSSLIOHandler;
  245. fHTTPServer.OnCommandGet := OnGetRequest;
  246. fHTTPServer.OnCommandOther := OnGetRequest;
  247. fHTTPServer.OnConnect := DoConnect;
  248. fHTTPServer.OnDisconnect := DoDisconnect;
  249. //fHTTPServer.OnExecute := DoConnect;
  250. fHTTPServer.OnQuerySSLPort := DoOnQuerySSLPort;
  251. fHTTPServer.ServerSoftware := 'Quick.HttpServer';
  252. end;
  253. destructor THTTPServer.Destroy;
  254. begin
  255. if Assigned(fHTTPServer) then
  256. begin
  257. if Assigned(fHTTPServer.IOHandler) then fHTTPServer.IOHandler.Free;
  258. fHTTPServer.Free;
  259. end;
  260. inherited;
  261. end;
  262. function THTTPServer.GetSSLIOHandler : TIdServerIOHandlerSSLOpenSSL;
  263. begin
  264. Result := TIdServerIOHandlerSSLOpenSSL.Create(nil);
  265. //Result.SSLOptions.RootCertFile := '.\ca.cert.pem';
  266. Result.SSLOptions.CertFile := '.\server.cert.pem';
  267. Result.SSLOptions.KeyFile := '.\server.key.pem';
  268. Result.SSLOptions.Method := sslvSSLv23;
  269. Result.SSLOptions.Mode := sslmServer;
  270. Result.OnVerifyPeer := OnVerifyPeer;
  271. end;
  272. function THTTPServer.OnVerifyPeer(aCertificate: TIdX509; aOk: Boolean; aDepth, aError: Integer): Boolean;
  273. begin
  274. Result := aOk;
  275. end;
  276. function THttpServer.GetRequestInfo(aRequestInfo: TIdHTTPRequestInfo): THttpRequest;
  277. var
  278. i : Integer;
  279. uhost : TArray<string>;
  280. begin
  281. Result := THttpRequest.Create;
  282. if aRequestInfo.Host.Contains(':') then
  283. begin
  284. uhost := aRequestInfo.Host.Split([':']);
  285. Result.Host := uhost[0];
  286. Result.Port := StrToIntDef(uhost[1],80);
  287. end
  288. else Result.Host := aRequestInfo.Host;
  289. Result.URL := aRequestInfo.URI;
  290. Result.ClientIP := aRequestInfo.RemoteIP;
  291. Result.UnParsedParams := aRequestInfo.QueryParams;
  292. Result.SetMethodFromString(aRequestInfo.Command);
  293. Result.UserAgent := aRequestInfo.UserAgent;
  294. Result.CacheControl := aRequestInfo.CacheControl;
  295. Result.Referer := aRequestInfo.Referer;
  296. Result.Content := aRequestInfo.PostStream;
  297. Result.ContentType := aRequestInfo.ContentType;
  298. Result.ContentEncoding := aRequestInfo.ContentEncoding;
  299. Result.ContentLength := aRequestInfo.ContentLength;
  300. for i := 0 to aRequestInfo.RawHeaders.Count -1 do
  301. begin
  302. if not StrInArray(aRequestInfo.RawHeaders.Names[i],['Host','Accept-Encoding','Accept','User-Agent','Connection','Cache-Control']) then
  303. begin
  304. Result.Headers.Add(aRequestInfo.RawHeaders.Names[i],aRequestInfo.RawHeaders.Values[aRequestInfo.RawHeaders.Names[i]]);
  305. end;
  306. end;
  307. end;
  308. procedure THttpServer.SetResponseInfo(aResponseInfo: TIdHTTPResponseInfo; aResponse: IHttpResponse);
  309. var
  310. pair : TPairItem;
  311. begin
  312. for pair in aResponse.Headers do
  313. begin
  314. aResponseInfo.CustomHeaders.AddValue(pair.Name,pair.Value);
  315. end;
  316. aResponseInfo.ResponseNo := aResponse.StatusCode;
  317. aResponseInfo.ResponseText := aResponse.StatusText;
  318. aResponseInfo.ContentStream := aResponse.Content;
  319. aResponseInfo.ContentText := aResponse.ContentText;
  320. aResponseInfo.ContentType := aResponse.ContentType;
  321. //delegate stream to responseinfo
  322. aResponse.Content := nil;
  323. end;
  324. procedure THttpServer.ProcessRequest(aRequest: IHttpRequest; aResponse: IHttpResponse);
  325. begin
  326. if Assigned(fOnRequest) then fOnRequest(aRequest,aResponse);
  327. end;
  328. procedure THttpServer.DoConnect(aContext: TIdContext);
  329. begin
  330. {$IFDEF DEBUG_HTTPSERVER}
  331. TDebugger.Enter(Self,'DoConnect').TimeIt;
  332. {$ENDIF}
  333. Logger.Debug('Client connected');
  334. if Assigned(fOnConnect) then fOnConnect;
  335. end;
  336. procedure THttpServer.DoDisconnect(aContext: TIdContext);
  337. begin
  338. {$IFDEF DEBUG_HTTPSERVER}
  339. TDebugger.Enter(Self,'DoDisconnect').TimeIt;
  340. {$ENDIF}
  341. Logger.Debug('Client disconnected!');
  342. if Assigned(fOnDisconnect) then fOnDisconnect;
  343. end;
  344. procedure THTTPServer.DoOnQuerySSLPort(aPort: Word; var vUseSSL: Boolean);
  345. begin
  346. vUseSSL := (aPort <> 443);
  347. end;
  348. procedure THTTPServer.OnGetRequest(aContext: TIdContext; aRequestInfo: TIdHTTPRequestInfo; aResponseInfo: TIdHTTPResponseInfo);
  349. var
  350. request : IHttpRequest;
  351. response : IHttpResponse;
  352. begin
  353. {$IFDEF DEBUG_HTTPSERVER}
  354. TDebugger.Enter(Self,Format('OnGetRequest (%s %s)',[aRequestInfo.Command,aRequestInfo.URI])).TimeIt;
  355. {$ENDIF}
  356. Logger.Debug('Request: %s',[aRequestInfo.RawHTTPCommand]);
  357. request := GetRequestInfo(aRequestInfo);
  358. response := THttpResponse.Create;
  359. //process incoming Request
  360. try
  361. ProcessRequest(request,response);
  362. except
  363. on E : Exception do
  364. begin
  365. //get unexpected exception
  366. if E.InheritsFrom(EControlledException) then response.ContentText := response.ContentText + '<BR>' + e.Message
  367. else
  368. begin
  369. if response.StatusCode = 200 then
  370. begin
  371. response.StatusCode := 500;
  372. response.StatusText := 'Internal server error';
  373. end;
  374. response.ContentText := e.Message;
  375. end;
  376. end;
  377. end;
  378. //check if need return error page
  379. if response.StatusCode > 399 then GetErrorPage(aRequestInfo.URI,response);
  380. //return response to client
  381. {$IFDEF DEBUG_HTTPSERVER}
  382. TDebugger.TimeIt(Self,Format('OnGetRequest (%s)',[aRequestInfo.URI]),'SendResponse');
  383. {$ENDIF}
  384. SetResponseInfo(aResponseInfo,response);
  385. aResponseInfo.WriteContent;
  386. end;
  387. procedure THttpServer.Start;
  388. begin
  389. fHTTPServer.Active := True;
  390. end;
  391. procedure THttpServer.Stop;
  392. begin
  393. fHTTPServer.Active := False;
  394. end;
  395. end.