|
|
using System; using System.Collections.Generic; using System.IO; using System.Text;
namespace Apewer.Web {
/// <summary>静态站点控制器。</summary>
public class StaticController : ApiController {
#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;
if (_initializer != null) { var jump = _initializer.Invoke(this); if (jump == JumpStatement.Break) return; }
// 获取相对路径
var urlPath = GetUrlPath(); if (string.IsNullOrEmpty(urlPath)) return;
// 内置的处理程序
ApiUtility.StopReturn(Response); Execute(urlPath); }
#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) { // 自定义处理
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>
protected virtual string GetRoot() { if (_root) return _root.Value; var app = RuntimeUtility.ApplicationPath;
var paths = StorageUtility.GetSubFiles(app); foreach (var path in paths) { var split = path.Split('/', '\\'); var lower = split[split.Length - 1]; switch (lower) { case "index.html": case "index.htm": case "default.html": case "default.htm": case "favicon.ico": _root = new Class<string>(app); return app; } }
var www = StorageUtility.CombinePath(app, "www"); if (System.IO.Directory.Exists(www)) { _root = new Class<string>(www); return www; }
var web = StorageUtility.CombinePath(app, "web"); if (System.IO.Directory.Exists(web)) { _root = new Class<string>(web); return web; }
var @static = StorageUtility.CombinePath(app, "static"); if (System.IO.Directory.Exists(@static)) { _root = new Class<string>(@static); return @static; }
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);
#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();
// Zip 文件的缓存
static Dictionary<Type, Dictionary<string, byte[]>> WebZips = new Dictionary<Type, Dictionary<string, byte[]>>();
// 解析 URL 的路径,获取本地路径。
string MapPath(string urlPath) { var path = GetRoot(); if (path.IsEmpty()) return ""; if (string.IsNullOrEmpty(urlPath)) return path;
foreach (var split in urlPath.Split('/', '\\')) { 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 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);
// 本地文件
if (StorageUtility.FileExists(storagePath)) { ExecuteFile(storagePath, urlPath, urlExt, urlIsHtml); return; }
// 目录
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; } } }
// WebZip
var webZip = GetWebZip(); if (webZip != null) { // 文件
{ 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; } } }
// 未知
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 { 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>
string ReadTextFile(string urlPath, int recursive) { // 主文件保留原有的 BOM
var wipeBom = recursive > 0;
// 本地文件
var path = MapPath(urlPath); if (StorageUtility.FileExists(path)) { var data = StorageUtility.ReadFile(path, wipeBom); var text = data.Text(); text = ServerSideIncludes(text, recursive); return text; }
// Zip
var webZip = GetWebZip(); if (webZip != null) { if (webZip.TryGetValue(urlPath, out var data)) { var text = data.Text(); text = ServerSideIncludes(text, recursive); return text; } }
// 文件不存在
return null; }
/// <summary>在指定目录下搜索默认文件。</summary>
/// <returns>完整文件路径,当搜索失败时返回 NULL。</returns>
string SearchDefaultFile(string directoryPath) { // 获取子文件路径
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(filePath).ToLower(); if (dict.ContainsKey(name)) continue; dict.Add(name, filePath); }
// 按顺序获取
foreach (var defaultFileName in DefaultFileNames) { if (dict.TryGetValue("index.html", out var physicalPath)) { return physicalPath; } }
return null; }
Dictionary<string, byte[]> GetWebZip() { var instanceType = GetType(); lock (WebZips) { if (WebZips.TryGetValue(instanceType, out var webZip)) { return webZip; } else { 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 null; }
string ServerSideIncludes(string text, int recursive) { 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(); while (true) { var offset = text.IndexOf(left); if (offset < 0) { sb.Append(text); break; } if (offset > 0) { sb.Append(text.Substring(0, offset)); text = text.Substring(offset + left.Length); } else text = text.Substring(left.Length); var length = text.IndexOf(right); if (length < 1) { sb.Append(left); sb.Append(text); break; } var inner = text.Substring(0, length); var temp = inner.ToTrim(); if (temp.StartsWith(head)) { temp = temp.Substring(head.Length); temp = temp.Replace("\"", "");
var includeText = ReadTextFile(temp, recursive + 1); if (includeText != null && includeText.Length > 0) sb.Append(includeText); } else { sb.Append(left); sb.Append(inner); sb.Append(right); } text = text.Substring(length + right.Length); }
var output = sb.ToString(); return output; }
#endregion
#region private
/// <summary>列出指定目录的子项。</summary>
Json ListChildren(string directory) { if (!System.IO.Directory.Exists(directory)) return null; var json = Json.NewObject(); json.SetProperty("directories", ListDirectories(directory)); json.SetProperty("files", ListFiles(directory)); return json; }
Json ListDirectories(string directory) { var array = Json.NewArray(); var subs = StorageUtility.GetSubDirectories(directory); subs.Sort(); foreach (var sub in subs) { var split = sub.Split('/', '\\'); var name = split[split.Length - 1]; if (IsBlocked(name)) continue;
var json = Json.NewObject(); json.SetProperty("name", name); try { var info = new DirectoryInfo(sub); json.SetProperty("modified", info.LastWriteTimeUtc.Stamp()); } catch { } array.AddItem(json); } return array; }
Json ListFiles(string directory) { var array = Json.NewArray(); var subs = StorageUtility.GetSubFiles(directory); subs.Sort(); foreach (var sub in subs) { var name = Path.GetFileName(sub); if (IsBlocked(name)) continue;
var json = Json.NewObject(); json.SetProperty("name", name); try { var info = new FileInfo(sub); json.SetProperty("size", info.Length); json.SetProperty("modified", info.LastWriteTimeUtc.Stamp()); } catch { } array.AddItem(json); } return array; }
/// <summary>请求筛选:检查段内容是要被屏蔽。</summary>
bool IsBlocked(string segment) { if (string.IsNullOrEmpty(segment)) return true; var lower = segment.ToLower(); switch (lower) { case ".": case "..":
// Synology
case "@eadir": case "#recycle":
// Windows
case "$recycle.bin": case "recycler": case "system volume information": case "desktop.ini": case "thumbs.db":
// macOS
case ".ds_store": case ".localized":
// IIS
case "app_code": case "app_data": case "aspnet_client": case "bin": case "web.config":
return true; } return false; }
#endregion
}
}
|