using System.Net.Security; using System.Net.Sockets; namespace Uwaa.HTTP; /// /// A hypertext transfer protocol client which can fetch content over HTTP, optionally over SSL (HTTPS). /// public sealed class HttpClient { /// /// Performs a single HTTP(S) request and closes. /// /// The hostname or IP to send the request to. /// If true, HTTPS will be used. /// The request to send. /// The response from the server. /// Thrown if the URI is invalid. public static Task Request(string host, bool secure, HttpRequest req) { return Request(host, secure ? 443 : 80, secure, req); } /// /// Performs a single HTTP(S) request and closes. /// /// The hostname or IP to send the request to. /// The port number to connect to. /// If true, HTTPS will be used. /// The request to send. /// The response from the server. public static async Task Request(string host, int port, bool secure, HttpRequest request) { using TcpClient client = new TcpClient(); await client.ConnectAsync(host, port); Stream innerStream = client.GetStream(); if (secure) { SslStream ssl = new SslStream(client.GetStream()); await ssl.AuthenticateAsClientAsync(host); innerStream = ssl; } HttpStream stream = new HttpStream(innerStream, TimeSpan.FromSeconds(60)); //Send request await FixRequest(request, host, ConnectionType.Close).WriteTo(stream); //Read response HttpResponse res = await stream.ReadResponse(); //Close client.Close(); return res; } static HttpRequest FixRequest(HttpRequest req, string host, ConnectionType conType) { if (req.Fields.Host == host && req.Fields.Connection == conType) return req; return req with { Fields = req.Fields with { Host = host, Connection = conType, } }; } /// /// The host to connect to. /// public readonly string Host; /// /// If true, attempt to connect via SSL. /// public readonly bool Secure; /// /// The maximum time the socket may be inactive before it is presumed dead and is closed. /// public TimeSpan Timeout = TimeSpan.FromSeconds(60); TcpClient? client; HttpStream? stream; readonly SemaphoreSlim semaphore; public HttpClient(string host, bool secure) { Host = host; Secure = secure; semaphore = new SemaphoreSlim(1, 1); } /// /// Queues sending a request to the host and returns the response. /// public async Task Fetch(HttpRequest request) { await semaphore.WaitAsync(); try { return await FetchInner(request).WaitAsync(Timeout); } finally { semaphore.Release(); } } async Task FetchInner(HttpRequest request) { bool retried = false; while (true) { if (client == null || !client.Connected || stream == null) { client?.Dispose(); //New connection client = new TcpClient(); await client.ConnectAsync(Host, 443); Stream innerStream = client.GetStream(); if (Secure) { SslStream ssl = new SslStream(client.GetStream()); await ssl.AuthenticateAsClientAsync(Host); innerStream = ssl; } stream = new HttpStream(innerStream, Timeout); } try { //Send request await FixRequest(request, Host, ConnectionType.KeepAlive).WriteTo(stream); //Read response return await stream.ReadResponse(); } catch (SocketException e) { if (e.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted) { if (retried) throw; //Connection down: Dispose stream and retry stream = null; retried = true; continue; } else { throw; } } } } }