-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathWebServer.cs
165 lines (149 loc) · 6.17 KB
/
WebServer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//-------------------
// Reachable Games
// Copyright 2023
//-------------------
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace ReachableGames
{
namespace RGWebSocket
{
// Use this to easily register endpoints for callbacks for normal HTTP requests, whereas all websocket upgrades will be handled by the IConnectionManager that is passed in.
public class WebServer
{
private readonly string _url;
private readonly string _urlPath; // if this server is hosted at http://some.com/foo/bar then this variable will contain foo/bar for easy removal
private readonly int _listenerThreads;
private readonly int _connectionTimeoutMS;
private readonly int _idleSeconds;
private readonly OnLogDelegate _logger;
private readonly IConnectionManager _connectionManager;
private CancellationTokenSource _cancellationTokenSrc = null; // this gets allocated and destroyed based on server status being listening or not.
//-------------------
public WebServer(string url, int listenerThreads, int connectionTimeoutMS, int idleSeconds, IConnectionManager connectionManager, OnLogDelegate logger)
{
_url = url;
_listenerThreads = listenerThreads;
_connectionTimeoutMS = connectionTimeoutMS;
_idleSeconds = idleSeconds;
_connectionManager = connectionManager;
_logger = logger;
string[] urlParts = url.Split('/'); // When you have a url, you have protocol://domain:port/path/part/etc
_urlPath = string.Join('/', urlParts, 3, urlParts.Length-3); // this leaves you with path/part/etc
}
//-------------------
public async Task Start()
{
if (_cancellationTokenSrc!=null)
throw new Exception("WebServer cannot be started multiple times without Shutdown being called.");
_cancellationTokenSrc = new CancellationTokenSource();
using (WebSocketServer httpServer = new WebSocketServer(_listenerThreads, _connectionTimeoutMS, _idleSeconds, _url, HttpRequestHandler, _connectionManager, _logger))
{
try
{
httpServer.StartListening(); // start listening AFTER we have registered the handlers
// Since the main program passed in the cancellation token, it literally controls the completion of this task,
// which only happens when told to shut down with ^C or SIGINT.
await _cancellationTokenSrc.Token; // magic!
}
catch (OperationCanceledException)
{
_logger(ELogVerboseType.Error, "Canceling WebServer.");
}
catch (Exception e)
{
if (e is HttpListenerException)
{
_logger(ELogVerboseType.Error, "If you get an Access Denied error, open an ADMIN command shell and run:");
_logger(ELogVerboseType.Error, $" netsh http add urlacl url={_url} user=\"{Environment.UserDomainName}\\{Environment.UserName}\"");
}
else
{
_logger(ELogVerboseType.Error, $"Exception: {e}");
}
}
finally
{
await httpServer.StopListening().ConfigureAwait(false); // kill all the connections and abort any that don't die quietly
_logger(ELogVerboseType.Warning, "WebServer has shutdown");
}
}
}
public void Shutdown()
{
if (_cancellationTokenSrc!=null)
{
_cancellationTokenSrc.Cancel();
_cancellationTokenSrc.Dispose();
_cancellationTokenSrc = null;
}
_logger(ELogVerboseType.Error, "WebServer shutdown requested");
}
//-------------------
// HTTP handlers
//-------------------
// This is the set of http endpoint handlers are kept. "/metrics" -> Metrics.HandleMetricsRequest, for example.
public delegate Task<(int, string, byte[])> HTTPRequestHandler(HttpListenerContext context); // handlers should return (httpStatus, contentType, content) so we can handle errors gracefully
private Dictionary<string, HTTPRequestHandler> _endpointHandlers = new Dictionary<string, HTTPRequestHandler>();
public void RegisterEndpoint(string urlPath, HTTPRequestHandler handler)
{
if (_endpointHandlers.TryAdd(urlPath, handler) == false)
{
_logger(ELogVerboseType.Error, $"RegisterEndpoint {urlPath} is already defined. Ignoring.");
}
}
public void UnregisterEndpoint(string urlPath)
{
if (_endpointHandlers.Remove(urlPath) == false)
{
_logger(ELogVerboseType.Error, $"UnregisterEndpoint {urlPath} not found to unregister.");
}
}
// Regular HTTP calls come here. They are dispatched to any registered endpoints.
private async Task HttpRequestHandler(HttpListenerContext httpContext)
{
int responseCode = 500;
string responseContentType = "text/plain";
byte[] responseContent = null;
string path = httpContext.Request.Url?.AbsolutePath ?? string.Empty;
string relativeEndpoint = string.IsNullOrEmpty(_urlPath) ? path : path.Replace(_urlPath, string.Empty);
if (_endpointHandlers.TryGetValue(relativeEndpoint, out HTTPRequestHandler handler))
{
try
{
(responseCode, responseContentType, responseContent) = await handler(httpContext).ConfigureAwait(false);
}
catch (Exception e)
{
responseCode = 500;
responseContentType = "text/plain";
responseContent = System.Text.Encoding.UTF8.GetBytes($"Exception {httpContext.Request.Url?.ToString() ?? string.Empty} {e}");
}
}
else
{
responseCode = 404;
responseContentType = "text/plain";
responseContent = System.Text.Encoding.UTF8.GetBytes($"No endpoint found for {httpContext.Request.Url?.ToString() ?? string.Empty}");
}
try
{
httpContext.Response.ContentType = responseContentType;
httpContext.Response.StatusCode = responseCode;
if (responseContent!=null)
{
httpContext.Response.ContentLength64 = responseContent.Length;
await httpContext.Response.OutputStream.WriteAsync(responseContent, 0, responseContent.Length).ConfigureAwait(false);
}
}
catch (Exception e)
{
_logger(ELogVerboseType.Error, $"Exception while trying to write to http response. {httpContext.Request.Url?.ToString() ?? string.Empty} {e}");
}
}
}
}
}