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;
}
}
}
}
}