Quick.HttpServer.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. property OnNewRequest : TRequestEvent read GetOnRequest write SetOnRequest;
  62. property CustomErrorPages : TCustomErrorPages read GetCustomErrorPages write SetCustomErrorPages;
  63. function Logger : ILogger;
  64. procedure Start;
  65. procedure Stop;
  66. end;
  67. TCustomHttpServer = class(TInterfacedObject,IHttpServer)
  68. private
  69. fLogger : ILogger;
  70. fOnConnect : TOnConnectEvent;
  71. fOnDisconnect : TOnDisconnectEvent;
  72. fCustomErrorPages : TCustomErrorPages;
  73. procedure SetOnRequest(aRequestEvent : TRequestEvent);
  74. function GetOnRequest : TRequestEvent;
  75. function GetCustomErrorPages: TCustomErrorPages;
  76. procedure SetCustomErrorPages(const Value: TCustomErrorPages);
  77. protected
  78. fOnRequest : TRequestEvent;
  79. fHost : string;
  80. fPort : Integer;
  81. fSSLSecured : Boolean;
  82. procedure GetErrorPage(const aURL : string; aResponse : IHttpResponse); virtual;
  83. public
  84. constructor Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil); virtual;
  85. destructor Destroy; override;
  86. property Host : string read fHost;
  87. property Port : Integer read fPort;
  88. property CustomErrorPages : TCustomErrorPages read GetCustomErrorPages write SetCustomErrorPages;
  89. property OnNewRequest : TRequestEvent read GetOnRequest write SetOnRequest;
  90. property OnConnect : TOnConnectEvent read fOnConnect write fOnConnect;
  91. property OnDisconnect : TOnDisconnectEvent read fOnDisconnect write fOnDisconnect;
  92. function Logger : ILogger;
  93. procedure Start; virtual; abstract;
  94. procedure Stop; virtual; abstract;
  95. end;
  96. THttpServer = class(TCustomHttpServer)
  97. private
  98. fHTTPServer : TidHTTPServer;
  99. procedure OnGetRequest(aContext: TIdContext; aRequestInfo: TIdHTTPRequestInfo; aResponseInfo: TIdHTTPResponseInfo);
  100. function GetSSLIOHandler : TIdServerIOHandlerSSLOpenSSL;
  101. function OnVerifyPeer(aCertificate: TIdX509; aOk: Boolean; aDepth, aError: Integer): Boolean;
  102. function GetRequestInfo(aRequestInfo : TIdHTTPRequestInfo) : THttpRequest;
  103. procedure SetResponseInfo(aResponseInfo : TIdHTTPResponseInfo; aResponse : IHttpResponse);
  104. procedure DoOnQuerySSLPort(aPort: Word; var vUseSSL: Boolean);
  105. procedure DoConnect(aContext: TIdContext);
  106. procedure DoDisconnect(aContext: TIdContext);
  107. protected
  108. procedure ProcessRequest(aRequest: IHttpRequest; aResponse: IHttpResponse); virtual;
  109. public
  110. constructor Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil); override;
  111. destructor Destroy; override;
  112. procedure Start; override;
  113. procedure Stop; override;
  114. end;
  115. implementation
  116. { TCustomHttpServer }
  117. constructor TCustomHttpServer.Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil);
  118. begin
  119. fCustomErrorPages := TCustomErrorPages.Create;
  120. fCustomErrorPages.Path := '.';
  121. fCustomErrorPages.DynamicErrorPage := False;
  122. fCustomErrorPages.Enabled := False;
  123. if aHost.IsEmpty then fHost := '127.0.0.1'
  124. else fHost := aHost;
  125. {$IFDEF DELPHILINUX}
  126. if fHost = '127.0.0.1' then fHost := '0.0.0.0';
  127. {$ENDIF}
  128. fPort := aPort;
  129. if aLogger = nil then
  130. begin
  131. fLogger := TNullLogger.Create;
  132. end
  133. else fLogger := aLogger;
  134. fSSLSecured := aSSLEnabled;
  135. end;
  136. destructor TCustomHttpServer.Destroy;
  137. begin
  138. fCustomErrorPages.Free;
  139. inherited;
  140. end;
  141. function TCustomHttpServer.GetCustomErrorPages: TCustomErrorPages;
  142. begin
  143. Result := fCustomErrorPages;
  144. end;
  145. procedure TCustomHttpServer.GetErrorPage(const aURL : string; aResponse : IHttpResponse);
  146. var
  147. filestream : TFileStream;
  148. pagestream : TStringStream;
  149. pagefilename : string;
  150. found : Boolean;
  151. content : string;
  152. begin
  153. content := '';
  154. found := False;
  155. if (fCustomErrorPages.Enabled) then
  156. begin
  157. pagestream := TStringStream.Create;
  158. try
  159. //get specific error filename
  160. pagefilename := Format('%s\%d.html',[fCustomErrorPages.Path,aResponse.StatusCode]);
  161. found := FileExists(pagefilename);
  162. //get generic error type filanema
  163. if not found then
  164. begin
  165. pagefilename := Format('%s\%sxx.html',[fCustomErrorPages.Path,(aResponse.StatusCode).ToString[Low(string)]]);
  166. found := FileExists(pagefilename);
  167. end;
  168. //get generic error filename
  169. if not found then
  170. begin
  171. pagefilename := Format('%s\error.html',[fCustomErrorPages.Path]);
  172. found := FileExists(pagefilename);
  173. end;
  174. if found then
  175. begin
  176. filestream := TFileStream.Create(pagefilename,fmShareDenyNone);
  177. try
  178. pagestream.CopyFrom(filestream,filestream.Size);
  179. finally
  180. filestream.Free;
  181. end;
  182. content := pagestream.DataString;
  183. if fCustomErrorPages.DynamicErrorPage then
  184. begin
  185. content := StringReplace(content,'{{URL}}',aURL,[rfReplaceAll,rfIgnoreCase]);
  186. content := StringReplace(content,'{{STATUSCODE}}',aResponse.StatusCode.ToString,[rfReplaceAll,rfIgnoreCase]);
  187. content := StringReplace(content,'{{STATUSTEXT}}',aResponse.StatusText,[rfReplaceAll,rfIgnoreCase]);
  188. content := StringReplace(content,'{{CONTENT}}',aResponse.ContentText,[rfReplaceAll,rfIgnoreCase]);
  189. end;
  190. end;
  191. finally
  192. pagestream.Free;
  193. end;
  194. end;
  195. if not found then
  196. begin
  197. aResponse.ContentText := Format('<h2>%d Error: %s</h2>',[aResponse.StatusCode,aResponse.StatusText])
  198. + Format('<h4>Message: %s</h4>',[aResponse.ContentText]);
  199. end
  200. else aResponse.ContentText := content;
  201. end;
  202. function TCustomHttpServer.GetOnRequest: TRequestEvent;
  203. begin
  204. Result := fOnRequest;
  205. end;
  206. procedure TCustomHttpServer.SetCustomErrorPages(const Value: TCustomErrorPages);
  207. begin
  208. fCustomErrorPages := Value;
  209. end;
  210. procedure TCustomHttpServer.SetOnRequest(aRequestEvent: TRequestEvent);
  211. begin
  212. fOnRequest := aRequestEvent;
  213. end;
  214. function TCustomHttpServer.Logger: ILogger;
  215. begin
  216. Result := fLogger;
  217. end;
  218. { THTTPServer }
  219. constructor THTTPServer.Create(const aHost : string; aPort : Integer; aSSLEnabled : Boolean; aLogger : ILogger = nil);
  220. begin
  221. inherited Create(aHost, aPort, aSSLEnabled, aLogger);
  222. Logger.Info('HTTPServer: Indy');
  223. fHTTPServer := TIdHTTPServer.Create(nil);
  224. fHTTPServer.Bindings.Clear; //make sure there's no other bindings
  225. with fHTTPServer.Bindings.Add do
  226. begin
  227. IP := fHost;
  228. Port := fPort;
  229. end;
  230. if fSSLSecured then fHTTPServer.IOHandler := GetSSLIOHandler;
  231. fHTTPServer.OnCommandGet := OnGetRequest;
  232. fHTTPServer.OnCommandOther := OnGetRequest;
  233. fHTTPServer.OnConnect := DoConnect;
  234. fHTTPServer.OnDisconnect := DoDisconnect;
  235. //fHTTPServer.OnExecute := DoConnect;
  236. fHTTPServer.OnQuerySSLPort := DoOnQuerySSLPort;
  237. fHTTPServer.ServerSoftware := 'Quick.HttpServer';
  238. end;
  239. destructor THTTPServer.Destroy;
  240. begin
  241. if Assigned(fHTTPServer) then
  242. begin
  243. if Assigned(fHTTPServer.IOHandler) then fHTTPServer.IOHandler.Free;
  244. fHTTPServer.Free;
  245. end;
  246. inherited;
  247. end;
  248. function THTTPServer.GetSSLIOHandler : TIdServerIOHandlerSSLOpenSSL;
  249. begin
  250. Result := TIdServerIOHandlerSSLOpenSSL.Create(nil);
  251. //Result.SSLOptions.RootCertFile := '.\ca.cert.pem';
  252. Result.SSLOptions.CertFile := '.\server.cert.pem';
  253. Result.SSLOptions.KeyFile := '.\server.key.pem';
  254. Result.SSLOptions.Method := sslvSSLv23;
  255. Result.SSLOptions.Mode := sslmServer;
  256. Result.OnVerifyPeer := OnVerifyPeer;
  257. end;
  258. function THTTPServer.OnVerifyPeer(aCertificate: TIdX509; aOk: Boolean; aDepth, aError: Integer): Boolean;
  259. begin
  260. Result := aOk;
  261. end;
  262. function THttpServer.GetRequestInfo(aRequestInfo: TIdHTTPRequestInfo): THttpRequest;
  263. var
  264. i : Integer;
  265. uhost : TArray<string>;
  266. begin
  267. Result := THttpRequest.Create;
  268. if aRequestInfo.Host.Contains(':') then
  269. begin
  270. uhost := aRequestInfo.Host.Split([':']);
  271. Result.Host := uhost[0];
  272. Result.Port := StrToIntDef(uhost[1],80);
  273. end
  274. else Result.Host := aRequestInfo.Host;
  275. Result.URL := aRequestInfo.URI;
  276. Result.ClientIP := aRequestInfo.RemoteIP;
  277. Result.UnParsedParams := aRequestInfo.QueryParams;
  278. Result.SetMethodFromString(aRequestInfo.Command);
  279. Result.UserAgent := aRequestInfo.UserAgent;
  280. Result.CacheControl := aRequestInfo.CacheControl;
  281. Result.Referer := aRequestInfo.Referer;
  282. Result.Content := aRequestInfo.PostStream;
  283. Result.ContentType := aRequestInfo.ContentType;
  284. Result.ContentEncoding := aRequestInfo.ContentEncoding;
  285. Result.ContentLength := aRequestInfo.ContentLength;
  286. for i := 0 to aRequestInfo.RawHeaders.Count -1 do
  287. begin
  288. if not StrInArray(aRequestInfo.RawHeaders.Names[i],['Host','Accept-Encoding','Accept','User-Agent','Connection','Cache-Control']) then
  289. begin
  290. Result.Headers.Add(aRequestInfo.RawHeaders.Names[i],aRequestInfo.RawHeaders.Values[aRequestInfo.RawHeaders.Names[i]]);
  291. end;
  292. end;
  293. end;
  294. procedure THttpServer.SetResponseInfo(aResponseInfo: TIdHTTPResponseInfo; aResponse: IHttpResponse);
  295. var
  296. pair : TPairItem;
  297. begin
  298. for pair in aResponse.Headers do
  299. begin
  300. aResponseInfo.CustomHeaders.AddValue(pair.Name,pair.Value);
  301. end;
  302. aResponseInfo.ResponseNo := aResponse.StatusCode;
  303. aResponseInfo.ResponseText := aResponse.StatusText;
  304. aResponseInfo.ContentStream := aResponse.Content;
  305. aResponseInfo.ContentText := aResponse.ContentText;
  306. aResponseInfo.ContentType := aResponse.ContentType;
  307. //delegate stream to responseinfo
  308. aResponse.Content := nil;
  309. end;
  310. procedure THttpServer.ProcessRequest(aRequest: IHttpRequest; aResponse: IHttpResponse);
  311. begin
  312. if Assigned(fOnRequest) then fOnRequest(aRequest,aResponse);
  313. end;
  314. procedure THttpServer.DoConnect(aContext: TIdContext);
  315. begin
  316. {$IFDEF DEBUG_HTTPSERVER}
  317. TDebugger.Enter(Self,'DoConnect').TimeIt;
  318. {$ENDIF}
  319. Logger.Debug('Client connected');
  320. if Assigned(fOnConnect) then fOnConnect;
  321. end;
  322. procedure THttpServer.DoDisconnect(aContext: TIdContext);
  323. begin
  324. {$IFDEF DEBUG_HTTPSERVER}
  325. TDebugger.Enter(Self,'DoDisconnect').TimeIt;
  326. {$ENDIF}
  327. Logger.Debug('Client disconnected!');
  328. if Assigned(fOnDisconnect) then fOnDisconnect;
  329. end;
  330. procedure THTTPServer.DoOnQuerySSLPort(aPort: Word; var vUseSSL: Boolean);
  331. begin
  332. vUseSSL := (aPort <> 443);
  333. end;
  334. procedure THTTPServer.OnGetRequest(aContext: TIdContext; aRequestInfo: TIdHTTPRequestInfo; aResponseInfo: TIdHTTPResponseInfo);
  335. var
  336. request : IHttpRequest;
  337. response : IHttpResponse;
  338. begin
  339. {$IFDEF DEBUG_HTTPSERVER}
  340. TDebugger.Enter(Self,Format('OnGetRequest (%s %s)',[aRequestInfo.Command,aRequestInfo.URI])).TimeIt;
  341. {$ENDIF}
  342. Logger.Debug('Request: %s',[aRequestInfo.RawHTTPCommand]);
  343. request := GetRequestInfo(aRequestInfo);
  344. response := THttpResponse.Create;
  345. //process incoming Request
  346. try
  347. ProcessRequest(request,response);
  348. except
  349. on E : Exception do
  350. begin
  351. //get unexpected exception
  352. if E.InheritsFrom(EControlledException) then response.ContentText := response.ContentText + '<BR>' + e.Message
  353. else
  354. begin
  355. if response.StatusCode = 200 then
  356. begin
  357. response.StatusCode := 500;
  358. response.StatusText := 'Internal server error';
  359. end;
  360. response.ContentText := e.Message;
  361. end;
  362. end;
  363. end;
  364. //check if need return error page
  365. if response.StatusCode > 399 then GetErrorPage(aRequestInfo.URI,response);
  366. //return response to client
  367. {$IFDEF DEBUG_HTTPSERVER}
  368. TDebugger.TimeIt(Self,Format('OnGetRequest (%s)',[aRequestInfo.URI]),'SendResponse');
  369. {$ENDIF}
  370. SetResponseInfo(aResponseInfo,response);
  371. aResponseInfo.WriteContent;
  372. end;
  373. procedure THttpServer.Start;
  374. begin
  375. fHTTPServer.Active := True;
  376. end;
  377. procedure THttpServer.Stop;
  378. begin
  379. fHTTPServer.Active := False;
  380. end;
  381. end.