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; /// /// A hypertext transfer protocol server which can listen on a port and deliver content over HTTP, optionally over SSL (HTTPS). /// public sealed class HttpServer { /// /// The router to be used to find and serve content to clients. /// public readonly RouterBase Router; /// /// If non-null, all connections will be secured via SSL using this certificate. /// public readonly X509Certificate? Certificate; /// /// Maximum number of connections per IP before new connections are rejected. /// /// /// Prevents Slow Loris attacks. /// public int MaxConnections = 20; /// /// The port the server will listen on. /// public int Port => ((IPEndPoint)listener.LocalEndpoint).Port; /// /// Called when a client establishes a connection with the server. /// public event Action? OnConnectionBegin; /// /// Called when a connection has terminated. /// public event Action? OnConnectionEnd; /// /// Called when a request has been served a response. /// public event Action? OnResponse; /// /// Called when a request causes an exception. /// public event Action? OnException; /// /// The maximum time a socket may be inactive before it is presumed dead and closed. /// public TimeSpan Timeout = TimeSpan.FromSeconds(20); readonly Dictionary IPCounts = new Dictionary(); 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; } /// /// Begins listening for new connections. /// 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, Timeout); try { HttpClientInfo clientInfo = new HttpClientInfo(client, endpoint); HttpRequest req = await httpStream.ReadRequest(); //Parse path ArraySegment 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, clientInfo, response); await response.WriteTo(httpStream); 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); await ws.Close(closeStatus); break; //Close } if (req.Fields.Connection == ConnectionType.Close) break; //Close } catch (HttpException e) { try { await new HttpResponse(400, e.Message).WriteTo(httpStream).WaitAsync(Timeout); } catch { } throw; } catch (TimeoutException) { //Timeout throw; } catch (IOException) { //Disconnect throw; } catch (SocketException) { //Disconnect throw; } catch (Exception) { await new HttpResponse(500, "Internal server error").WriteTo(httpStream).WaitAsync(Timeout); throw; } } } catch (Exception e) { //Swallow exceptions to prevent the server from crashing. //When debugging, use a debugger to break on exceptions. OnException?.Invoke(endpoint, e); } 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); } } }