Quick.HttpServer.pas 15 KB

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