2020-04-24 02:48:39 +02:00
|
|
|
|
// ***************************************************************************
|
|
|
|
|
//
|
|
|
|
|
// Delphi MVC Framework
|
|
|
|
|
//
|
|
|
|
|
// Copyright (c) 2010-2020 Daniele Teti and the DMVCFramework Team
|
|
|
|
|
//
|
|
|
|
|
// https://github.com/danieleteti/delphimvcframework
|
|
|
|
|
//
|
|
|
|
|
// Collaborators on this file:
|
|
|
|
|
// Jo<4A>o Ant<6E>nio Duarte (https://github.com/joaoduarte19)
|
|
|
|
|
//
|
|
|
|
|
// ***************************************************************************
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
//
|
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
|
|
|
|
//
|
|
|
|
|
// *************************************************************************** }
|
|
|
|
|
|
|
|
|
|
unit MVCFramework.Middleware.StaticFiles;
|
|
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
|
|
uses
|
|
|
|
|
MVCFramework,
|
2020-04-25 01:48:07 +02:00
|
|
|
|
MVCFramework.Commons,
|
2020-04-24 02:48:39 +02:00
|
|
|
|
System.Generics.Collections;
|
|
|
|
|
|
|
|
|
|
type
|
2020-04-25 01:48:07 +02:00
|
|
|
|
TMVCStaticFilesDefaults = class sealed
|
|
|
|
|
public const
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// URL segment that represents the path to static files
|
|
|
|
|
/// </summary>
|
2020-05-02 16:39:32 +02:00
|
|
|
|
STATIC_FILES_PATH = '/static';
|
2020-04-25 01:48:07 +02:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Physical path of the root folder that contains the static files
|
|
|
|
|
/// </summary>
|
|
|
|
|
DOCUMENT_ROOT = '.\www';
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Default static file
|
|
|
|
|
/// </summary>
|
|
|
|
|
INDEX_DOCUMENT = 'index.html';
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Charset of static files
|
|
|
|
|
/// </summary>
|
|
|
|
|
STATIC_FILES_CONTENT_CHARSET = TMVCConstants.DEFAULT_CONTENT_CHARSET;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-04-24 02:48:39 +02:00
|
|
|
|
TMVCStaticFilesMiddleware = class(TInterfacedObject, IMVCMiddleware)
|
|
|
|
|
private
|
|
|
|
|
fMediaTypes: TDictionary<string, string>;
|
2020-04-25 01:48:07 +02:00
|
|
|
|
fStaticFilesPath: string;
|
|
|
|
|
fDocumentRoot: string;
|
|
|
|
|
fIndexDocument: string;
|
|
|
|
|
fStaticFilesCharset: string;
|
2020-04-30 11:26:32 +02:00
|
|
|
|
fSPAWebAppSupport: Boolean;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
procedure AddMediaTypes;
|
2020-09-14 15:52:50 +02:00
|
|
|
|
function IsStaticFileRequest(const APathInfo: string; out AFileName: string;
|
|
|
|
|
out AIsDirectoryTraversalAttach: Boolean): Boolean;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
function SendStaticFileIfPresent(const AContext: TWebContext; const AFileName: string): Boolean;
|
|
|
|
|
public
|
2020-04-25 01:48:07 +02:00
|
|
|
|
constructor Create(
|
|
|
|
|
const AStaticFilesPath: string = TMVCStaticFilesDefaults.STATIC_FILES_PATH;
|
|
|
|
|
const ADocumentRoot: string = TMVCStaticFilesDefaults.DOCUMENT_ROOT;
|
|
|
|
|
const AIndexDocument: string = TMVCStaticFilesDefaults.INDEX_DOCUMENT;
|
2020-04-30 11:26:32 +02:00
|
|
|
|
const ASPAWebAppSupport: Boolean = True;
|
2020-04-25 01:48:07 +02:00
|
|
|
|
const AStaticFilesCharset: string = TMVCStaticFilesDefaults.STATIC_FILES_CONTENT_CHARSET);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
destructor Destroy; override;
|
|
|
|
|
|
|
|
|
|
procedure OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
|
|
|
|
|
procedure OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName: string;
|
|
|
|
|
const AActionName: string; var AHandled: Boolean);
|
2020-04-28 01:36:45 +02:00
|
|
|
|
|
2020-04-24 02:48:39 +02:00
|
|
|
|
procedure OnAfterControllerAction(AContext: TWebContext; const AActionName: string; const AHandled: Boolean);
|
|
|
|
|
|
2020-04-28 01:36:45 +02:00
|
|
|
|
procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
|
|
uses
|
|
|
|
|
System.SysUtils,
|
2020-05-02 16:39:32 +02:00
|
|
|
|
System.NetEncoding,
|
|
|
|
|
System.IOUtils, System.Classes;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
|
|
|
|
|
{ TMVCStaticFilesMiddleware }
|
|
|
|
|
|
|
|
|
|
procedure TMVCStaticFilesMiddleware.AddMediaTypes;
|
|
|
|
|
begin
|
|
|
|
|
fMediaTypes.Add('.html', TMVCMediaType.TEXT_HTML);
|
|
|
|
|
fMediaTypes.Add('.htm', TMVCMediaType.TEXT_HTML);
|
|
|
|
|
fMediaTypes.Add('.txt', TMVCMediaType.TEXT_PLAIN);
|
2020-04-26 22:56:20 +02:00
|
|
|
|
fMediaTypes.Add('.text', TMVCMediaType.TEXT_PLAIN);
|
|
|
|
|
fMediaTypes.Add('.csv', TMVCMediaType.TEXT_CSV);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
fMediaTypes.Add('.css', TMVCMediaType.TEXT_CSS);
|
|
|
|
|
fMediaTypes.Add('.js', TMVCMediaType.TEXT_JAVASCRIPT);
|
|
|
|
|
fMediaTypes.Add('.jpg', TMVCMediaType.IMAGE_JPEG);
|
|
|
|
|
fMediaTypes.Add('.jpeg', TMVCMediaType.IMAGE_JPEG);
|
2020-04-26 22:56:20 +02:00
|
|
|
|
fMediaTypes.Add('.jpe', TMVCMediaType.IMAGE_JPEG);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
fMediaTypes.Add('.png', TMVCMediaType.IMAGE_PNG);
|
|
|
|
|
fMediaTypes.Add('.ico', TMVCMediaType.IMAGE_X_ICON);
|
|
|
|
|
fMediaTypes.Add('.appcache', TMVCMediaType.TEXT_CACHEMANIFEST);
|
2020-04-26 22:56:20 +02:00
|
|
|
|
fMediaTypes.Add('.svg', TMVCMediaType.IMAGE_SVG_XML);
|
|
|
|
|
fMediaTypes.Add('.svgz', TMVCMediaType.IMAGE_SVG_XML);
|
2020-04-30 11:26:32 +02:00
|
|
|
|
fMediaTypes.Add('.gif', TMVCMediaType.IMAGE_GIF);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
|
|
|
|
|
2020-04-30 11:26:32 +02:00
|
|
|
|
constructor TMVCStaticFilesMiddleware.Create(
|
|
|
|
|
const AStaticFilesPath: string = TMVCStaticFilesDefaults.STATIC_FILES_PATH;
|
|
|
|
|
const ADocumentRoot: string = TMVCStaticFilesDefaults.DOCUMENT_ROOT;
|
|
|
|
|
const AIndexDocument: string = TMVCStaticFilesDefaults.INDEX_DOCUMENT;
|
|
|
|
|
const ASPAWebAppSupport: Boolean = True;
|
|
|
|
|
const AStaticFilesCharset: string = TMVCStaticFilesDefaults.STATIC_FILES_CONTENT_CHARSET);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
|
|
|
|
inherited Create;
|
2020-05-14 17:41:20 +02:00
|
|
|
|
fStaticFilesPath := AStaticFilesPath;
|
2020-05-02 17:06:59 +02:00
|
|
|
|
fDocumentRoot := TPath.Combine(AppPath, ADocumentRoot);
|
2020-04-25 01:48:07 +02:00
|
|
|
|
fIndexDocument := AIndexDocument;
|
2020-04-26 22:56:20 +02:00
|
|
|
|
fStaticFilesCharset := AStaticFilesCharset;
|
2020-04-30 11:26:32 +02:00
|
|
|
|
fSPAWebAppSupport := ASPAWebAppSupport;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
fMediaTypes := TDictionary<string, string>.Create;
|
|
|
|
|
AddMediaTypes;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
destructor TMVCStaticFilesMiddleware.Destroy;
|
|
|
|
|
begin
|
|
|
|
|
fMediaTypes.Free;
|
|
|
|
|
|
|
|
|
|
inherited Destroy;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-09-14 15:52:50 +02:00
|
|
|
|
function TMVCStaticFilesMiddleware.IsStaticFileRequest(const APathInfo: string; out AFileName: string;
|
|
|
|
|
out AIsDirectoryTraversalAttach: Boolean): Boolean;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-09-14 15:52:50 +02:00
|
|
|
|
Result := (not fDocumentRoot.IsEmpty) and (TMVCStaticContents.IsStaticFile(fDocumentRoot, APathInfo, AFileName,
|
|
|
|
|
AIsDirectoryTraversalAttach));
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TMVCStaticFilesMiddleware.OnAfterControllerAction(AContext: TWebContext; const AActionName: string;
|
|
|
|
|
const AHandled: Boolean);
|
|
|
|
|
begin
|
|
|
|
|
// do nothing
|
|
|
|
|
end;
|
|
|
|
|
|
2020-04-28 01:36:45 +02:00
|
|
|
|
procedure TMVCStaticFilesMiddleware.OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
|
|
|
|
begin
|
|
|
|
|
// do nothing
|
|
|
|
|
end;
|
|
|
|
|
|
2020-04-24 02:48:39 +02:00
|
|
|
|
procedure TMVCStaticFilesMiddleware.OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName,
|
|
|
|
|
AActionName: string; var AHandled: Boolean);
|
|
|
|
|
begin
|
|
|
|
|
// do nothing
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TMVCStaticFilesMiddleware.OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
|
|
|
|
|
var
|
2020-04-25 01:48:07 +02:00
|
|
|
|
lPathInfo: string;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
lFileName: string;
|
2020-09-11 18:14:28 +02:00
|
|
|
|
lRequestedFolder: string;
|
2020-09-14 15:52:50 +02:00
|
|
|
|
lIsDirTraversalAttack: Boolean;
|
|
|
|
|
lPotentialDirName: string;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-04-25 01:48:07 +02:00
|
|
|
|
lPathInfo := AContext.Request.PathInfo;
|
|
|
|
|
|
2020-09-14 15:52:50 +02:00
|
|
|
|
if not lPathInfo.StartsWith(fStaticFilesPath, True) then
|
2020-04-25 01:48:07 +02:00
|
|
|
|
begin
|
|
|
|
|
AHandled := False;
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-05-14 17:41:20 +02:00
|
|
|
|
{
|
|
|
|
|
If user ask for
|
|
|
|
|
www.server.it/folder
|
|
|
|
|
the browser is redirected to
|
|
|
|
|
www.server.it/folder/
|
|
|
|
|
}
|
|
|
|
|
if SameText(lPathInfo, fStaticFilesPath) then
|
|
|
|
|
begin
|
|
|
|
|
if (not lPathInfo.EndsWith('/')) and (not fIndexDocument.IsEmpty) then
|
|
|
|
|
begin
|
|
|
|
|
AContext.Response.StatusCode := HTTP_STATUS.MovedPermanently;
|
|
|
|
|
AContext.Response.CustomHeaders.Values['Location'] := lPathInfo + '/';
|
|
|
|
|
AHandled := True;
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-09-14 15:52:50 +02:00
|
|
|
|
// removes the staticfilespath from the requestedpath
|
|
|
|
|
// if not((fStaticFilesPath = '/') or (fStaticFilesPath = '')) then
|
|
|
|
|
// begin
|
|
|
|
|
if lPathInfo.StartsWith(fStaticFilesPath + '/', True) then
|
2020-04-25 01:48:07 +02:00
|
|
|
|
begin
|
|
|
|
|
lPathInfo := lPathInfo.Remove(0, fStaticFilesPath.Length);
|
|
|
|
|
end;
|
2020-09-14 15:52:50 +02:00
|
|
|
|
// end;
|
2020-04-25 01:48:07 +02:00
|
|
|
|
|
2020-09-11 18:14:28 +02:00
|
|
|
|
// check if it's a direct file request
|
2020-09-14 15:52:50 +02:00
|
|
|
|
if IsStaticFileRequest(lPathInfo, lFileName, lIsDirTraversalAttack) then
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-09-11 18:14:28 +02:00
|
|
|
|
AHandled := SendStaticFileIfPresent(AContext, lFileName);
|
|
|
|
|
if AHandled then
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-09-14 15:52:50 +02:00
|
|
|
|
if lIsDirTraversalAttack then
|
|
|
|
|
begin
|
|
|
|
|
AContext.Response.StatusCode := HTTP_STATUS.NotFound;
|
|
|
|
|
AHandled := True;
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
|
2020-09-11 18:14:28 +02:00
|
|
|
|
// check if pathinfo is a subfolder of staticfilespath
|
|
|
|
|
if (lPathInfo.Length > fStaticFilesPath.Length) and lPathInfo.StartsWith(fStaticFilesPath, True) then
|
|
|
|
|
begin
|
2020-09-14 15:52:50 +02:00
|
|
|
|
// check if the requested path is an actual folder
|
|
|
|
|
lPotentialDirName := TPath.Combine(fDocumentRoot, lPathInfo.Substring(1).Replace('/', PathDelim, [rfReplaceAll]));
|
|
|
|
|
if TDirectory.Exists(lPotentialDirName) then
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-09-14 15:52:50 +02:00
|
|
|
|
if (not lPathInfo.EndsWith('/')) and (not fIndexDocument.IsEmpty) then
|
|
|
|
|
begin
|
|
|
|
|
AContext.Response.StatusCode := HTTP_STATUS.MovedPermanently;
|
|
|
|
|
AContext.Response.CustomHeaders.Values['Location'] := lPathInfo + '/';
|
|
|
|
|
AHandled := True;
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
|
|
|
|
end;
|
2020-04-30 11:26:32 +02:00
|
|
|
|
|
2020-09-11 18:14:28 +02:00
|
|
|
|
lRequestedFolder := fDocumentRoot;
|
|
|
|
|
|
|
|
|
|
if (not AHandled) and (not fIndexDocument.IsEmpty) then
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-09-11 18:14:28 +02:00
|
|
|
|
if (lPathInfo = '/') or (lPathInfo = '') then
|
|
|
|
|
begin
|
|
|
|
|
lFileName := TPath.GetFullPath(TPath.Combine(lRequestedFolder, fIndexDocument));
|
|
|
|
|
AHandled := SendStaticFileIfPresent(AContext, lFileName);
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
begin
|
|
|
|
|
if lPathInfo.StartsWith('/') then
|
|
|
|
|
begin
|
|
|
|
|
lPathInfo := lPathInfo.Substring(1);
|
|
|
|
|
end;
|
|
|
|
|
lRequestedFolder := TPath.Combine(lRequestedFolder, lPathInfo.Replace('/', PathDelim, [rfReplaceAll]));
|
|
|
|
|
lFileName := TPath.Combine(lRequestedFolder, fIndexDocument);
|
|
|
|
|
AHandled := SendStaticFileIfPresent(AContext, lFileName);
|
|
|
|
|
end;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
2020-04-30 11:26:32 +02:00
|
|
|
|
|
|
|
|
|
if (not AHandled) and fSPAWebAppSupport and AContext.Request.ClientPreferHTML and (not fIndexDocument.IsEmpty) then
|
|
|
|
|
begin
|
2020-09-11 18:14:28 +02:00
|
|
|
|
while (not lRequestedFolder.IsEmpty) and (not TDirectory.Exists(lRequestedFolder)) do
|
|
|
|
|
begin
|
|
|
|
|
lRequestedFolder := TDirectory.GetParent(lRequestedFolder);
|
|
|
|
|
end;
|
|
|
|
|
if not lRequestedFolder.IsEmpty then
|
|
|
|
|
lFileName := TPath.GetFullPath(TPath.Combine(lRequestedFolder, fIndexDocument))
|
|
|
|
|
else
|
|
|
|
|
lFileName := TPath.GetFullPath(TPath.Combine(fDocumentRoot, fIndexDocument));
|
2020-04-30 11:26:32 +02:00
|
|
|
|
AHandled := SendStaticFileIfPresent(AContext, lFileName);
|
|
|
|
|
end;
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TMVCStaticFilesMiddleware.SendStaticFileIfPresent(const AContext: TWebContext;
|
|
|
|
|
const AFileName: string): Boolean;
|
|
|
|
|
var
|
|
|
|
|
lContentType: string;
|
|
|
|
|
begin
|
|
|
|
|
Result := False;
|
|
|
|
|
if TFile.Exists(AFileName) then
|
|
|
|
|
begin
|
2020-04-30 11:26:32 +02:00
|
|
|
|
if fMediaTypes.TryGetValue(LowerCase(ExtractFileExt(AFileName)), lContentType) then
|
2020-04-24 02:48:39 +02:00
|
|
|
|
begin
|
2020-04-25 01:48:07 +02:00
|
|
|
|
lContentType := BuildContentType(lContentType, fStaticFilesCharset);
|
2020-04-24 02:48:39 +02:00
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
begin
|
|
|
|
|
lContentType := BuildContentType(TMVCMediaType.APPLICATION_OCTETSTREAM, '');
|
|
|
|
|
end;
|
|
|
|
|
TMVCStaticContents.SendFile(AFileName, lContentType, AContext);
|
|
|
|
|
Result := True;
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
end.
|