diff --git a/src/Middleware/BuiltIns/BaseStaticFilesMiddlewareImpl.pas b/src/Middleware/BuiltIns/BaseStaticFilesMiddlewareImpl.pas new file mode 100644 index 00000000..e895a6cf --- /dev/null +++ b/src/Middleware/BuiltIns/BaseStaticFilesMiddlewareImpl.pas @@ -0,0 +1,130 @@ +{*! + * Fano Web Framework (https://fanoframework.github.io) + * + * @link https://github.com/fanoframework/fano + * @copyright Copyright (c) 2018 - 2022 Zamrony P. Juhara + * @license https://github.com/fanoframework/fano/blob/master/LICENSE (MIT) + *} + +unit BaseStaticFilesMiddlewareImpl; + +interface + +{$MODE OBJFPC} +{$H+} + +uses + + RequestIntf, + ResponseIntf, + MiddlewareIntf, + RouteArgsReaderIntf, + RequestHandlerIntf, + ReadOnlyKeyValuePairIntf, + InjectableObjectImpl; + +type + + (*!------------------------------------------------ + * base middleware class that serves static files from + * a base directory. + *------------------------------------------------- + * Content type of response will be determined + * using file extension that is stored in fMimeTypes + * if not set then 'application/octet-stream' is assumed + *------------------------------------------------- + * @author Zamrony P. Juhara + *-------------------------------------------------*) + TBaseStaticFilesMiddleware = class(TInjectableObject, IMiddleware) + protected + fBaseDirectory : string; + fMimeTypes : IReadOnlyKeyValuePair; + function getContentTypeFromFilename(const filename : string) : string; + + (*!------------------------------------------- + * clean filepath (if required) + *-------------------------------------------- + * This method is provided so that descendant + * have opportunity to clean file path + *-------------------------------------------- + * @param filePath original file path + * @return new cleaned file path + *--------------------------------------------*) + function clean(const filePath: string) : string; virtual; + public + constructor create( + const baseDir : string; + const mimeTypes : IReadOnlyKeyValuePair + ); + + function handleRequest( + const request : IRequest; + const response : IResponse; + const args : IRouteArgsReader; + const nextMdlwr : IRequestHandler + ) : IResponse; virtual; + end; + +implementation + +uses + + SysUtils, + FileResponseImpl; + + constructor TBaseStaticFilesMiddleware.create( + const baseDir : string; + const mimeTypes : IReadOnlyKeyValuePair + ); + begin + fBaseDirectory := baseDir; + fMimeTypes := mimeTypes; + end; + + function TBaseStaticFilesMiddleware.getContentTypeFromFilename( + const filename : string + ) : string; + var ext : string; + begin + ext := ExtractFileExt(filename); + //remove dot from ext + ext := copy(ext, 2, length(ext)-1); + if (fMimeTypes.has(ext)) then + begin + result := fMimeTypes.getValue(ext); + end else + begin + //set default + result := 'application/octet-stream'; + end; + end; + + function TBaseStaticFilesMiddleware.clean(const filePath: string) : string; + begin + result := filePath; + end; + + function TBaseStaticFilesMiddleware.handleRequest( + const request : IRequest; + const response : IResponse; + const args : IRouteArgsReader; + const nextMdlwr : IRequestHandler + ) : IResponse; + var filename : string; + begin + filename := fBaseDirectory + clean(request.uri().getPath()); + if fileExists(filename) then + begin + //serve file + result := TFileResponse.create( + response.headers(), + getContentTypeFromFilename(filename), + filename + ); + end else + begin + //file not found, just pass to next middleware + result := nextMdlwr.handleRequest(request, response, args); + end; + end; +end. diff --git a/src/Middleware/BuiltIns/StaticFilesMiddlewareImpl.pas b/src/Middleware/BuiltIns/StaticFilesMiddlewareImpl.pas index a97a343d..30a0d6e7 100644 --- a/src/Middleware/BuiltIns/StaticFilesMiddlewareImpl.pas +++ b/src/Middleware/BuiltIns/StaticFilesMiddlewareImpl.pas @@ -15,13 +15,7 @@ interface uses - RequestIntf, - ResponseIntf, - MiddlewareIntf, - RouteArgsReaderIntf, - RequestHandlerIntf, - ReadOnlyKeyValuePairIntf, - InjectableObjectImpl; + BaseStaticFilesMiddlewareImpl; type @@ -35,80 +29,38 @@ interface *------------------------------------------------- * @author Zamrony P. Juhara *-------------------------------------------------*) - TStaticFilesMiddleware = class(TInjectableObject, IMiddleware) + TStaticFilesMiddleware = class(TBaseStaticFilesMiddleware) protected - fBaseDirectory : string; - fMimeTypes : IReadOnlyKeyValuePair; - function getContentTypeFromFilename(const filename : string) : string; + (*!------------------------------------------- + * clean filepath avoid serve hidden dot files in unix + *-------------------------------------------- + * @param filePath original file path + * @return new cleaned file path + *--------------------------------------------*) + function clean(const filePath: string) : string; override; public - constructor create( - const baseDir : string; - const mimeTypes : IReadOnlyKeyValuePair - ); - - function handleRequest( - const request : IRequest; - const response : IResponse; - const args : IRouteArgsReader; - const nextMdlwr : IRequestHandler - ) : IResponse; virtual; end; implementation uses - SysUtils, - FileResponseImpl; + SysUtils; - constructor TStaticFilesMiddleware.create( - const baseDir : string; - const mimeTypes : IReadOnlyKeyValuePair - ); - begin - fBaseDirectory := baseDir; - fMimeTypes := mimeTypes; - end; - function TStaticFilesMiddleware.getContentTypeFromFilename( - const filename : string - ) : string; - var ext : string; + (*!------------------------------------------- + * clean filepath avoid serve hidden dot files in unix + *-------------------------------------------- + * @param filePath original file path + * @return new cleaned file path + *--------------------------------------------*) + function TStaticFilesMiddleware.clean(const filePath: string) : string; begin - ext := ExtractFileExt(filename); - //remove dot from ext - ext := copy(ext, 2, length(ext)-1); - if (fMimeTypes.has(ext)) then - begin - result := fMimeTypes.getValue(ext); - end else - begin - //set default - result := 'application/octet-stream'; - end; + // for example if filePath contain '/.htaccess' we replace it so + // filePath become '/htaccess' + result := stringReplace(filePath, '/.', '/', [rfReplaceAll]); + // just paranoia handle .. too + result := stringReplace(result, '..', '', [rfReplaceAll]); end; - function TStaticFilesMiddleware.handleRequest( - const request : IRequest; - const response : IResponse; - const args : IRouteArgsReader; - const nextMdlwr : IRequestHandler - ) : IResponse; - var filename : string; - begin - filename := fBaseDirectory + request.uri().getPath(); - if fileExists(filename) then - begin - //serve file - result := TFileResponse.create( - response.headers(), - getContentTypeFromFilename(filename), - filename - ); - end else - begin - //file not found, just pass to next middleware - result := nextMdlwr.handleRequest(request, response, args); - end; - end; end.