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 { /// /// 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. /// /// public abstract class WebResources { /// /// Mapping of file extension => HTTP mime type /// Treated as immutable. /// public static readonly Dictionary extensionMimeTypes = new Dictionary() { {"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? Some resources: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types // http://www.iana.org/assignments/media-types/media-types.xhtml //Default/fallback {"*", "application/octet-stream"}, }; /// /// Mapping of status code to status text. /// Treated as immutable. /// public static readonly Dictionary statusTexts = new Dictionary() { // 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 { /// /// HTTP Status code (e.g. 200 for ok, 404 for not found) /// public int statusCode = 200; /// /// HTTP Status text ("OK", "Not Found", etc.) /// public string statusText = null; /// /// Content mime-type. /// public string mimeType = "text/plain; charset=UTF-8"; /// /// Number of bytes in the response. -1 if unknown. /// If set >= 0, the number of bytes in the result need to match. /// public int length = -1; /// /// Any additional headers you'd like to send with the request /// public Dictionary headers = new Dictionary(); } /// /// 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). /// /// /// public abstract void HandleRequest(int id, string url); /// /// Sends the full binary response to a request. /// /// /// /// 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); } /// /// Sends the full HTML or text response to a request. /// /// /// /// 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); } /// /// Sends an HTML formatted error message. /// /// /// /// 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, "

File not found

", 404); return; } FileStream fileStream = null; try { fileStream = file.OpenRead(); } catch (Exception ex) { Debug.LogException(ex); SendError(id, "

File unavailable

", 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(), 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(); } /// /// Sends headers, status code, content-type, etc. for a request. /// /// /// 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); } } /// /// 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. /// /// /// /// How much of data to write, or -1 to send it all 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(); } /// /// Call this after you are done calling SendData and you are ready to complete the response. /// /// protected void SendEnd(int id) { lock (BrowserNative.symbolsLock) { BrowserNative.zfb_sendRequestData(id, IntPtr.Zero, 0); } } } }