150 lines
4.3 KiB
C#
150 lines
4.3 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 request.WriteTo(stream);
|
|||
|
|
|||
|
//Read response
|
|||
|
return await stream.ReadResponse();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/// <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 request.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;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|