diff --git a/Apewer/Web/StaticController.cs b/Apewer/Web/StaticController.cs
index 6733cb2..1b0068c 100644
--- a/Apewer/Web/StaticController.cs
+++ b/Apewer/Web/StaticController.cs
@@ -10,61 +10,74 @@ namespace Apewer.Web
public class StaticController : ApiController
{
- /// 允许服务器端包含(Server Side Include)。
- /// 默认值:允许。
- protected bool AllowSSI { get; set; } = true;
-
- /// 当执行目录且没有默认文档时,枚举子目录和子文件。
- /// 默认值:不允许。
- protected bool AllowEnumerate { get; set; } = false;
-
- /// 获取已解析的站点根目录。
- protected string Root { get => _root?.Value; }
-
- List PathSegments;
+ #region 初始化
+ Func _initializer = null;
Class _root = null;
///
public StaticController() : base((c) => { ((StaticController)c).Initialize(); return false; }) { }
+ ///
+ public StaticController(Func 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(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;
- /// 响应 404 状态。
- /// 默认:设置状态为 404,不输出内容。
- protected virtual void Respond404(string path)
- {
- Response.Model = new ApiStatusModel(404);
+ // 内置的处理程序
+ ApiUtility.StopReturn(Response);
+ Execute(urlPath);
}
- /// 响应 403 状态。
- /// 默认:设置状态为 403,不输出内容。
- protected virtual void Respond403(string path)
+ #endregion
+
+ #region 自定义
+
+ /// 允许服务器端包含(Server Side Include)。
+ /// 默认值:允许。
+ protected bool AllowSSI { get; set; } = true;
+
+ /// 允许 GetRoot 方法访问应用程序目录。
+ /// 默认值:不允许。
+ protected bool AllowApplicationPath { get; set; } = false;
+
+ /// 获取已解析的站点根目录。
+ protected string Root { get => _root?.Value; }
+
+ /// 执行路径。
+ 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);
}
/// 获取此静态站点的目录。
@@ -111,156 +124,365 @@ namespace Apewer.Web
return @static;
}
- _root = new Class(app);
- return app;
+ if (AllowApplicationPath)
+ {
+ _root = new Class(app);
+ return app;
+ }
+ else
+ {
+ _root = new Class(null);
+ return null;
+ }
+ }
+
+ /// 获取 URL 路径。
+ protected virtual string GetUrlPath() => Request.Url.AbsolutePath;
+
+ /// 获取响应结果。
+ protected virtual IActionResult GetResult(string urlPath) => null;
+
+ /// 获取响应结果。
+ protected virtual ApiModel GetModel(string urlPath) => null;
+
+ /// 加载 Web.zip 文件。
+ protected virtual Dictionary 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;
}
/// 从扩展名获取内容类型。
protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension);
- /// 从扩展名和文件路径获取过期时间。
- /// 默认值:0,不缓存。
- protected virtual int Expires(string extension, string path) => 0;
+ #endregion
+
+ #region 开放方法
+
+ /// 响应重定向。
+ protected virtual void RespondRedirect(string urlPath, string location) => Response.Model = new ApiRedirectModel(location);
+
+ /// 响应 400 状态。
+ /// 默认:设置状态为 400,不输出内容。
+ protected virtual void Respond400(string path, string reason) => Response.Model = new ApiStatusModel(400);
+
+ /// 响应 404 状态。
+ /// 默认:设置状态为 404,不输出内容。
+ protected virtual void Respond404(string urlPath) => Response.Model = new ApiStatusModel(404);
+
+ /// 响应 403 状态。
+ /// 默认:设置状态为 403,不输出内容。
+ 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();
- /// 已解析到本地文本路径,执行此路径。
- /// 默认:输出文件内容,文件不存在时输出 404 状态。
- protected virtual void File(string path)
+ // Zip 文件的缓存
+ static Dictionary> WebZips = new Dictionary>();
+
+ // 解析 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();
+ {
+ 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;
}
}
- /// 已解析到本地目录路径,执行此路径。
- /// 默认:输出文件内容,文件不存在时输出 404 状态。
- protected virtual void Directory(string path)
+ /// 读取文本文件。
+ 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;
}
/// 在指定目录下搜索默认文件。
/// 完整文件路径,当搜索失败时返回 NULL。
- 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(subs.Count);
- foreach (var sub in subs)
+ // 获取子文件路径
+ var filePaths = StorageUtility.GetSubFiles(directoryPath);
+ if (filePaths.Count < 0) return null;
+ filePaths.Sort();
+
+ // 获取文件名
+ var dict = new Dictionary(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 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(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 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
+
/// 列出指定目录的子项。
Json ListChildren(string directory)
{
@@ -363,7 +589,8 @@ namespace Apewer.Web
return array;
}
- static bool IsBlocked(string segment)
+ /// 请求筛选:检查段内容是要被屏蔽。
+ bool IsBlocked(string segment)
{
if (string.IsNullOrEmpty(segment)) return true;
var lower = segment.ToLower();