OfflineWebServer.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. using Rac.Common;
  2. using Rac.Tools;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Mime;
  9. using System.Text;
  10. namespace Rac
  11. {
  12. public class OfflineWebServer : BaseService
  13. {
  14. private readonly string _dbFilename;
  15. private DataAccess _db;
  16. private HttpListener _server;
  17. private Uri _homeUrl;
  18. private HashSet<string> _hostInclude;
  19. private string[] _excludeUrlPrefixes;
  20. private Encoding _defaultEncoding;
  21. public OfflineWebServer(string dbFilename)
  22. {
  23. _dbFilename = dbFilename;
  24. }
  25. public override void Start()
  26. {
  27. if (false == File.Exists(_dbFilename))
  28. {
  29. LogFatal($"Can not find database file:{_dbFilename}");
  30. return;
  31. }
  32. _db = new DataAccess(_dbFilename);
  33. var conf = new ConfigAdapter(_db.GetConfigs());
  34. _defaultEncoding = null != conf.DefaultCharset ? Encoding.GetEncoding(conf.DefaultCharset) : Encoding.UTF8;
  35. _hostInclude = new HashSet<string>(conf.HostsInclude);
  36. _excludeUrlPrefixes = conf.UrlPrefixExclude;
  37. _homeUrl = new Uri(conf.HomeUrl);
  38. _server = new HttpListener();
  39. _server.Prefixes.Add("http://*:" + conf.OwsPort + "/");
  40. _server.Start();
  41. _server.BeginGetContext(ProcessRequest, null);
  42. LogInfo($"OWS runing on " + (string.Join("|", _server.Prefixes)));
  43. }
  44. private string UrlTrancode(Uri uri, string link = null, bool includedOnly = false)
  45. {
  46. if (true == link?.ToLower().StartsWith("mailto:")) return link;
  47. // http://host:port/path?query#hash
  48. // -->> /http/host/port/path?query#hash
  49. if (false == string.IsNullOrEmpty(link)) uri = new Uri(uri, link);
  50. if (includedOnly)
  51. {
  52. var url = uri.ToString();
  53. if (_excludeUrlPrefixes.Any(p => url.StartsWith(p))) return link;
  54. var hos = uri.Host;
  55. if (hos != _homeUrl.Host && false == _hostInclude.Contains(hos)) return link;
  56. }
  57. return $"/{uri.Scheme}/{uri.Host}/{uri.Port}{uri.PathAndQuery}{uri.Fragment}";
  58. }
  59. private void ProcessRequest(IAsyncResult ar)
  60. {
  61. var ctx = _server.EndGetContext(ar);
  62. LogTrace($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} From {ctx.Request.RemoteEndPoint}");
  63. //turn scheme/host/port to virtual path
  64. if (ctx.Request.Url.LocalPath == "/")
  65. {
  66. ctx.Response.Redirect(UrlTrancode(_homeUrl));
  67. }
  68. else
  69. {
  70. var path = ctx.Request.Url.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.SafeUnescaped);
  71. var parts = path.Split('/');
  72. //check request path format /scheme/host/port/...
  73. if (parts.Length < 4)
  74. {
  75. ctx.Response.StatusCode = 400;
  76. ctx.Response.StatusDescription = "Bad Archive Request";
  77. ctx.Response.ContentType = "text/html; charset=utf-8";
  78. var buffer = Encoding.UTF8.GetBytes("<h1>400 Bad Archive Request</h1>");
  79. ctx.Response.OutputStream.Write(buffer, 0, buffer.Length);
  80. }
  81. else
  82. {
  83. //decode to raw url pass to db
  84. try
  85. {
  86. var decodedUrl = $"{parts[1]}://{parts[2]}:{parts[3]}/{string.Join("/", parts.Skip(4))}";
  87. var decodedUri = new Uri(decodedUrl);
  88. var archiveUrl = decodedUri.ToString();
  89. var entry = _db.GetEntry(archiveUrl);
  90. if (entry == null || entry.StatusCode == 0)
  91. {
  92. ctx.Response.StatusCode = 404;
  93. ctx.Response.StatusDescription = "ArchiveEntryNotFound";
  94. ctx.Response.ContentType = "text/html; charset=utf-8";
  95. var buffer = Encoding.UTF8.GetBytes($"<h1>404 Not Found In Archive by {archiveUrl}</h1>");
  96. ctx.Response.OutputStream.Write(buffer, 0, buffer.Length);
  97. }
  98. else
  99. {
  100. var headers = HttpHeaderUtility.ParseStringLines(entry.Headers);
  101. ctx.Response.StatusCode = entry.StatusCode;
  102. ctx.Response.StatusDescription = entry.StatusDescription;
  103. string contentType = null;
  104. var contentEncoding = _defaultEncoding;
  105. // replace all urls to /scheme/host/port/path?query#hash
  106. // in header location
  107. // in HTML(href/src/embedded css)
  108. // in CSS (url)
  109. foreach (var header in headers)
  110. {
  111. if (header.Name == "location") header.Value = UrlTrancode(decodedUri, header.Value);
  112. if (header.Name == "content-type")
  113. {
  114. var ct = new ContentType(header.Value);
  115. contentType = ct.MediaType;
  116. if (null != ct.CharSet) contentEncoding = Encoding.GetEncoding(ct.CharSet);
  117. }
  118. ctx.Response.Headers.Set(header.Name, header.Value);
  119. }
  120. var output = entry.Content;
  121. if (contentType == "text/html")
  122. {
  123. var replaced = LinkProcessor.ReplaceHtmlLinks(entry.Content, p => UrlTrancode(decodedUri, p, true), ref contentEncoding);
  124. output = contentEncoding.GetBytes(replaced);
  125. }
  126. else if (contentType == "text/css")
  127. {
  128. var css = contentEncoding.GetString(entry.Content);
  129. var replaced = LinkProcessor.ReplaceCssLinks(css, p => UrlTrancode(decodedUri, p, true));
  130. output = contentEncoding.GetBytes(replaced);
  131. }
  132. try
  133. {
  134. ctx.Response.OutputStream.Write(output, 0, output.Length);
  135. }
  136. catch (Exception e)
  137. {
  138. LogError($"Error when writing output: {e.Message}");
  139. }
  140. }
  141. }
  142. catch (Exception e)
  143. {
  144. ctx.Response.StatusCode = 500;
  145. ctx.Response.StatusDescription = "ArchiveEntryNotFound";
  146. ctx.Response.ContentType = "text/html; charset=utf-8";
  147. var buffer = Encoding.UTF8.GetBytes($"<h1>Error</h1><pre>{e}</pre>");
  148. ctx.Response.OutputStream.Write(buffer, 0, buffer.Length);
  149. }
  150. }
  151. }
  152. try
  153. {
  154. LogTrace($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} -- {ctx.Response.StatusCode}");
  155. ctx.Response.Close();
  156. }
  157. catch
  158. {
  159. //Do nothing!
  160. }
  161. _server.BeginGetContext(ProcessRequest, null);
  162. }
  163. public override void Stop()
  164. {
  165. _server.Stop();
  166. }
  167. }
  168. }