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.
320 lines
9.2 KiB
320 lines
9.2 KiB
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? 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"}, |
|
}; |
|
|
|
/// <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); |
|
} |
|
} |
|
|
|
} |
|
|
|
}
|
|
|