fpwebfile.pp 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. {
  2. This file is part of the Free Pascal run time library.
  3. Copyright (c) 2019 by the Free Pascal development team
  4. Classes to implement a file serving mechanism.
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {$mode objfpc}
  12. {$h+}
  13. unit fpwebfile;
  14. interface
  15. uses SysUtils, Classes, httpdefs, fphttp, httproute;
  16. Type
  17. { TFPCustomFileModule }
  18. TFPCustomFileModule = Class(TCustomHTTPModule)
  19. private
  20. FCacheControlMaxAge: Integer;
  21. Protected
  22. // Determine filename frome request.
  23. Function GetRequestFileName(Const ARequest : TRequest) : String; virtual;
  24. // Map request filename to physical filename.
  25. Function MapFileName(Const AFileName : String) : String; virtual;
  26. // Override to implement security. Returns true by default.
  27. Function AllowFile(Const AFileName : String) : Boolean; virtual;
  28. // Actually Send file to client.
  29. Procedure SendFile(Const AFileName : String; AResponse : TResponse); virtual;
  30. Public
  31. Constructor CreateNew(AOwner: TComponent; CreateMode: Integer); override; overload;
  32. // Overrides TCustomHTTPModule to implement file serving.
  33. Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); override;
  34. Property CacheControlMaxAge : Integer Read FCacheControlMaxAge Write FCacheControlMaxAge;
  35. end;
  36. TFPCustomFileModuleClass = Class of TFPCustomFileModule;
  37. { TSimpleFileModule }
  38. TSimpleFileLog = Procedure (EventType : TEventType; Const Msg : String) of object;
  39. TSimpleFileModule = class(TFPCustomFileModule,IRouteInterface)
  40. private
  41. FRequestedFileName,
  42. FMappedFileName : String;
  43. class procedure HandleSimpleFileRequest(ARequest: TRequest; AResponse: TResponse); static;
  44. Protected
  45. Function AllowFile(Const AFileName : String) : Boolean; override;
  46. Function MapFileName(Const AFileName : String) : String; override;
  47. Function GetRequestFileName(Const ARequest : TRequest) : String; override;
  48. Public
  49. Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); override;
  50. Public
  51. Class var
  52. // Where to serve files from
  53. BaseDir : String;
  54. // For directories, convert to index.html if this is set.
  55. IndexPageName : String;
  56. // If you want some logging, set this.
  57. OnLog : TSimpleFileLog;
  58. Class Procedure RegisterDefaultRoute;
  59. end;
  60. Var
  61. // Set this if you want a descendent class to serve the files.
  62. // You can use this to customize the behaviour in the descendent, for instance if you have multiple virtual hosts.
  63. DefaultFileModuleClass : TFPCustomFileModuleClass = TFPCustomFileModule;
  64. // Setting this will load mime types from that file.
  65. MimeTypesFile : string;
  66. DefaultCacheControlMaxAge : Integer = 0;
  67. // use this to map locations (relative to BaseURL of the application) to physical directories.
  68. // More than one location can be registered. Directory must exist, location must not have / or \
  69. Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
  70. implementation
  71. uses fpmimetypes;
  72. Resourcestring
  73. SErrNoLocation = 'Cannot register an empty location.';
  74. SErrInvalidLocation = 'Location contains invalid characters.';
  75. SErrInvalidDirectory = 'Directory "%s" does not exist';
  76. Var
  77. Locations : TStrings;
  78. MimeLoaded : Boolean;
  79. Procedure CheckMimeLoaded;
  80. begin
  81. If (Not MimeLoaded) and (MimeTypesFile<>'') then
  82. begin
  83. MimeTypes.LoadFromFile(MimeTypesFile);
  84. MimeLoaded:=true;
  85. end;
  86. end;
  87. Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
  88. Var
  89. D,BaseDir : String;
  90. begin
  91. if (ALocation='') then
  92. Raise HTTPError.Create(SErrNoLocation);
  93. if Pos('/',ALocation)<>0 then
  94. Raise HTTPError.Create(SErrInvalidLocation);
  95. if (Locations=Nil) then
  96. Locations:=TStringList.Create;
  97. if DefaultFileModuleClass=Nil then
  98. DefaultFileModuleClass:=TFPCustomFileModule;
  99. BaseDir:=ExtractFilePath(ParamStr(0));
  100. if (ADirectory='') then
  101. Locations.Values[IncludeHTTPPathDelimiter(ALocation)]:=BaseDir
  102. else
  103. begin
  104. D:=ADirectory;
  105. if (D<>ExpandFileName(D)) then
  106. D:=BaseDir+D;
  107. if not DirectoryExists(D) then
  108. Raise HTTPError.CreateFmt(SErrInvalidDirectory,[D]);
  109. Locations.Values[IncludeHTTPPathDelimiter(ALocation)]:=IncludeTrailingPathDelimiter(D);
  110. end;
  111. RegisterHTTPModule(ALocation,DefaultFileModuleClass,true);
  112. end;
  113. { TSimpleFileModule }
  114. Class Procedure TSimpleFileModule.HandleSimpleFileRequest(ARequest : TRequest; AResponse : TResponse); static;
  115. begin
  116. With TSimpleFileModule.CreateNew(Nil) do
  117. try
  118. HandleRequest(ARequest,AResponse);
  119. finally
  120. Free;
  121. end;
  122. end;
  123. function TSimpleFileModule.AllowFile(const AFileName: String): Boolean;
  124. Var
  125. FN : String;
  126. begin
  127. FN:=ExpandFileName(aFileName);
  128. FN:=ExtractRelativepath(IncludeTrailingPathDelimiter(BaseDir),FN);
  129. Result:=Pos('..'+PathDelim,FN)=0;
  130. end;
  131. function TSimpleFileModule.MapFileName(const AFileName: String): String;
  132. begin
  133. Result:=AFileName;
  134. While (Result<>'') and (Result[1]='/') do
  135. Delete(Result,1,1);
  136. Result:=ExpandFileName(IncludeTrailingPathDelimiter(BaseDir)+Result);
  137. FRequestedFileName:=AFileName;
  138. FMappedFileName:=Result;
  139. end;
  140. function TSimpleFileModule.GetRequestFileName(const ARequest: TRequest): String;
  141. begin
  142. Result:=inherited GetRequestFileName(ARequest);
  143. if (IndexPageName<>'') and ((Result='') or (Result[Length(Result)]='/')) then
  144. Result:=Result+IndexPageName;
  145. end;
  146. procedure TSimpleFileModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
  147. begin
  148. Inherited;
  149. if Assigned (OnLog) then
  150. OnLog(etInfo,Format('%d serving "%s" -> "%s"',[AResponse.Code,FRequestedFileName,FMappedFileName]));
  151. end;
  152. class procedure TSimpleFileModule.RegisterDefaultRoute;
  153. begin
  154. if BaseDir='' then
  155. BaseDir:=IncludeTrailingPathDelimiter(GetCurrentDir);
  156. httprouter.RegisterRoute('/*',@HandleSimpleFileRequest);
  157. end;
  158. Function TFPCustomFileModule.GetRequestFileName(Const ARequest : TRequest) : String;
  159. procedure sb;
  160. begin
  161. If (Result<>'') and (Result[1]='/') then
  162. Delete(Result,1,1);
  163. end;
  164. begin
  165. Result:=ARequest.PathInfo;
  166. If (Result='') then
  167. Result:=ARequest.URI;
  168. sb;
  169. If (BaseURL<>'') and (Pos(BaseURL,Result)=1) then
  170. Delete(Result,1,Length(BaseURL));
  171. sb;
  172. end;
  173. Function TFPCustomFileModule.MapFileName(Const AFileName : String) : String;
  174. Var
  175. D : String;
  176. begin
  177. if (BaseURL='') then
  178. Result:=AFileName
  179. else
  180. begin
  181. D:=Locations.Values[BaseURL];
  182. If (D='') then
  183. Result:=''
  184. else
  185. begin
  186. Result:=D+AFileName;
  187. DoDirSeparators(Result);
  188. Result:=ExpandFileName(Result);
  189. end;
  190. end;
  191. end;
  192. Function TFPCustomFileModule.AllowFile(Const AFileName : String) : Boolean;
  193. Var
  194. BaseDir,FN : String;
  195. begin
  196. FN:=ExpandFileName(aFileName);
  197. if (BaseURL='') then
  198. BaseDir:=ExtractFilePath(Paramstr(0))
  199. else
  200. begin
  201. BaseDir:=Locations.Values[BaseURL];
  202. if (BaseURL='') then
  203. BaseDir:=ExtractFilePath(Paramstr(0))
  204. end;
  205. FN:=ExtractRelativepath(BaseDir,aFileName);
  206. Result:=Pos('..'+PathDelim,FN)=0;
  207. end;
  208. procedure TFPCustomFileModule.SendFile(Const AFileName : String; AResponse : TResponse);
  209. Var
  210. F : TFileStream;
  211. begin
  212. CheckMimeLoaded;
  213. AResponse.ContentType:=MimeTypes.GetMimeType(ExtractFileExt(AFileName));
  214. If (AResponse.ContentType='') then
  215. AResponse.ContentType:='Application/octet-stream';
  216. if CacheControlMaxAge>0 then
  217. aResponse.CacheControl:=Format('max-age=%d',[CacheControlMaxAge]);
  218. F:=TFileStream.Create(AFileName,fmOpenRead or fmShareDenyWrite);
  219. try
  220. AResponse.ContentLength:=F.Size;
  221. AResponse.ContentStream:=F;
  222. AResponse.SendContent;
  223. AResponse.ContentStream:=Nil;
  224. finally
  225. F.Free;
  226. end;
  227. end;
  228. constructor TFPCustomFileModule.CreateNew(AOwner: TComponent; CreateMode: Integer);
  229. begin
  230. inherited CreateNew(aOwner,CreateMode);
  231. CacheControlMaxAge:=DefaultCacheControlMaxAge;
  232. end;
  233. Procedure TFPCustomFileModule.HandleRequest(ARequest : TRequest; AResponse : TResponse);
  234. Var
  235. RFN,FN : String;
  236. begin
  237. If CompareText(ARequest.Method,'GET')<>0 then
  238. begin
  239. AResponse.Code:=405;
  240. AResponse.CodeText:='Method not allowed';
  241. AResponse.SendContent;
  242. Exit;
  243. end;
  244. RFN:=GetRequestFileName(ARequest);
  245. if (RFN='') then
  246. begin
  247. AResponse.Code:=400;
  248. AResponse.CodeText:='Bad request';
  249. AResponse.SendContent;
  250. exit;
  251. end;
  252. FN:=MapFileName(RFN);
  253. if (FN='') or not AllowFile(FN) then
  254. begin
  255. AResponse.Code:=403;
  256. AResponse.CodeText:='Forbidden';
  257. AResponse.SendContent;
  258. exit;
  259. end;
  260. if not FileExists(FN) then
  261. begin
  262. AResponse.Code:=404;
  263. AResponse.CodeText:='Not found';
  264. AResponse.SendContent;
  265. exit;
  266. end;
  267. SendFile(FN,AResponse);
  268. end;
  269. initialization
  270. finalization
  271. FreeAndNil(Locations);
  272. end.