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.

633 lines
20 KiB

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
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. #region 初始化
  11. Func<StaticController, JumpStatement> _initializer = null;
  12. Class<string> _root = null;
  13. /// <summary></summary>
  14. public StaticController() : base((c) => { ((StaticController)c).Initialize(); return false; }) { }
  15. /// <summary></summary>
  16. public StaticController(Func<StaticController, JumpStatement> initializer) : base((c) => { ((StaticController)c).Initialize(); return false; })
  17. {
  18. _initializer = initializer;
  19. }
  20. void Initialize()
  21. {
  22. if (Request == null || Request.Url == null) return;
  23. if (_initializer != null)
  24. {
  25. var jump = _initializer.Invoke(this);
  26. if (jump == JumpStatement.Break) return;
  27. }
  28. // 获取相对路径
  29. var urlPath = GetUrlPath();
  30. if (string.IsNullOrEmpty(urlPath)) return;
  31. // 内置的处理程序
  32. ApiUtility.StopReturn(Response);
  33. Execute(urlPath);
  34. }
  35. #endregion
  36. #region 自定义
  37. /// <summary>允许服务器端包含(Server Side Include)。</summary>
  38. /// <remarks>默认值:允许。</remarks>
  39. protected bool AllowSSI { get; set; } = true;
  40. /// <summary>允许 GetRoot 方法访问应用程序目录。</summary>
  41. /// <remarks>默认值:不允许。</remarks>
  42. protected bool AllowApplicationPath { get; set; } = false;
  43. /// <summary>获取已解析的站点根目录。</summary>
  44. protected string Root { get => _root?.Value; }
  45. /// <summary>执行路径。</summary>
  46. protected virtual void Execute(string urlPath)
  47. {
  48. // 自定义处理
  49. var model = GetModel(urlPath);
  50. if (model != null)
  51. {
  52. Response.Model = model;
  53. return;
  54. }
  55. // 自定义处理
  56. var result = GetResult(urlPath);
  57. if (result != null)
  58. {
  59. Response.Model = result;
  60. return;
  61. }
  62. InternalExecute(urlPath);
  63. }
  64. /// <summary>获取此静态站点的目录。</summary>
  65. protected virtual string GetRoot()
  66. {
  67. if (_root) return _root.Value;
  68. var app = RuntimeUtility.ApplicationPath;
  69. var paths = StorageUtility.GetSubFiles(app);
  70. foreach (var path in paths)
  71. {
  72. var split = path.Split('/', '\\');
  73. var lower = split[split.Length - 1];
  74. switch (lower)
  75. {
  76. case "index.html":
  77. case "index.htm":
  78. case "default.html":
  79. case "default.htm":
  80. case "favicon.ico":
  81. _root = new Class<string>(app);
  82. return app;
  83. }
  84. }
  85. var www = StorageUtility.CombinePath(app, "www");
  86. if (System.IO.Directory.Exists(www))
  87. {
  88. _root = new Class<string>(www);
  89. return www;
  90. }
  91. var web = StorageUtility.CombinePath(app, "web");
  92. if (System.IO.Directory.Exists(web))
  93. {
  94. _root = new Class<string>(web);
  95. return web;
  96. }
  97. var @static = StorageUtility.CombinePath(app, "static");
  98. if (System.IO.Directory.Exists(@static))
  99. {
  100. _root = new Class<string>(@static);
  101. return @static;
  102. }
  103. if (AllowApplicationPath)
  104. {
  105. _root = new Class<string>(app);
  106. return app;
  107. }
  108. else
  109. {
  110. _root = new Class<string>(null);
  111. return null;
  112. }
  113. }
  114. /// <summary>获取 URL 路径。</summary>
  115. protected virtual string GetUrlPath() => Request.Url.AbsolutePath;
  116. /// <summary>获取响应结果。</summary>
  117. protected virtual IActionResult GetResult(string urlPath) => null;
  118. /// <summary>获取响应结果。</summary>
  119. protected virtual ApiModel GetModel(string urlPath) => null;
  120. /// <summary>加载 Web.zip 文件。</summary>
  121. protected virtual Dictionary<string, byte[]> LoadWebZip()
  122. {
  123. var names = new string[] { "Web.zip", "web.zip" };
  124. foreach (var name in names)
  125. {
  126. var path = StorageUtility.CombinePath(RuntimeUtility.ApplicationPath, name);
  127. if (StorageUtility.FileExists(path))
  128. {
  129. var data = StorageUtility.ReadFile(path);
  130. var dict = BytesUtility.FromZip(data);
  131. return dict;
  132. }
  133. }
  134. return null;
  135. }
  136. /// <summary>从扩展名获取内容类型。</summary>
  137. protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension);
  138. #endregion
  139. #region 开放方法
  140. /// <summary>响应重定向。</summary>
  141. protected virtual void RespondRedirect(string urlPath, string location) => Response.Model = new ApiRedirectModel(location);
  142. /// <summary>响应 400 状态。</summary>
  143. /// <remarks>默认:设置状态为 400,不输出内容。</remarks>
  144. protected virtual void Respond400(string path, string reason) => Response.Model = new ApiStatusModel(400);
  145. /// <summary>响应 404 状态。</summary>
  146. /// <remarks>默认:设置状态为 404,不输出内容。</remarks>
  147. protected virtual void Respond404(string urlPath) => Response.Model = new ApiStatusModel(404);
  148. /// <summary>响应 403 状态。</summary>
  149. /// <remarks>默认:设置状态为 403,不输出内容。</remarks>
  150. protected virtual void Respond403(string urlPath) => Response.Model = new ApiStatusModel(403);
  151. #endregion
  152. #region 内置的执行程序
  153. const bool WipeBom = true;
  154. // 默认文档的文件名
  155. static string[] DefaultFileNames = new string[] { "index.html", "index.htm", "default.html", "default.htm" };
  156. // 文件名中不允许出现的字符
  157. static char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
  158. // Zip 文件的缓存
  159. static Dictionary<Type, Dictionary<string, byte[]>> WebZips = new Dictionary<Type, Dictionary<string, byte[]>>();
  160. // 解析 URL 的路径,获取本地路径。
  161. string MapPath(string urlPath)
  162. {
  163. var path = GetRoot();
  164. if (path.IsEmpty()) return "";
  165. if (string.IsNullOrEmpty(urlPath)) return path;
  166. foreach (var split in urlPath.Split('/', '\\'))
  167. {
  168. var segment = split.ToTrim();
  169. if (segment == null || segment == "" || segment == "." || segment == "..") continue;
  170. path = StorageUtility.CombinePath(path, segment);
  171. }
  172. return path;
  173. }
  174. void InternalExecute(string urlPath)
  175. {
  176. if (urlPath.IsEmpty())
  177. {
  178. Respond400(urlPath, "请求的 URL 路径无效。");
  179. return;
  180. }
  181. // 检查路径
  182. var urlIsDir = urlPath.EndsWith("/") || urlPath.EndsWith("\\");
  183. var urlExt = null as string;
  184. var urlIsHtml = false;
  185. var segments = new List<string>();
  186. {
  187. var split = urlPath == null ? new string[0] : urlPath.Split('/', '\\');
  188. segments.Capacity = split.Length;
  189. foreach (var item in split)
  190. {
  191. var segment = TextUtility.DecodeUrl(item).Trim();
  192. if (string.IsNullOrEmpty(segment)) continue;
  193. if (segment == "." || segment == "..") continue;
  194. // 检查无效字符
  195. foreach (var c in segment)
  196. {
  197. if (Array.IndexOf(InvalidFileNameChars, c) > -1)
  198. {
  199. Respond400(urlPath, $"请求的路径中含有无效字符。");
  200. return;
  201. }
  202. }
  203. segments.Add(segment);
  204. }
  205. // 重置 UrlPath
  206. urlPath = "/" + segments.Join("/");
  207. if (segments.Count > 0)
  208. {
  209. // 请求筛选
  210. if (IsBlocked(segments[0]))
  211. {
  212. Respond404(urlPath);
  213. return;
  214. }
  215. urlExt = segments[segments.Count - 1].ToLower().Split('.').Last();
  216. urlIsHtml = urlExt == "html" || urlExt == "htm" || urlExt == "shtml";
  217. }
  218. }
  219. // 本地存储路径
  220. var storagePath = MapPath(urlPath);
  221. // 本地文件
  222. if (StorageUtility.FileExists(storagePath))
  223. {
  224. ExecuteFile(storagePath, urlPath, urlExt, urlIsHtml);
  225. return;
  226. }
  227. // 目录
  228. if (StorageUtility.DirectoryExists(storagePath))
  229. {
  230. var filePath = SearchDefaultFile(storagePath);
  231. if (!string.IsNullOrEmpty(filePath))
  232. {
  233. if (urlIsDir)
  234. {
  235. ExecuteFile(filePath, urlPath, urlExt, urlIsHtml);
  236. return;
  237. }
  238. else
  239. {
  240. var location = urlPath + "/";
  241. RespondRedirect(urlPath, location);
  242. return;
  243. }
  244. }
  245. }
  246. // WebZip
  247. var webZip = GetWebZip();
  248. if (webZip != null)
  249. {
  250. // 文件
  251. {
  252. if (webZip.TryGetValue(urlPath, out var data))
  253. {
  254. ExecuteFile(data, urlExt, urlIsHtml);
  255. return;
  256. }
  257. }
  258. // 尝试匹配默认文档
  259. foreach (var defaultName in DefaultFileNames)
  260. {
  261. var defaultPath = TextUtility.AssureEnds(urlPath, "/") + defaultName;
  262. if (webZip.TryGetValue(defaultPath, out var data))
  263. {
  264. if (urlIsDir)
  265. {
  266. ExecuteFile(data, defaultName, true);
  267. }
  268. else
  269. {
  270. var location = TextUtility.AssureEnds(urlPath, "/");
  271. RespondRedirect(urlPath, location);
  272. }
  273. return;
  274. }
  275. }
  276. }
  277. // 未知
  278. Respond404(urlPath);
  279. }
  280. void ExecuteFile(string storagePath, string urlPath, string urlExt, bool urlIsHtml)
  281. {
  282. var contentType = ContentType(urlExt);
  283. if (urlIsHtml)
  284. {
  285. var text = ReadTextFile(urlPath, 0);
  286. var model = new ApiTextModel(text, contentType);
  287. Response.Model = model;
  288. }
  289. else
  290. {
  291. try
  292. {
  293. var stream = StorageUtility.OpenFile(storagePath, true);
  294. var model = new ApiStreamModel();
  295. model.ContentType = contentType;
  296. model.AutoDispose = true;
  297. model.Stream = stream;
  298. Response.Model = model;
  299. }
  300. catch (Exception ex)
  301. {
  302. Respond400(storagePath, $"<{ex.GetType().Name}> {ex.Message}");
  303. }
  304. }
  305. }
  306. void ExecuteFile(byte[] data, string urlExt, bool urlIsHtml)
  307. {
  308. var contentType = ContentType(urlExt);
  309. if (urlIsHtml)
  310. {
  311. var text = data.Text();
  312. text = ServerSideIncludes(text, 0);
  313. var model = new ApiTextModel(text, contentType);
  314. Response.Model = model;
  315. }
  316. else
  317. {
  318. var model = new ApiBytesModel(data, contentType);
  319. Response.Model = model;
  320. }
  321. }
  322. /// <summary>读取文本文件。</summary>
  323. string ReadTextFile(string urlPath, int recursive)
  324. {
  325. // 主文件保留原有的 BOM
  326. var wipeBom = recursive > 0;
  327. // 本地文件
  328. var path = MapPath(urlPath);
  329. if (StorageUtility.FileExists(path))
  330. {
  331. var data = StorageUtility.ReadFile(path, wipeBom);
  332. var text = data.Text();
  333. text = ServerSideIncludes(text, recursive);
  334. return text;
  335. }
  336. // Zip
  337. var webZip = GetWebZip();
  338. if (webZip != null)
  339. {
  340. if (webZip.TryGetValue(urlPath, out var data))
  341. {
  342. var text = data.Text();
  343. text = ServerSideIncludes(text, recursive);
  344. return text;
  345. }
  346. }
  347. // 文件不存在
  348. return null;
  349. }
  350. /// <summary>在指定目录下搜索默认文件。</summary>
  351. /// <returns>完整文件路径,当搜索失败时返回 NULL。</returns>
  352. string SearchDefaultFile(string directoryPath)
  353. {
  354. // 获取子文件路径
  355. var filePaths = StorageUtility.GetSubFiles(directoryPath);
  356. if (filePaths.Count < 0) return null;
  357. filePaths.Sort();
  358. // 获取文件名
  359. var dict = new Dictionary<string, string>(filePaths.Count);
  360. foreach (var filePath in filePaths)
  361. {
  362. var name = Path.GetFileName(filePath).ToLower();
  363. if (dict.ContainsKey(name)) continue;
  364. dict.Add(name, filePath);
  365. }
  366. // 按顺序获取
  367. foreach (var defaultFileName in DefaultFileNames)
  368. {
  369. if (dict.TryGetValue("index.html", out var physicalPath))
  370. {
  371. return physicalPath;
  372. }
  373. }
  374. return null;
  375. }
  376. Dictionary<string, byte[]> GetWebZip()
  377. {
  378. var instanceType = GetType();
  379. lock (WebZips)
  380. {
  381. if (WebZips.TryGetValue(instanceType, out var webZip))
  382. {
  383. return webZip;
  384. }
  385. else
  386. {
  387. var dict = LoadWebZip();
  388. if (dict != null)
  389. {
  390. var temp = new Dictionary<string, byte[]>(dict.Count);
  391. foreach (var kvp in dict)
  392. {
  393. if (kvp.Key.IsEmpty()) continue;
  394. var url = string.Join("/", kvp.Key.Split('/', '\\'));
  395. url = "/" + url.ToLower();
  396. if (temp.ContainsKey(url)) continue;
  397. temp.Add(url, kvp.Value);
  398. }
  399. dict = temp;
  400. }
  401. WebZips.Add(instanceType, dict);
  402. }
  403. }
  404. return null;
  405. }
  406. string ServerSideIncludes(string text, int recursive)
  407. {
  408. if (!AllowSSI) return text;
  409. if (recursive > 10) return text;
  410. if (string.IsNullOrEmpty(text)) return "";
  411. // 按首尾截取。
  412. const string left = "<!--";
  413. const string right = "-->";
  414. const string head = "#include virtual=";
  415. var sb = new StringBuilder();
  416. while (true)
  417. {
  418. var offset = text.IndexOf(left);
  419. if (offset < 0)
  420. {
  421. sb.Append(text);
  422. break;
  423. }
  424. if (offset > 0)
  425. {
  426. sb.Append(text.Substring(0, offset));
  427. text = text.Substring(offset + left.Length);
  428. }
  429. else text = text.Substring(left.Length);
  430. var length = text.IndexOf(right);
  431. if (length < 1)
  432. {
  433. sb.Append(left);
  434. sb.Append(text);
  435. break;
  436. }
  437. var inner = text.Substring(0, length);
  438. var temp = inner.ToTrim();
  439. if (temp.StartsWith(head))
  440. {
  441. temp = temp.Substring(head.Length);
  442. temp = temp.Replace("\"", "");
  443. var includeText = ReadTextFile(temp, recursive + 1);
  444. if (includeText != null && includeText.Length > 0) sb.Append(includeText);
  445. }
  446. else
  447. {
  448. sb.Append(left);
  449. sb.Append(inner);
  450. sb.Append(right);
  451. }
  452. text = text.Substring(length + right.Length);
  453. }
  454. var output = sb.ToString();
  455. return output;
  456. }
  457. #endregion
  458. #region private
  459. /// <summary>列出指定目录的子项。</summary>
  460. Json ListChildren(string directory)
  461. {
  462. if (!System.IO.Directory.Exists(directory)) return null;
  463. var json = Json.NewObject();
  464. json.SetProperty("directories", ListDirectories(directory));
  465. json.SetProperty("files", ListFiles(directory));
  466. return json;
  467. }
  468. Json ListDirectories(string directory)
  469. {
  470. var array = Json.NewArray();
  471. var subs = StorageUtility.GetSubDirectories(directory);
  472. subs.Sort();
  473. foreach (var sub in subs)
  474. {
  475. var split = sub.Split('/', '\\');
  476. var name = split[split.Length - 1];
  477. if (IsBlocked(name)) continue;
  478. var json = Json.NewObject();
  479. json.SetProperty("name", name);
  480. try
  481. {
  482. var info = new DirectoryInfo(sub);
  483. json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
  484. }
  485. catch { }
  486. array.AddItem(json);
  487. }
  488. return array;
  489. }
  490. Json ListFiles(string directory)
  491. {
  492. var array = Json.NewArray();
  493. var subs = StorageUtility.GetSubFiles(directory);
  494. subs.Sort();
  495. foreach (var sub in subs)
  496. {
  497. var name = Path.GetFileName(sub);
  498. if (IsBlocked(name)) continue;
  499. var json = Json.NewObject();
  500. json.SetProperty("name", name);
  501. try
  502. {
  503. var info = new FileInfo(sub);
  504. json.SetProperty("size", info.Length);
  505. json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
  506. }
  507. catch { }
  508. array.AddItem(json);
  509. }
  510. return array;
  511. }
  512. /// <summary>请求筛选:检查段内容是要被屏蔽。</summary>
  513. bool IsBlocked(string segment)
  514. {
  515. if (string.IsNullOrEmpty(segment)) return true;
  516. var lower = segment.ToLower();
  517. switch (lower)
  518. {
  519. case ".":
  520. case "..":
  521. // Synology
  522. case "@eadir":
  523. case "#recycle":
  524. // Windows
  525. case "$recycle.bin":
  526. case "recycler":
  527. case "system volume information":
  528. case "desktop.ini":
  529. case "thumbs.db":
  530. // macOS
  531. case ".ds_store":
  532. case ".localized":
  533. // IIS
  534. case "app_code":
  535. case "app_data":
  536. case "aspnet_client":
  537. case "bin":
  538. case "web.config":
  539. return true;
  540. }
  541. return false;
  542. }
  543. #endregion
  544. }
  545. }
  546. ;