234 lines
7.3 KiB
C#
234 lines
7.3 KiB
C#
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Uwaa.HTTP.Routing;
|
|
using Uwaa.HTTP.Websockets;
|
|
|
|
namespace Uwaa.HTTP;
|
|
|
|
/// <summary>
|
|
/// A hypertext transfer protocol server which can listen on a port and deliver content over HTTP, optionally over SSL (HTTPS).
|
|
/// </summary>
|
|
public sealed class HttpServer
|
|
{
|
|
/// <summary>
|
|
/// The router to be used to find and serve content to clients.
|
|
/// </summary>
|
|
public readonly RouterBase Router;
|
|
|
|
/// <summary>
|
|
/// If non-null, all connections will be secured via SSL using this certificate.
|
|
/// </summary>
|
|
public readonly X509Certificate? Certificate;
|
|
|
|
/// <summary>
|
|
/// Maximum number of connections per IP before new connections are rejected.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Prevents Slow Loris attacks.
|
|
/// </remarks>
|
|
public int MaxConnections = 20;
|
|
|
|
/// <summary>
|
|
/// The port the server will listen on.
|
|
/// </summary>
|
|
public int Port => ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
|
|
/// <summary>
|
|
/// Called when a client establishes a connection with the server.
|
|
/// </summary>
|
|
public event Action<TcpClient>? OnConnectionBegin;
|
|
|
|
/// <summary>
|
|
/// Called when a connection has terminated.
|
|
/// </summary>
|
|
public event Action<IPEndPoint>? OnConnectionEnd;
|
|
|
|
/// <summary>
|
|
/// Called when a request has been served a response.
|
|
/// </summary>
|
|
public event Action<HttpRequest, HttpResponse>? OnResponse;
|
|
|
|
/// <summary>
|
|
/// The maximum time the socket may be inactive before it is presumed dead and closed.
|
|
/// </summary>
|
|
public TimeSpan Timeout = TimeSpan.FromSeconds(20);
|
|
|
|
readonly Dictionary<IPAddress, int> IPCounts = new Dictionary<IPAddress, int>();
|
|
|
|
readonly SemaphoreSlim IPCountsLock = new SemaphoreSlim(1, 1);
|
|
|
|
readonly TcpListener listener;
|
|
|
|
public HttpServer(int port, X509Certificate? certificate, RouterBase router)
|
|
{
|
|
if (OperatingSystem.IsWindows() && certificate is X509Certificate2 cert)
|
|
{
|
|
//Hack because SslStream is stupid on windows
|
|
certificate = new X509Certificate2(cert.Export(X509ContentType.Pfx));
|
|
}
|
|
|
|
listener = new TcpListener(IPAddress.Any, port);
|
|
Certificate = certificate;
|
|
Router = router;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins listening for new connections.
|
|
/// </summary>
|
|
public async void Start()
|
|
{
|
|
listener.Start();
|
|
|
|
while (true)
|
|
HandleClient(await listener.AcceptTcpClientAsync());
|
|
}
|
|
|
|
async void HandleClient(TcpClient client)
|
|
{
|
|
OnConnectionBegin?.Invoke(client);
|
|
|
|
if (client.Client.RemoteEndPoint is not IPEndPoint endpoint)
|
|
return;
|
|
|
|
//Count IPs to prevent Slow Loris attacks
|
|
try
|
|
{
|
|
await IPCountsLock.WaitAsync();
|
|
if (IPCounts.TryGetValue(endpoint.Address, out int count))
|
|
{
|
|
if (count >= MaxConnections)
|
|
{
|
|
//Too many connections
|
|
client.Close();
|
|
return;
|
|
}
|
|
|
|
IPCounts[endpoint.Address] = count + 1;
|
|
}
|
|
else
|
|
{
|
|
IPCounts[endpoint.Address] = 1;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
IPCountsLock.Release();
|
|
}
|
|
|
|
try
|
|
{
|
|
//Setup client
|
|
Stream stream = client.GetStream();
|
|
client.LingerState = new LingerOption(true, 5);
|
|
|
|
if (Certificate != null)
|
|
{
|
|
//Pass through SSL stream
|
|
SslStream ssl = new SslStream(stream);
|
|
await ssl.AuthenticateAsServerAsync(Certificate).WaitAsync(Timeout);
|
|
stream = ssl;
|
|
}
|
|
|
|
//HTTP request-response loop
|
|
while (client.Connected)
|
|
{
|
|
HttpStream httpStream = new HttpStream(stream);
|
|
try
|
|
{
|
|
HttpClientInfo clientInfo = new HttpClientInfo(client, endpoint);
|
|
HttpRequest req = await httpStream.ReadRequest().WaitAsync(Timeout);
|
|
|
|
//Parse path
|
|
ArraySegment<string> pathSpl = req.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
if (pathSpl.Count == 0)
|
|
{
|
|
pathSpl = new string[] { string.Empty };
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < pathSpl.Count; i++)
|
|
pathSpl[i] = WebUtility.UrlDecode(pathSpl[i]);
|
|
}
|
|
|
|
//Execute
|
|
HttpResponse? response = (await Router.GetResponse(req, clientInfo, pathSpl)) ?? HttpResponse.NotFound("Router produced no response");
|
|
|
|
OnResponse?.Invoke(req, response);
|
|
await response.WriteTo(httpStream).WaitAsync(Timeout);
|
|
|
|
if (response is SwitchingProtocols swp)
|
|
{
|
|
//Create and run websocket
|
|
WebsocketRemote ws = new WebsocketRemote(req, clientInfo, httpStream, swp.Fields.WebSocketProtocol);
|
|
CloseStatus closeStatus = await swp.Callback(ws);
|
|
ws.Close(closeStatus);
|
|
break; //Close
|
|
}
|
|
else
|
|
{
|
|
if (response.StatusCode is not >= 200 or not < 300 || (req.Fields.Connection == ConnectionType.Close))
|
|
break; //Close
|
|
}
|
|
}
|
|
catch (HttpException e)
|
|
{
|
|
try
|
|
{
|
|
await new HttpResponse(400, e.Message).WriteTo(httpStream).WaitAsync(Timeout);
|
|
}
|
|
catch { }
|
|
break;
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
//Timeout
|
|
break;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
//Disconnect
|
|
throw;
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
//Disconnect
|
|
throw;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
await new HttpResponse(500, "Internal server error").WriteTo(httpStream).WaitAsync(Timeout);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
//Swallow exceptions to prevent the server from crashing.
|
|
//When debugging, use a debugger to break on exceptions.
|
|
}
|
|
finally
|
|
{
|
|
client.Close();
|
|
try
|
|
{
|
|
await IPCountsLock.WaitAsync();
|
|
if (IPCounts[endpoint.Address] <= 1)
|
|
{
|
|
IPCounts.Remove(endpoint.Address);
|
|
}
|
|
else
|
|
{
|
|
IPCounts[endpoint.Address]--;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
IPCountsLock.Release();
|
|
}
|
|
|
|
OnConnectionEnd?.Invoke(endpoint);
|
|
}
|
|
}
|
|
}
|