WebResources.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. using System;
  2. using UnityEngine;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Runtime.InteropServices;
  7. using System.Text;
  8. using System.Text.RegularExpressions;
  9. using System.Threading;
  10. using Debug = UnityEngine.Debug;
  11. namespace ZenFulcrum.EmbeddedBrowser {
  12. /// <summary>
  13. /// Acts like a webserver for local files in Assets/../BrowserAssets.
  14. /// To override this, extend the class and call `BrowserNative.webResources = myInstance`
  15. /// before doing anything with Browsers.
  16. ///
  17. /// Basic workflow:
  18. /// HandleRequest will get called when a browser needs something. From there you can either:
  19. /// - Call SendPreamble, then SendData (any number of times), then SendEnd or
  20. /// - Call one of the other Send* functions to send the whole response at once
  21. /// Response sending is asynchronous, so you can do the above immediately, or after a delay.
  22. ///
  23. /// Additionally, the Send* methods may be called from any thread given they are called in the right
  24. /// order and the right number of times.
  25. ///
  26. /// </summary>
  27. public abstract class WebResources {
  28. /// <summary>
  29. /// Mapping of file extension => HTTP mime type
  30. /// Treated as immutable.
  31. /// </summary>
  32. public static readonly Dictionary<string, string> extensionMimeTypes = new Dictionary<string, string>() {
  33. {"css", "text/css"},
  34. {"gif", "image/gif"},
  35. {"html", "text/html"},
  36. {"htm", "text/html"},
  37. {"jpeg", "image/jpeg"},
  38. {"jpg", "image/jpeg"},
  39. {"js", "application/javascript"},
  40. {"mp3", "audio/mpeg"},
  41. {"mpeg", "video/mpeg"},
  42. {"ogg", "application/ogg"},
  43. {"ogv", "video/ogg"},
  44. {"webm", "video/webm"},
  45. {"png", "image/png"},
  46. {"svg", "image/svg+xml"},
  47. {"txt", "text/plain"},
  48. {"xml", "application/xml"},
  49. //Need to add something? Look it up here: http://hul.harvard.edu/ois/systems/wax/wax-public-help/mimetypes.htm
  50. //Default/fallback
  51. {"*", "application/octet-stream"},
  52. };
  53. /// <summary>
  54. /// Mapping of status code to status text.
  55. /// Treated as immutable.
  56. /// </summary>
  57. public static readonly Dictionary<int, string> statusTexts = new Dictionary<int, string>() {
  58. // https://tools.ietf.org/html/rfc2616#section-10
  59. {100, "Continue"},
  60. {101, "Switching Protocols"},
  61. {200, "OK"},
  62. {201, "Created"},
  63. {202, "Accepted"},
  64. {203, "Non-Authoritative Information"},
  65. {204, "No Content"},
  66. {205, "Reset Content"},
  67. {206, "Partial Content"},
  68. {300, "Multiple Choices"},
  69. {301, "Moved Permanently"},
  70. {302, "Found"},
  71. {303, "See Other"},
  72. {304, "Not Modified"},
  73. {305, "Use Proxy"},
  74. {307, "Temporary Redirect"},
  75. {400, "Bad Request"},
  76. {401, "Unauthorized"},
  77. {402, "Payment Required"},
  78. {403, "Forbidden"},
  79. {404, "Not Found"},
  80. {405, "Method Not Allowed"},
  81. {406, "Not Acceptable"},
  82. {407, "Proxy Authentication Required"},
  83. {408, "Request Timeout"},
  84. {409, "Conflict"},
  85. {410, "Gone"},
  86. {411, "Length Required"},
  87. {412, "Precondition Failed"},
  88. {413, "Request Entity Too Large"},
  89. {414, "Request-URI Too Long"},
  90. {415, "Unsupported Media Type"},
  91. {416, "Requested Range Not Satisfiable"},
  92. {417, "Expectation Failed"},
  93. {500, "Internal Server Error"},
  94. {501, "Not Implemented"},
  95. {502, "Bad Gateway"},
  96. {503, "Service Unavailable"},
  97. {504, "Gateway Timeout"},
  98. {505, "HTTP Version Not Supported"},
  99. //Default/fallback
  100. {-1, ""},
  101. };
  102. public class ResponsePreamble {
  103. /// <summary>
  104. /// HTTP Status code (e.g. 200 for ok, 404 for not found)
  105. /// </summary>
  106. public int statusCode = 200;
  107. /// <summary>
  108. /// HTTP Status text ("OK", "Not Found", etc.)
  109. /// </summary>
  110. public string statusText = null;
  111. /// <summary>
  112. /// Content mime-type.
  113. /// </summary>
  114. public string mimeType = "text/plain; charset=UTF-8";
  115. /// <summary>
  116. /// Number of bytes in the response. -1 if unknown.
  117. /// If set >= 0, the number of bytes in the result need to match.
  118. /// </summary>
  119. public int length = -1;
  120. /// <summary>
  121. /// Any additional headers you'd like to send with the request
  122. /// </summary>
  123. public Dictionary<string, string> headers = new Dictionary<string, string>();
  124. }
  125. /// <summary>
  126. /// Called when a resource is requested. (Only GET requests are supported at present.)
  127. /// After this is called, eventually call one or more of the Send* functions with the given id
  128. /// to send the response (see class docs).
  129. /// </summary>
  130. /// <param name="id"></param>
  131. /// <param name="url"></param>
  132. public abstract void HandleRequest(int id, string url);
  133. /// <summary>
  134. /// Sends the full binary response to a request.
  135. /// </summary>
  136. /// <param name="id"></param>
  137. /// <param name="data"></param>
  138. /// <param name="mimeType"></param>
  139. protected virtual void SendResponse(int id, byte[] data, string mimeType = "application/octet-stream") {
  140. var pre = new ResponsePreamble {
  141. headers = null,
  142. length = data.Length,
  143. mimeType = mimeType,
  144. statusCode = 200,
  145. };
  146. SendPreamble(id, pre);
  147. SendData(id, data);
  148. SendEnd(id);
  149. }
  150. /// <summary>
  151. /// Sends the full HTML or text response to a request.
  152. /// </summary>
  153. /// <param name="id"></param>
  154. /// <param name="text"></param>
  155. /// <param name="mimeType"></param>
  156. protected virtual void SendResponse(int id, string text, string mimeType = "text/html; charset=UTF-8") {
  157. var data = Encoding.UTF8.GetBytes(text);
  158. var pre = new ResponsePreamble {
  159. headers = null,
  160. length = data.Length,
  161. mimeType = mimeType,
  162. statusCode = 200,
  163. };
  164. SendPreamble(id, pre);
  165. SendData(id, data);
  166. SendEnd(id);
  167. }
  168. /// <summary>
  169. /// Sends an HTML formatted error message.
  170. /// </summary>
  171. /// <param name="id"></param>
  172. /// <param name="html"></param>
  173. /// <param name="errorCode"></param>
  174. protected virtual void SendError(int id, string html, int errorCode = 500) {
  175. var data = Encoding.UTF8.GetBytes(html);
  176. var pre = new ResponsePreamble {
  177. headers = null,
  178. length = data.Length,
  179. mimeType = "text/html; charset=UTF-8",
  180. statusCode = errorCode,
  181. };
  182. SendPreamble(id, pre);
  183. SendData(id, data);
  184. SendEnd(id);
  185. }
  186. protected virtual void SendFile(int id, FileInfo file, bool forceDownload = false) {
  187. new Thread(() => {
  188. try {
  189. if (!file.Exists) {
  190. SendError(id, "<h2>File not found</h2>", 404);
  191. return;
  192. }
  193. FileStream fileStream = null;
  194. try {
  195. fileStream = file.OpenRead();
  196. } catch (Exception ex) {
  197. Debug.LogException(ex);
  198. SendError(id, "<h2>File unavailable</h2>", 500);
  199. return;
  200. }
  201. string mimeType;
  202. var ext = file.Extension;
  203. if (ext.Length > 0) ext = ext.Substring(1).ToLowerInvariant();
  204. if (!extensionMimeTypes.TryGetValue(ext, out mimeType)) {
  205. mimeType = extensionMimeTypes["*"];
  206. }
  207. //Debug.Log("response type: " + mimeType + " extension " + file.Extension);
  208. var pre = new ResponsePreamble {
  209. headers = new Dictionary<string, string>(),
  210. length = (int)file.Length,
  211. mimeType = mimeType,
  212. statusCode = 200,
  213. };
  214. if (forceDownload) {
  215. pre.headers["Content-Disposition"] = "attachment; filename=\"" + file.Name.Replace("\"", "\\\"") + "\"";
  216. }
  217. SendPreamble(id, pre);
  218. int readCount = -1;
  219. byte[] buffer = new byte[Math.Min(pre.length, 32 * 1024)];
  220. while (readCount != 0) {
  221. readCount = fileStream.Read(buffer, 0, buffer.Length);
  222. SendData(id, buffer, readCount);
  223. }
  224. SendEnd(id);
  225. fileStream.Close();
  226. } catch (Exception ex) {
  227. Debug.LogException(ex);
  228. }
  229. }).Start();
  230. }
  231. /// <summary>
  232. /// Sends headers, status code, content-type, etc. for a request.
  233. /// </summary>
  234. /// <param name="id"></param>
  235. /// <param name="pre"></param>
  236. protected void SendPreamble(int id, ResponsePreamble pre) {
  237. var headers = new JSONNode(JSONNode.NodeType.Object);
  238. if (pre.headers != null) {
  239. foreach (var kvp in pre.headers) {
  240. headers[kvp.Key] = kvp.Value;
  241. }
  242. }
  243. if (pre.statusText == null) {
  244. if (!statusTexts.TryGetValue(pre.statusCode, out pre.statusText)) {
  245. pre.statusText = statusTexts[-1];
  246. }
  247. }
  248. headers[":status:"] = pre.statusCode.ToString();
  249. headers[":statusText:"] = pre.statusText;
  250. headers["Content-Type"] = pre.mimeType;
  251. //Debug.Log("response headers " + headers.AsJSON);
  252. lock (BrowserNative.symbolsLock) {
  253. BrowserNative.zfb_sendRequestHeaders(id, pre.length, headers.AsJSON);
  254. }
  255. }
  256. /// <summary>
  257. /// Sends response body for the request.
  258. /// Call as many times as you'd like.
  259. /// If you specified a length in the preamble make sure all writes add up to exactly that number of bytes.
  260. /// </summary>
  261. /// <param name="id"></param>
  262. /// <param name="data"></param>
  263. /// <param name="length">How much of data to write, or -1 to send it all</param>
  264. protected void SendData(int id, byte[] data, int length = -1) {
  265. if (data == null || data.Length == 0 || length == 0) return;
  266. if (length < 0) length = data.Length;
  267. if (length > data.Length) throw new IndexOutOfRangeException();
  268. var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
  269. lock (BrowserNative.symbolsLock) {
  270. BrowserNative.zfb_sendRequestData(id, handle.AddrOfPinnedObject(), length);
  271. }
  272. handle.Free();
  273. }
  274. /// <summary>
  275. /// Call this after you are done calling SendData and you are ready to complete the response.
  276. /// </summary>
  277. /// <param name="id"></param>
  278. protected void SendEnd(int id) {
  279. lock (BrowserNative.symbolsLock) {
  280. BrowserNative.zfb_sendRequestData(id, IntPtr.Zero, 0);
  281. }
  282. }
  283. }
  284. }