Browse Source

StaticController 增加 Web.zip 的支持

master
王厅 2 days ago
parent
commit
d26056daaf
  1. 493
      Apewer/Web/StaticController.cs

493
Apewer/Web/StaticController.cs

@ -10,61 +10,74 @@ namespace Apewer.Web
public class StaticController : ApiController
{
/// <summary>允许服务器端包含(Server Side Include)。</summary>
/// <remarks>默认值:允许。</remarks>
protected bool AllowSSI { get; set; } = true;
/// <summary>当执行目录且没有默认文档时,枚举子目录和子文件。</summary>
/// <remarks>默认值:不允许。</remarks>
protected bool AllowEnumerate { get; set; } = false;
/// <summary>获取已解析的站点根目录。</summary>
protected string Root { get => _root?.Value; }
List<string> PathSegments;
#region 初始化
Func<StaticController, JumpStatement> _initializer = null;
Class<string> _root = null;
/// <summary></summary>
public StaticController() : base((c) => { ((StaticController)c).Initialize(); return false; }) { }
/// <summary></summary>
public StaticController(Func<StaticController, JumpStatement> initializer) : base((c) => { ((StaticController)c).Initialize(); return false; })
{
_initializer = initializer;
}
void Initialize()
{
if (Request == null || Request.Url == null) return;
var absolute = Request.Url.AbsolutePath;
var split = absolute == null ? new string[0] : absolute.Split('/', '\\');
PathSegments = new List<string>(split.Length);
foreach (var item in split)
if (_initializer != null)
{
var trim = TextUtility.Trim(TextUtility.DecodeUrl(item));
if (string.IsNullOrEmpty(trim)) continue;
if (trim == "." || trim == "..") continue;
PathSegments.Add(trim);
var jump = _initializer.Invoke(this);
if (jump == JumpStatement.Break) return;
}
absolute = TextUtility.Join("/", PathSegments);
var path = MapPath(absolute);
if (PathSegments.Count < 1) Directory(GetRoot());
else if (IsBlocked(PathSegments[0])) Respond404(path);
else if (StorageUtility.FileExists(path)) File(path);
else if (StorageUtility.DirectoryExists(path)) Directory(path);
else Respond404(path);
}
#region virtual
// 获取相对路径
var urlPath = GetUrlPath();
if (string.IsNullOrEmpty(urlPath)) return;
/// <summary>响应 404 状态。</summary>
/// <remarks>默认:设置状态为 404,不输出内容。</remarks>
protected virtual void Respond404(string path)
{
Response.Model = new ApiStatusModel(404);
// 内置的处理程序
ApiUtility.StopReturn(Response);
Execute(urlPath);
}
/// <summary>响应 403 状态。</summary>
/// <remarks>默认:设置状态为 403,不输出内容。</remarks>
protected virtual void Respond403(string path)
#endregion
#region 自定义
/// <summary>允许服务器端包含(Server Side Include)。</summary>
/// <remarks>默认值:允许。</remarks>
protected bool AllowSSI { get; set; } = true;
/// <summary>允许 GetRoot 方法访问应用程序目录。</summary>
/// <remarks>默认值:不允许。</remarks>
protected bool AllowApplicationPath { get; set; } = false;
/// <summary>获取已解析的站点根目录。</summary>
protected string Root { get => _root?.Value; }
/// <summary>执行路径。</summary>
protected virtual void Execute(string urlPath)
{
Response.Model = new ApiStatusModel(403);
// 自定义处理
var model = GetModel(urlPath);
if (model != null)
{
Response.Model = model;
return;
}
// 自定义处理
var result = GetResult(urlPath);
if (result != null)
{
Response.Model = result;
return;
}
InternalExecute(urlPath);
}
/// <summary>获取此静态站点的目录。</summary>
@ -111,156 +124,365 @@ namespace Apewer.Web
return @static;
}
_root = new Class<string>(app);
return app;
if (AllowApplicationPath)
{
_root = new Class<string>(app);
return app;
}
else
{
_root = new Class<string>(null);
return null;
}
}
/// <summary>获取 URL 路径。</summary>
protected virtual string GetUrlPath() => Request.Url.AbsolutePath;
/// <summary>获取响应结果。</summary>
protected virtual IActionResult GetResult(string urlPath) => null;
/// <summary>获取响应结果。</summary>
protected virtual ApiModel GetModel(string urlPath) => null;
/// <summary>加载 Web.zip 文件。</summary>
protected virtual Dictionary<string, byte[]> LoadWebZip()
{
var names = new string[] { "Web.zip", "web.zip" };
foreach (var name in names)
{
var path = StorageUtility.CombinePath(RuntimeUtility.ApplicationPath, name);
if (StorageUtility.FileExists(path))
{
var data = StorageUtility.ReadFile(path);
var dict = BytesUtility.FromZip(data);
return dict;
}
}
return null;
}
/// <summary>从扩展名获取内容类型。</summary>
protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension);
/// <summary>从扩展名和文件路径获取过期时间。</summary>
/// <remarks>默认值:0,不缓存。</remarks>
protected virtual int Expires(string extension, string path) => 0;
#endregion
#region 开放方法
/// <summary>响应重定向。</summary>
protected virtual void RespondRedirect(string urlPath, string location) => Response.Model = new ApiRedirectModel(location);
/// <summary>响应 400 状态。</summary>
/// <remarks>默认:设置状态为 400,不输出内容。</remarks>
protected virtual void Respond400(string path, string reason) => Response.Model = new ApiStatusModel(400);
/// <summary>响应 404 状态。</summary>
/// <remarks>默认:设置状态为 404,不输出内容。</remarks>
protected virtual void Respond404(string urlPath) => Response.Model = new ApiStatusModel(404);
/// <summary>响应 403 状态。</summary>
/// <remarks>默认:设置状态为 403,不输出内容。</remarks>
protected virtual void Respond403(string urlPath) => Response.Model = new ApiStatusModel(403);
#endregion
#region 内置的执行程序
const bool WipeBom = true;
// 默认文档的文件名
static string[] DefaultFileNames = new string[] { "index.html", "index.htm", "default.html", "default.htm" };
// 文件名中不允许出现的字符
static char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
/// <summary>已解析到本地文本路径,执行此路径。</summary>
/// <remarks>默认:输出文件内容,文件不存在时输出 404 状态。</remarks>
protected virtual void File(string path)
// Zip 文件的缓存
static Dictionary<Type, Dictionary<string, byte[]>> WebZips = new Dictionary<Type, Dictionary<string, byte[]>>();
// 解析 URL 的路径,获取本地路径。
string MapPath(string urlPath)
{
if (!System.IO.File.Exists(path))
var path = GetRoot();
if (path.IsEmpty()) return "";
if (string.IsNullOrEmpty(urlPath)) return path;
foreach (var split in urlPath.Split('/', '\\'))
{
Respond404(path);
var segment = split.ToTrim();
if (segment == null || segment == "" || segment == "." || segment == "..") continue;
path = StorageUtility.CombinePath(path, segment);
}
return path;
}
void InternalExecute(string urlPath)
{
if (urlPath.IsEmpty())
{
Respond400(urlPath, "请求的 URL 路径无效。");
return;
}
// 获取文件扩展名。
var ext = Path.GetExtension(path).Lower();
if (ext.Length > 1 && ext.StartsWith(".")) ext = ext.Substring(1);
// 检查路径
var urlIsDir = urlPath.EndsWith("/") || urlPath.EndsWith("\\");
var urlExt = null as string;
var urlIsHtml = false;
var segments = new List<string>();
{
var split = urlPath == null ? new string[0] : urlPath.Split('/', '\\');
segments.Capacity = split.Length;
foreach (var item in split)
{
var segment = TextUtility.DecodeUrl(item).Trim();
if (string.IsNullOrEmpty(segment)) continue;
if (segment == "." || segment == "..") continue;
// 检查无效字符
foreach (var c in segment)
{
if (Array.IndexOf(InvalidFileNameChars, c) > -1)
{
Respond400(urlPath, $"请求的路径中含有无效字符。");
return;
}
}
segments.Add(segment);
}
// 重置 UrlPath
urlPath = "/" + segments.Join("/");
if (segments.Count > 0)
{
// 请求筛选
if (IsBlocked(segments[0]))
{
Respond404(urlPath);
return;
}
urlExt = segments[segments.Count - 1].ToLower().Split('.').Last();
urlIsHtml = urlExt == "html" || urlExt == "htm" || urlExt == "shtml";
}
}
// 本地存储路径
var storagePath = MapPath(urlPath);
// 按扩展名获取缓存过期时间。
var expires = Expires(ext, path);
// 本地文件
if (StorageUtility.FileExists(storagePath))
{
ExecuteFile(storagePath, urlPath, urlExt, urlIsHtml);
return;
}
// 按扩展名获取 Content-Type。
var type = ContentType(ext);
if (string.IsNullOrEmpty(type)) type = NetworkUtility.Mime(ext);
// 目录
if (StorageUtility.DirectoryExists(storagePath))
{
var filePath = SearchDefaultFile(storagePath);
if (!string.IsNullOrEmpty(filePath))
{
if (urlIsDir)
{
ExecuteFile(filePath, urlPath, urlExt, urlIsHtml);
return;
}
else
{
var location = urlPath + "/";
RespondRedirect(urlPath, location);
return;
}
}
}
// Server Side Includes
if (AllowSSI && ext == "html" || ext == "htm" || ext == "shtml")
// WebZip
var webZip = GetWebZip();
if (webZip != null)
{
var html = ReadWithSSI(path);
var bytes = html.Bytes();
// 文件
{
if (webZip.TryGetValue(urlPath, out var data))
{
ExecuteFile(data, urlExt, urlIsHtml);
return;
}
}
// 尝试匹配默认文档
foreach (var defaultName in DefaultFileNames)
{
var defaultPath = TextUtility.AssureEnds(urlPath, "/") + defaultName;
if (webZip.TryGetValue(defaultPath, out var data))
{
if (urlIsDir)
{
ExecuteFile(data, defaultName, true);
}
else
{
var location = TextUtility.AssureEnds(urlPath, "/");
RespondRedirect(urlPath, location);
}
return;
}
}
}
var model = new ApiBytesModel();
if (expires > 0) model.Expires = expires;
model.ContentType = type;
model.Bytes = bytes;
// 未知
Respond404(urlPath);
}
void ExecuteFile(string storagePath, string urlPath, string urlExt, bool urlIsHtml)
{
var contentType = ContentType(urlExt);
if (urlIsHtml)
{
var text = ReadTextFile(urlPath, 0);
var model = new ApiTextModel(text, contentType);
Response.Model = model;
}
else
{
var stream = StorageUtility.OpenFile(path, true);
var model = new ApiStreamModel();
if (expires > 0) model.Expires = expires;
model.ContentType = type;
model.AutoDispose = true;
model.Stream = stream;
try
{
var stream = StorageUtility.OpenFile(storagePath, true);
var model = new ApiStreamModel();
model.ContentType = contentType;
model.AutoDispose = true;
model.Stream = stream;
Response.Model = model;
}
catch (Exception ex)
{
Respond400(storagePath, $"<{ex.GetType().Name}> {ex.Message}");
}
}
}
void ExecuteFile(byte[] data, string urlExt, bool urlIsHtml)
{
var contentType = ContentType(urlExt);
if (urlIsHtml)
{
var text = data.Text();
text = ServerSideIncludes(text, 0);
var model = new ApiTextModel(text, contentType);
Response.Model = model;
}
else
{
var model = new ApiBytesModel(data, contentType);
Response.Model = model;
}
}
/// <summary>已解析到本地目录路径,执行此路径。</summary>
/// <remarks>默认:输出文件内容,文件不存在时输出 404 状态。</remarks>
protected virtual void Directory(string path)
/// <summary>读取文本文件。</summary>
string ReadTextFile(string urlPath, int recursive)
{
if (!System.IO.Directory.Exists(path))
// 主文件保留原有的 BOM
var wipeBom = recursive > 0;
// 本地文件
var path = MapPath(urlPath);
if (StorageUtility.FileExists(path))
{
Respond404(path);
return;
var data = StorageUtility.ReadFile(path, wipeBom);
var text = data.Text();
text = ServerSideIncludes(text, recursive);
return text;
}
var @default = Default(path);
if (!string.IsNullOrEmpty(@default))
// Zip
var webZip = GetWebZip();
if (webZip != null)
{
File(@default);
return;
if (webZip.TryGetValue(urlPath, out var data))
{
var text = data.Text();
text = ServerSideIncludes(text, recursive);
return text;
}
}
if (AllowEnumerate) Response.Data = ListChildren(path);
else Respond403(path);
// 文件不存在
return null;
}
/// <summary>在指定目录下搜索默认文件。</summary>
/// <returns>完整文件路径,当搜索失败时返回 NULL。</returns>
protected string Default(string directory)
string SearchDefaultFile(string directoryPath)
{
var subs = StorageUtility.GetSubFiles(directory);
if (subs.Count < 0) return null;
subs.Sort();
var names = new Dictionary<string, string>(subs.Count);
foreach (var sub in subs)
// 获取子文件路径
var filePaths = StorageUtility.GetSubFiles(directoryPath);
if (filePaths.Count < 0) return null;
filePaths.Sort();
// 获取文件名
var dict = new Dictionary<string, string>(filePaths.Count);
foreach (var filePath in filePaths)
{
var name = Path.GetFileName(sub);
if (names.ContainsKey(name)) continue;
names.Add(name, sub);
var name = Path.GetFileName(filePath).ToLower();
if (dict.ContainsKey(name)) continue;
dict.Add(name, filePath);
}
foreach (var sub in subs)
// 按顺序获取
foreach (var defaultFileName in DefaultFileNames)
{
var name = Path.GetFileName(sub);
var lower = name.ToLower();
if (lower == name) continue;
if (names.ContainsKey(lower)) continue;
names.Add(lower, sub);
if (dict.TryGetValue("index.html", out var physicalPath))
{
return physicalPath;
}
}
if (names.ContainsKey("index.html")) return names["index.html"];
if (names.ContainsKey("index.htm")) return names["index.htm"];
if (names.ContainsKey("default.html")) return names["default.html"];
if (names.ContainsKey("default.htm")) return names["default.htm"];
return null;
}
#endregion
#region private
// 解析 URL 的路径,获取本地路径。
string MapPath(string urlPath)
Dictionary<string, byte[]> GetWebZip()
{
var path = GetRoot();
if (!string.IsNullOrEmpty(urlPath))
var instanceType = GetType();
lock (WebZips)
{
foreach (var split in urlPath.Split('/'))
if (WebZips.TryGetValue(instanceType, out var webZip))
{
return webZip;
}
else
{
var seg = split.ToTrim();
if (string.IsNullOrEmpty(seg)) continue;
if (seg == "." || seg == "..") continue;
path = StorageUtility.CombinePath(path, seg);
var dict = LoadWebZip();
if (dict != null)
{
var temp = new Dictionary<string, byte[]>(dict.Count);
foreach (var kvp in dict)
{
if (kvp.Key.IsEmpty()) continue;
var url = string.Join("/", kvp.Key.Split('/', '\\'));
url = "/" + url.ToLower();
if (temp.ContainsKey(url)) continue;
temp.Add(url, kvp.Value);
}
dict = temp;
}
WebZips.Add(instanceType, dict);
}
}
return path;
return null;
}
// Server Side Includes
string ReadWithSSI(string path, int recursive = 0)
string ServerSideIncludes(string text, int recursive)
{
if (recursive > 10) return "";
var input = StorageUtility.ReadFile(path, true);
if (input == null || input.LongLength < 1) return "";
// 尝试解码。
var html = TextUtility.FromBytes(input);
if (string.IsNullOrEmpty(html)) return "";
if (!AllowSSI) return text;
if (recursive > 10) return text;
if (string.IsNullOrEmpty(text)) return "";
// 按首尾截取。
const string left = "<!--";
const string right = "-->";
const string head = "#include virtual=";
var sb = new StringBuilder();
var text = html;
while (true)
{
var offset = text.IndexOf(left);
@ -288,9 +510,9 @@ namespace Apewer.Web
{
temp = temp.Substring(head.Length);
temp = temp.Replace("\"", "");
var subPath = MapPath(temp);
var subText = ReadWithSSI(subPath, recursive + 1);
if (subText != null && subText.Length > 0) sb.Append(subText);
var includeText = ReadTextFile(temp, recursive + 1);
if (includeText != null && includeText.Length > 0) sb.Append(includeText);
}
else
{
@ -305,6 +527,10 @@ namespace Apewer.Web
return output;
}
#endregion
#region private
/// <summary>列出指定目录的子项。</summary>
Json ListChildren(string directory)
{
@ -363,7 +589,8 @@ namespace Apewer.Web
return array;
}
static bool IsBlocked(string segment)
/// <summary>请求筛选:检查段内容是要被屏蔽。</summary>
bool IsBlocked(string segment)
{
if (string.IsNullOrEmpty(segment)) return true;
var lower = segment.ToLower();

Loading…
Cancel
Save