You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

406 lines
14 KiB

4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. namespace Apewer.Web
  6. {
  7. /// <summary>静态站点控制器。</summary>
  8. public class StaticController : ApiController
  9. {
  10. /// <summary>允许服务器端包含(Server Side Include)。</summary>
  11. /// <remarks>默认值:允许。</remarks>
  12. protected bool AllowSSI { get; set; } = true;
  13. /// <summary>当执行目录且没有默认文档时,枚举子目录和子文件。</summary>
  14. /// <remarks>默认值:不允许。</remarks>
  15. protected bool AllowEnumerate { get; set; } = false;
  16. /// <summary>获取已解析的站点根目录。</summary>
  17. protected string Root { get => _root?.Value; }
  18. List<string> PathSegments;
  19. Class<string> _root = null;
  20. /// <summary></summary>
  21. public StaticController() : base((c) => { ((StaticController)c).Initialize(); return false; }) { }
  22. void Initialize()
  23. {
  24. if (Request == null || Request.Url == null) return;
  25. var absolute = Request.Url.AbsolutePath;
  26. var split = absolute == null ? new string[0] : absolute.Split('/', '\\');
  27. PathSegments = new List<string>(split.Length);
  28. foreach (var item in split)
  29. {
  30. var trim = TextUtility.Trim(TextUtility.DecodeUrl(item));
  31. if (string.IsNullOrEmpty(trim)) continue;
  32. if (trim == "." || trim == "..") continue;
  33. PathSegments.Add(trim);
  34. }
  35. absolute = TextUtility.Join("/", PathSegments);
  36. var path = MapPath(absolute);
  37. if (PathSegments.Count < 1) Directory(GetRoot());
  38. else if (IsBlocked(PathSegments[0])) Respond404(path);
  39. else if (StorageUtility.FileExists(path)) File(path);
  40. else if (StorageUtility.DirectoryExists(path)) Directory(path);
  41. else Respond404(path);
  42. }
  43. #region virtual
  44. /// <summary>响应 404 状态。</summary>
  45. /// <remarks>默认:设置状态为 404,不输出内容。</remarks>
  46. protected virtual void Respond404(string path)
  47. {
  48. Response.Model = new ApiStatusModel(404);
  49. }
  50. /// <summary>响应 403 状态。</summary>
  51. /// <remarks>默认:设置状态为 403,不输出内容。</remarks>
  52. protected virtual void Respond403(string path)
  53. {
  54. Response.Model = new ApiStatusModel(403);
  55. }
  56. /// <summary>获取此静态站点的目录。</summary>
  57. protected virtual string GetRoot()
  58. {
  59. if (_root) return _root.Value;
  60. var app = RuntimeUtility.ApplicationPath;
  61. var paths = StorageUtility.GetSubFiles(app);
  62. foreach (var path in paths)
  63. {
  64. var split = path.Split('/', '\\');
  65. var lower = split[split.Length - 1];
  66. switch (lower)
  67. {
  68. case "index.html":
  69. case "index.htm":
  70. case "default.html":
  71. case "default.htm":
  72. case "favicon.ico":
  73. _root = new Class<string>(app);
  74. return app;
  75. }
  76. }
  77. var www = StorageUtility.CombinePath(app, "www");
  78. if (System.IO.Directory.Exists(www))
  79. {
  80. _root = new Class<string>(www);
  81. return www;
  82. }
  83. var web = StorageUtility.CombinePath(app, "web");
  84. if (System.IO.Directory.Exists(web))
  85. {
  86. _root = new Class<string>(web);
  87. return web;
  88. }
  89. var @static = StorageUtility.CombinePath(app, "static");
  90. if (System.IO.Directory.Exists(@static))
  91. {
  92. _root = new Class<string>(@static);
  93. return @static;
  94. }
  95. _root = new Class<string>(app);
  96. return app;
  97. }
  98. /// <summary>从扩展名获取内容类型。</summary>
  99. protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension);
  100. /// <summary>从扩展名和文件路径获取过期时间。</summary>
  101. /// <remarks>默认值:0,不缓存。</remarks>
  102. protected virtual int Expires(string extension, string path) => 0;
  103. /// <summary>已解析到本地文本路径,执行此路径。</summary>
  104. /// <remarks>默认:输出文件内容,文件不存在时输出 404 状态。</remarks>
  105. protected virtual void File(string path)
  106. {
  107. if (!System.IO.File.Exists(path))
  108. {
  109. Respond404(path);
  110. return;
  111. }
  112. // 获取文件扩展名。
  113. var ext = Path.GetExtension(path).Lower();
  114. if (ext.Length > 1 && ext.StartsWith(".")) ext = ext.Substring(1);
  115. // 按扩展名获取缓存过期时间。
  116. var expires = Expires(ext, path);
  117. // 按扩展名获取 Content-Type。
  118. var type = ContentType(ext);
  119. if (string.IsNullOrEmpty(type)) type = NetworkUtility.Mime(ext);
  120. // Server Side Includes
  121. if (AllowSSI && ext == "html" || ext == "htm" || ext == "shtml")
  122. {
  123. var html = ReadWithSSI(path);
  124. var bytes = html.Bytes();
  125. var model = new ApiBytesModel();
  126. if (expires > 0) model.Expires = expires;
  127. model.ContentType = type;
  128. model.Bytes = bytes;
  129. Response.Model = model;
  130. }
  131. else
  132. {
  133. var stream = StorageUtility.OpenFile(path, true);
  134. var model = new ApiStreamModel();
  135. if (expires > 0) model.Expires = expires;
  136. model.ContentType = type;
  137. model.AutoDispose = true;
  138. model.Stream = stream;
  139. Response.Model = model;
  140. }
  141. }
  142. /// <summary>已解析到本地目录路径,执行此路径。</summary>
  143. /// <remarks>默认:输出文件内容,文件不存在时输出 404 状态。</remarks>
  144. protected virtual void Directory(string path)
  145. {
  146. if (!System.IO.Directory.Exists(path))
  147. {
  148. Respond404(path);
  149. return;
  150. }
  151. var @default = Default(path);
  152. if (!string.IsNullOrEmpty(@default))
  153. {
  154. File(@default);
  155. return;
  156. }
  157. if (AllowEnumerate) Response.Data = ListChildren(path);
  158. else Respond403(path);
  159. }
  160. /// <summary>在指定目录下搜索默认文件。</summary>
  161. /// <returns>完整文件路径,当搜索失败时返回 NULL。</returns>
  162. protected string Default(string directory)
  163. {
  164. var subs = StorageUtility.GetSubFiles(directory);
  165. if (subs.Count < 0) return null;
  166. subs.Sort();
  167. var names = new Dictionary<string, string>(subs.Count);
  168. foreach (var sub in subs)
  169. {
  170. var name = Path.GetFileName(sub);
  171. if (names.ContainsKey(name)) continue;
  172. names.Add(name, sub);
  173. }
  174. foreach (var sub in subs)
  175. {
  176. var name = Path.GetFileName(sub);
  177. var lower = name.ToLower();
  178. if (lower == name) continue;
  179. if (names.ContainsKey(lower)) continue;
  180. names.Add(lower, sub);
  181. }
  182. if (names.ContainsKey("index.html")) return names["index.html"];
  183. if (names.ContainsKey("index.htm")) return names["index.htm"];
  184. if (names.ContainsKey("default.html")) return names["default.html"];
  185. if (names.ContainsKey("default.htm")) return names["default.htm"];
  186. return null;
  187. }
  188. #endregion
  189. #region private
  190. // 解析 URL 的路径,获取本地路径。
  191. string MapPath(string urlPath)
  192. {
  193. var path = GetRoot();
  194. if (!string.IsNullOrEmpty(urlPath))
  195. {
  196. foreach (var split in urlPath.Split('/'))
  197. {
  198. var seg = split.ToTrim();
  199. if (string.IsNullOrEmpty(seg)) continue;
  200. if (seg == "." || seg == "..") continue;
  201. path = StorageUtility.CombinePath(path, seg);
  202. }
  203. }
  204. return path;
  205. }
  206. // Server Side Includes
  207. string ReadWithSSI(string path, int recursive = 0)
  208. {
  209. if (recursive > 10) return "";
  210. var input = StorageUtility.ReadFile(path, true);
  211. if (input == null || input.LongLength < 1) return "";
  212. // 尝试解码。
  213. var html = TextUtility.FromBytes(input);
  214. if (string.IsNullOrEmpty(html)) return "";
  215. // 按首尾截取。
  216. const string left = "<!--";
  217. const string right = "-->";
  218. const string head = "#include virtual=";
  219. var sb = new StringBuilder();
  220. var text = html;
  221. while (true)
  222. {
  223. var offset = text.IndexOf(left);
  224. if (offset < 0)
  225. {
  226. sb.Append(text);
  227. break;
  228. }
  229. if (offset > 0)
  230. {
  231. sb.Append(text.Substring(0, offset));
  232. text = text.Substring(offset + left.Length);
  233. }
  234. else text = text.Substring(left.Length);
  235. var length = text.IndexOf(right);
  236. if (length < 1)
  237. {
  238. sb.Append(left);
  239. sb.Append(text);
  240. break;
  241. }
  242. var inner = text.Substring(0, length);
  243. var temp = inner.ToTrim();
  244. if (temp.StartsWith(head))
  245. {
  246. temp = temp.Substring(head.Length);
  247. temp = temp.Replace("\"", "");
  248. var subPath = MapPath(temp);
  249. var subText = ReadWithSSI(subPath, recursive + 1);
  250. if (subText != null && subText.Length > 0) sb.Append(subText);
  251. }
  252. else
  253. {
  254. sb.Append(left);
  255. sb.Append(inner);
  256. sb.Append(right);
  257. }
  258. text = text.Substring(length + right.Length);
  259. }
  260. var output = sb.ToString();
  261. return output;
  262. }
  263. /// <summary>列出指定目录的子项。</summary>
  264. Json ListChildren(string directory)
  265. {
  266. if (!System.IO.Directory.Exists(directory)) return null;
  267. var json = Json.NewObject();
  268. json.SetProperty("directories", ListDirectories(directory));
  269. json.SetProperty("files", ListFiles(directory));
  270. return json;
  271. }
  272. Json ListDirectories(string directory)
  273. {
  274. var array = Json.NewArray();
  275. var subs = StorageUtility.GetSubDirectories(directory);
  276. subs.Sort();
  277. foreach (var sub in subs)
  278. {
  279. var split = sub.Split('/', '\\');
  280. var name = split[split.Length - 1];
  281. if (IsBlocked(name)) continue;
  282. var json = Json.NewObject();
  283. json.SetProperty("name", name);
  284. try
  285. {
  286. var info = new DirectoryInfo(sub);
  287. json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
  288. }
  289. catch { }
  290. array.AddItem(json);
  291. }
  292. return array;
  293. }
  294. Json ListFiles(string directory)
  295. {
  296. var array = Json.NewArray();
  297. var subs = StorageUtility.GetSubFiles(directory);
  298. subs.Sort();
  299. foreach (var sub in subs)
  300. {
  301. var name = Path.GetFileName(sub);
  302. if (IsBlocked(name)) continue;
  303. var json = Json.NewObject();
  304. json.SetProperty("name", name);
  305. try
  306. {
  307. var info = new FileInfo(sub);
  308. json.SetProperty("size", info.Length);
  309. json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
  310. }
  311. catch { }
  312. array.AddItem(json);
  313. }
  314. return array;
  315. }
  316. static bool IsBlocked(string segment)
  317. {
  318. if (string.IsNullOrEmpty(segment)) return true;
  319. var lower = segment.ToLower();
  320. switch (lower)
  321. {
  322. case ".":
  323. case "..":
  324. // Synology
  325. case "@eadir":
  326. case "#recycle":
  327. // Windows
  328. case "$recycle.bin":
  329. case "recycler":
  330. case "system volume information":
  331. case "desktop.ini":
  332. case "thumbs.db":
  333. // macOS
  334. case ".ds_store":
  335. case ".localized":
  336. // IIS
  337. case "app_code":
  338. case "app_data":
  339. case "aspnet_client":
  340. case "bin":
  341. case "web.config":
  342. return true;
  343. }
  344. return false;
  345. }
  346. #endregion
  347. }
  348. }