123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- using System;
- using UnityEngine;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Runtime.InteropServices;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Threading;
- using Debug = UnityEngine.Debug;
- namespace ZenFulcrum.EmbeddedBrowser {
- /// <summary>
- /// Acts like a webserver for local files in Assets/../BrowserAssets.
- /// To override this, extend the class and call `BrowserNative.webResources = myInstance`
- /// before doing anything with Browsers.
- ///
- /// Basic workflow:
- /// HandleRequest will get called when a browser needs something. From there you can either:
- /// - Call SendPreamble, then SendData (any number of times), then SendEnd or
- /// - Call one of the other Send* functions to send the whole response at once
- /// Response sending is asynchronous, so you can do the above immediately, or after a delay.
- ///
- /// Additionally, the Send* methods may be called from any thread given they are called in the right
- /// order and the right number of times.
- ///
- /// </summary>
- public abstract class WebResources {
- /// <summary>
- /// Mapping of file extension => HTTP mime type
- /// Treated as immutable.
- /// </summary>
- public static readonly Dictionary<string, string> extensionMimeTypes = new Dictionary<string, string>() {
- {"css", "text/css"},
- {"gif", "image/gif"},
- {"html", "text/html"},
- {"htm", "text/html"},
- {"jpeg", "image/jpeg"},
- {"jpg", "image/jpeg"},
- {"js", "application/javascript"},
- {"mp3", "audio/mpeg"},
- {"mpeg", "video/mpeg"},
- {"ogg", "application/ogg"},
- {"ogv", "video/ogg"},
- {"webm", "video/webm"},
- {"png", "image/png"},
- {"svg", "image/svg+xml"},
- {"txt", "text/plain"},
- {"xml", "application/xml"},
- //Need to add something? Look it up here: http://hul.harvard.edu/ois/systems/wax/wax-public-help/mimetypes.htm
- //Default/fallback
- {"*", "application/octet-stream"},
- };
- /// <summary>
- /// Mapping of status code to status text.
- /// Treated as immutable.
- /// </summary>
- public static readonly Dictionary<int, string> statusTexts = new Dictionary<int, string>() {
- // https://tools.ietf.org/html/rfc2616#section-10
- {100, "Continue"},
- {101, "Switching Protocols"},
- {200, "OK"},
- {201, "Created"},
- {202, "Accepted"},
- {203, "Non-Authoritative Information"},
- {204, "No Content"},
- {205, "Reset Content"},
- {206, "Partial Content"},
- {300, "Multiple Choices"},
- {301, "Moved Permanently"},
- {302, "Found"},
- {303, "See Other"},
- {304, "Not Modified"},
- {305, "Use Proxy"},
- {307, "Temporary Redirect"},
- {400, "Bad Request"},
- {401, "Unauthorized"},
- {402, "Payment Required"},
- {403, "Forbidden"},
- {404, "Not Found"},
- {405, "Method Not Allowed"},
- {406, "Not Acceptable"},
- {407, "Proxy Authentication Required"},
- {408, "Request Timeout"},
- {409, "Conflict"},
- {410, "Gone"},
- {411, "Length Required"},
- {412, "Precondition Failed"},
- {413, "Request Entity Too Large"},
- {414, "Request-URI Too Long"},
- {415, "Unsupported Media Type"},
- {416, "Requested Range Not Satisfiable"},
- {417, "Expectation Failed"},
- {500, "Internal Server Error"},
- {501, "Not Implemented"},
- {502, "Bad Gateway"},
- {503, "Service Unavailable"},
- {504, "Gateway Timeout"},
- {505, "HTTP Version Not Supported"},
- //Default/fallback
- {-1, ""},
- };
- public class ResponsePreamble {
- /// <summary>
- /// HTTP Status code (e.g. 200 for ok, 404 for not found)
- /// </summary>
- public int statusCode = 200;
- /// <summary>
- /// HTTP Status text ("OK", "Not Found", etc.)
- /// </summary>
- public string statusText = null;
- /// <summary>
- /// Content mime-type.
- /// </summary>
- public string mimeType = "text/plain; charset=UTF-8";
- /// <summary>
- /// Number of bytes in the response. -1 if unknown.
- /// If set >= 0, the number of bytes in the result need to match.
- /// </summary>
- public int length = -1;
- /// <summary>
- /// Any additional headers you'd like to send with the request
- /// </summary>
- public Dictionary<string, string> headers = new Dictionary<string, string>();
- }
- /// <summary>
- /// Called when a resource is requested. (Only GET requests are supported at present.)
- /// After this is called, eventually call one or more of the Send* functions with the given id
- /// to send the response (see class docs).
- /// </summary>
- /// <param name="id"></param>
- /// <param name="url"></param>
- public abstract void HandleRequest(int id, string url);
- /// <summary>
- /// Sends the full binary response to a request.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="data"></param>
- /// <param name="mimeType"></param>
- protected virtual void SendResponse(int id, byte[] data, string mimeType = "application/octet-stream") {
- var pre = new ResponsePreamble {
- headers = null,
- length = data.Length,
- mimeType = mimeType,
- statusCode = 200,
- };
- SendPreamble(id, pre);
- SendData(id, data);
- SendEnd(id);
- }
- /// <summary>
- /// Sends the full HTML or text response to a request.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="text"></param>
- /// <param name="mimeType"></param>
- protected virtual void SendResponse(int id, string text, string mimeType = "text/html; charset=UTF-8") {
- var data = Encoding.UTF8.GetBytes(text);
- var pre = new ResponsePreamble {
- headers = null,
- length = data.Length,
- mimeType = mimeType,
- statusCode = 200,
- };
- SendPreamble(id, pre);
- SendData(id, data);
- SendEnd(id);
- }
- /// <summary>
- /// Sends an HTML formatted error message.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="html"></param>
- /// <param name="errorCode"></param>
- protected virtual void SendError(int id, string html, int errorCode = 500) {
- var data = Encoding.UTF8.GetBytes(html);
- var pre = new ResponsePreamble {
- headers = null,
- length = data.Length,
- mimeType = "text/html; charset=UTF-8",
- statusCode = errorCode,
- };
- SendPreamble(id, pre);
- SendData(id, data);
- SendEnd(id);
- }
- protected virtual void SendFile(int id, FileInfo file, bool forceDownload = false) {
- new Thread(() => {
- try {
- if (!file.Exists) {
- SendError(id, "<h2>File not found</h2>", 404);
- return;
- }
- FileStream fileStream = null;
- try {
- fileStream = file.OpenRead();
- } catch (Exception ex) {
- Debug.LogException(ex);
- SendError(id, "<h2>File unavailable</h2>", 500);
- return;
- }
- string mimeType;
- var ext = file.Extension;
- if (ext.Length > 0) ext = ext.Substring(1).ToLowerInvariant();
- if (!extensionMimeTypes.TryGetValue(ext, out mimeType)) {
- mimeType = extensionMimeTypes["*"];
- }
- //Debug.Log("response type: " + mimeType + " extension " + file.Extension);
- var pre = new ResponsePreamble {
- headers = new Dictionary<string, string>(),
- length = (int)file.Length,
- mimeType = mimeType,
- statusCode = 200,
- };
- if (forceDownload) {
- pre.headers["Content-Disposition"] = "attachment; filename=\"" + file.Name.Replace("\"", "\\\"") + "\"";
- }
- SendPreamble(id, pre);
- int readCount = -1;
- byte[] buffer = new byte[Math.Min(pre.length, 32 * 1024)];
- while (readCount != 0) {
- readCount = fileStream.Read(buffer, 0, buffer.Length);
- SendData(id, buffer, readCount);
- }
- SendEnd(id);
- fileStream.Close();
- } catch (Exception ex) {
- Debug.LogException(ex);
- }
- }).Start();
- }
- /// <summary>
- /// Sends headers, status code, content-type, etc. for a request.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="pre"></param>
- protected void SendPreamble(int id, ResponsePreamble pre) {
- var headers = new JSONNode(JSONNode.NodeType.Object);
- if (pre.headers != null) {
- foreach (var kvp in pre.headers) {
- headers[kvp.Key] = kvp.Value;
- }
- }
- if (pre.statusText == null) {
- if (!statusTexts.TryGetValue(pre.statusCode, out pre.statusText)) {
- pre.statusText = statusTexts[-1];
- }
- }
- headers[":status:"] = pre.statusCode.ToString();
- headers[":statusText:"] = pre.statusText;
- headers["Content-Type"] = pre.mimeType;
- //Debug.Log("response headers " + headers.AsJSON);
- lock (BrowserNative.symbolsLock) {
- BrowserNative.zfb_sendRequestHeaders(id, pre.length, headers.AsJSON);
- }
- }
- /// <summary>
- /// Sends response body for the request.
- /// Call as many times as you'd like.
- /// If you specified a length in the preamble make sure all writes add up to exactly that number of bytes.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="data"></param>
- /// <param name="length">How much of data to write, or -1 to send it all</param>
- protected void SendData(int id, byte[] data, int length = -1) {
- if (data == null || data.Length == 0 || length == 0) return;
- if (length < 0) length = data.Length;
- if (length > data.Length) throw new IndexOutOfRangeException();
- var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
- lock (BrowserNative.symbolsLock) {
- BrowserNative.zfb_sendRequestData(id, handle.AddrOfPinnedObject(), length);
- }
- handle.Free();
- }
- /// <summary>
- /// Call this after you are done calling SendData and you are ready to complete the response.
- /// </summary>
- /// <param name="id"></param>
- protected void SendEnd(int id) {
- lock (BrowserNative.symbolsLock) {
- BrowserNative.zfb_sendRequestData(id, IntPtr.Zero, 0);
- }
- }
- }
- }
|