Skip to content

Instantly share code, notes, and snippets.

@to-osaki
Created April 9, 2023 22:42
Show Gist options
  • Save to-osaki/815ca55b85bd644185d71a16aa0eb62a to your computer and use it in GitHub Desktop.
Save to-osaki/815ca55b85bd644185d71a16aa0eb62a to your computer and use it in GitHub Desktop.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using UnityEngine;
using ResponseCallback = System.Func<System.Net.HttpListenerRequest, System.Text.RegularExpressions.Match, ResponseValue>;
public class ResponseValue
{
static public readonly ResponseValue Empty = new ResponseValue
{
statusCode = (int)HttpStatusCode.OK,
contentType = "text/plain",
output = null,
};
static public readonly ResponseValue NotFound = new ResponseValue
{
statusCode = (int)HttpStatusCode.NotFound,
contentType = "text/plain",
output = null,
};
static public ResponseValue ResponseText(string text)
{
return new ResponseValue
{
statusCode = (int)HttpStatusCode.OK,
contentType = "text/plain",
output = System.Text.Encoding.UTF8.GetBytes(text),
};
}
static public ResponseValue ResponseJson(string json)
{
return new ResponseValue
{
statusCode = (int)HttpStatusCode.OK,
contentType = "application/json",
output = System.Text.Encoding.UTF8.GetBytes(json),
};
}
static public ResponseValue ResponseJson(System.Object obj)
{
string json = JsonUtility.ToJson(obj);
return ResponseJson(json);
}
static public ResponseValue ResponseJpeg(byte[] jpg)
{
return new ResponseValue
{
statusCode = (int)HttpStatusCode.OK,
contentType = "image/jpeg",
output = jpg,
};
}
public int statusCode;
public string contentType;
public byte[] output;
}
public class HttpServer : IDisposable
{
private readonly HttpListener _listener;
private readonly Dictionary<HttpMethod, List<(Regex, ResponseCallback)>> _routes;
private Queue<HttpListenerContext> _contexts = new Queue<HttpListenerContext>(16);
public int ArrivedRequestCount => _contexts.Count;
public HttpServer(int port)
{
_listener = new HttpListener();
_listener.Prefixes.Add($"http://+:{port}/");
_listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
_listener.Start();
_listener.BeginGetContext(ListenerCallback, _listener);
_routes = new Dictionary<HttpMethod, List<(Regex, ResponseCallback)>>
{
{HttpMethod.Post, new List<(Regex, ResponseCallback)>()},
{HttpMethod.Get, new List<(Regex, ResponseCallback)>()},
{HttpMethod.Put, new List<(Regex, ResponseCallback)>()},
{HttpMethod.Delete, new List<(Regex, ResponseCallback)>()},
};
}
public void Update()
{
lock (_contexts)
{
while (_contexts.Count > 0)
{
Response(_contexts.Dequeue());
}
}
}
public void Dispose()
{
lock (_contexts)
{
while (_contexts.Count > 0)
{
var context = _contexts.Dequeue();
context.Response.Abort();
}
}
if (_listener.IsListening)
{
_listener.Stop();
}
}
public void Route(HttpMethod method, Regex pattern, ResponseCallback callback)
{
if (_routes.TryGetValue(method, out var table))
{
table.Add((pattern, callback));
}
}
public void Route(Regex pattern, ResponseCallback callback)
{
Route(HttpMethod.Get, pattern, callback);
}
public ResponseValue InvokeRequest(HttpMethod method, HttpListenerRequest request)
{
if (_routes.TryGetValue(method, out var list))
{
for (int i = 0; i < list.Count; i++)
{
(Regex pattern, ResponseCallback callback) = list[i];
var match = pattern.Match(request.Url.LocalPath);
if (match.Success)
{
return callback?.Invoke(request, match);
}
}
}
return ResponseValue.NotFound;
}
private void ListenerCallback(IAsyncResult result)
{
var context = _listener.EndGetContext(result);
if (!_listener.IsListening) { context.Response.Abort(); return; }
_listener.BeginGetContext(ListenerCallback, _listener);
lock (_contexts)
{
_contexts.Enqueue(context);
}
}
private void Response(HttpListenerContext context)
{
var request = context.Request;
var response = context.Response;
try
{
var value = InvokeRequest(new HttpMethod(request.HttpMethod), request);
if (value != null)
{
response.ContentType = value.contentType;
response.StatusCode = value.statusCode;
response.AppendHeader("Cache-Control", "no-cache");
response.AppendHeader("Access-Control-Allow-Origin", "*");
if (value.output != null)
{
var bytes = value.output;
response.Close(bytes, false);
}
else
{
response.Close();
}
}
else
{
response.Abort();
}
}
catch (Exception e)
{
ResponseInternalError(response, e);
}
}
private void ResponseInternalError(HttpListenerResponse response, Exception e)
{
response.StatusCode = (int)HttpStatusCode.InternalServerError;
response.ContentType = "text/plain";
try
{
using (var writer = new System.IO.StreamWriter(response.OutputStream, System.Text.Encoding.UTF8))
writer.Write(e.ToString());
response.Close();
}
catch
{
response.Abort();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment