169 lines
4.9 KiB
C#
169 lines
4.9 KiB
C#
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
|
|
namespace Uwaa.HTTP;
|
|
|
|
/// <summary>
|
|
/// A hypertext transfer protocol client which can fetch content over HTTP, optionally over SSL (HTTPS).
|
|
/// </summary>
|
|
public sealed class HttpClient
|
|
{
|
|
/// <summary>
|
|
/// Performs a single HTTP(S) request and closes.
|
|
/// </summary>
|
|
/// <param name="host">The hostname or IP to send the request to.</param>
|
|
/// <param name="secure">If true, HTTPS will be used.</param>
|
|
/// <param name="req">The request to send.</param>
|
|
/// <returns>The response from the server.</returns>
|
|
/// <exception cref="InvalidOperationException">Thrown if the URI is invalid.</exception>
|
|
public static Task<HttpResponse> Request(string host, bool secure, HttpRequest req)
|
|
{
|
|
return Request(host, secure ? 443 : 80, secure, req);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a single HTTP(S) request and closes.
|
|
/// </summary>
|
|
/// <param name="host">The hostname or IP to send the request to.</param>
|
|
/// <param name="port">The port number to connect to.</param>
|
|
/// <param name="secure">If true, HTTPS will be used.</param>
|
|
/// <param name="request">The request to send.</param>
|
|
/// <returns>The response from the server.</returns>
|
|
public static async Task<HttpResponse> 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);
|
|
|
|
//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,
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The host to connect to.
|
|
/// </summary>
|
|
public readonly string Host;
|
|
|
|
/// <summary>
|
|
/// If true, attempt to connect via SSL.
|
|
/// </summary>
|
|
public readonly bool Secure;
|
|
|
|
/// <summary>
|
|
/// The maximum time the socket may be inactive before it is presumed dead and is closed.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Queues sending a request to the host and returns the response.
|
|
/// </summary>
|
|
public async Task<HttpResponse> Fetch(HttpRequest request)
|
|
{
|
|
await semaphore.WaitAsync();
|
|
try
|
|
{
|
|
return await FetchInner(request).WaitAsync(Timeout);
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
}
|
|
|
|
async Task<HttpResponse> 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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|