using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Apewer.Web { /// 静态站点控制器。 public class StaticController : ApiController { #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; 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 自定义 /// 允许服务器端包含(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) { // 自定义处理 var model = GetModel(urlPath); if (model != null) { Response.Model = model; return; } // 自定义处理 var result = GetResult(urlPath); if (result != null) { Response.Model = result; return; } InternalExecute(urlPath); } /// 获取此静态站点的目录。 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(app); return app; } } var www = StorageUtility.CombinePath(app, "www"); if (System.IO.Directory.Exists(www)) { _root = new Class(www); return www; } var web = StorageUtility.CombinePath(app, "web"); if (System.IO.Directory.Exists(web)) { _root = new Class(web); return web; } var @static = StorageUtility.CombinePath(app, "static"); if (System.IO.Directory.Exists(@static)) { _root = new Class(@static); return @static; } 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); #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(); // Zip 文件的缓存 static Dictionary> WebZips = new Dictionary>(); // 解析 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(); { 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; } } /// 读取文本文件。 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; } /// 在指定目录下搜索默认文件。 /// 完整文件路径,当搜索失败时返回 NULL。 string SearchDefaultFile(string directoryPath) { // 获取子文件路径 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(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 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(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 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 /// 列出指定目录的子项。 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; } /// 请求筛选:检查段内容是要被屏蔽。 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 } } ;