Skip to content

Commit

Permalink
Refactored RuriLib.Proxies and RuriLib.Proxies.Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
openbullet committed Sep 7, 2024
1 parent 915e32b commit 3ef3277
Show file tree
Hide file tree
Showing 13 changed files with 735 additions and 716 deletions.
119 changes: 58 additions & 61 deletions RuriLib.Proxies.Tests/ProxyClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,82 +8,79 @@
using System.Threading;
using RuriLib.Proxies.Exceptions;

namespace RuriLib.Proxies.Tests
namespace RuriLib.Proxies.Tests;

public class ProxyClientTests
{
public class ProxyClientTests
[Fact]
public async Task ConnectAsync_NoProxyClient_Http()
{
[Fact]
public async Task ConnectAsync_NoProxyClient_Http()
{
var settings = new ProxySettings();
var proxy = new NoProxyClient(settings);
var settings = new ProxySettings();
var proxy = new NoProxyClient(settings);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var client = await proxy.ConnectAsync("example.com", 80, null, cts.Token);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var client = await proxy.ConnectAsync("example.com", 80, null, cts.Token);

var response = await GetResponseAsync(client, BuildSampleGetRequest(), cts.Token);
Assert.Contains("Example Domain", response);
}
var response = await GetResponseAsync(client, BuildSampleGetRequest(), cts.Token);
Assert.Contains("Example Domain", response);
}

/*
[Fact]
public async Task ConnectAsync_HttpProxyClient_Http()
{
var settings = new ProxySettings() { Host = "127.0.0.1", Port = 8888 };
var proxy = new HttpProxyClient(settings);
[Fact(Skip = "This test requires a local HTTP proxy server")]
public async Task ConnectAsync_HttpProxyClient_Http()
{
var settings = new ProxySettings { Host = "127.0.0.1", Port = 8888 };
var proxy = new HttpProxyClient(settings);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var client = await proxy.ConnectAsync("example.com", 80, null, cts.Token);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var client = await proxy.ConnectAsync("example.com", 80, null, cts.Token);

var response = await GetResponseAsync(client, BuildSampleGetRequest(), cts.Token);
Assert.Contains("Example Domain", response);
}
*/
var response = await GetResponseAsync(client, BuildSampleGetRequest(), cts.Token);
Assert.Contains("Example Domain", response);
}

[Fact]
public async Task ConnectAsync_HttpProxyClient_Invalid()
{
// Set an invalid proxy
var settings = new ProxySettings() { Host = "example.com", Port = 80 };
var proxy = new HttpProxyClient(settings);
[Fact]
public async Task ConnectAsync_HttpProxyClient_Invalid()
{
// Set an invalid proxy
var settings = new ProxySettings { Host = "example.com", Port = 80 };
var proxy = new HttpProxyClient(settings);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await Assert.ThrowsAsync<ProxyException>(async () => await proxy.ConnectAsync("example.com", 80, null, cts.Token));
}
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await Assert.ThrowsAsync<ProxyException>(async () => await proxy.ConnectAsync("example.com", 80, null, cts.Token));
}

private static async Task<string> GetResponseAsync(TcpClient client, string request,
CancellationToken cancellationToken = default)
{
using var netStream = client.GetStream();
using var memory = new MemoryStream();
private static async Task<string> GetResponseAsync(TcpClient client, string request,
CancellationToken cancellationToken = default)
{
await using var netStream = client.GetStream();
using var memory = new MemoryStream();

// Send the data
var requestBytes = Encoding.ASCII.GetBytes(request);
await netStream.WriteAsync(requestBytes.AsMemory(0, requestBytes.Length), cancellationToken);
// Send the data
var requestBytes = Encoding.ASCII.GetBytes(request);
await netStream.WriteAsync(requestBytes.AsMemory(0, requestBytes.Length), cancellationToken);

// Read the response
await netStream.CopyToAsync(memory, cancellationToken);
memory.Position = 0;
var data = memory.ToArray();
return Encoding.UTF8.GetString(data);
}
// Read the response
await netStream.CopyToAsync(memory, cancellationToken);
memory.Position = 0;
var data = memory.ToArray();
return Encoding.UTF8.GetString(data);
}

private static string BuildSampleGetRequest()
private static string BuildSampleGetRequest()
{
var requestLines = new[]
{
var requestLines = new string[]
{
"GET / HTTP/1.1",
"Host: example.com",
"GET / HTTP/1.1",
"Host: example.com",

// We need this otherwise the server will default to Keep-Alive and
// not close the stream leaving us hanging...
"Connection: Close",
"Accept: */*",
string.Empty,
string.Empty
};
// We need this otherwise the server will default to Keep-Alive and
// not close the stream leaving us hanging...
"Connection: Close",
"Accept: */*",
string.Empty,
string.Empty
};

return string.Join("\r\n", requestLines);
}
return string.Join("\r\n", requestLines);
}
}
1 change: 1 addition & 0 deletions RuriLib.Proxies.Tests/RuriLib.Proxies.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand Down
198 changes: 102 additions & 96 deletions RuriLib.Proxies/Clients/HttpProxyClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,140 +9,146 @@
using RuriLib.Proxies.Helpers;
using RuriLib.Proxies.Exceptions;

namespace RuriLib.Proxies.Clients
namespace RuriLib.Proxies.Clients;

/// <summary>
/// A client that provides proxies connections via HTTP proxies.
/// </summary>
public class HttpProxyClient : ProxyClient
{
/// <summary>
/// A client that provides proxies connections via HTTP proxies.
/// The HTTP version to send in the first line of the request to the proxy.
/// By default it's 1.1
/// </summary>
public string ProtocolVersion { get; set; } = "1.1";

/// <summary>
/// Creates an HTTP proxy client given the proxy <paramref name="settings"/>.
/// </summary>
public class HttpProxyClient : ProxyClient
public HttpProxyClient(ProxySettings settings) : base(settings)
{
/// <summary>
/// The HTTP version to send in the first line of the request to the proxy.
/// By default it's 1.1
/// </summary>
public string ProtocolVersion { get; set; } = "1.1";

/// <summary>
/// Creates an HTTP proxy client given the proxy <paramref name="settings"/>.
/// </summary>
public HttpProxyClient(ProxySettings settings) : base(settings)
{

}
}

/// <inheritdoc/>
protected async override Task CreateConnectionAsync(TcpClient client, string destinationHost, int destinationPort,
CancellationToken cancellationToken = default)
/// <inheritdoc/>
protected override async Task CreateConnectionAsync(TcpClient client, string destinationHost, int destinationPort,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(destinationHost);

if (!PortHelper.ValidateTcpPort(destinationPort))
{
if (string.IsNullOrEmpty(destinationHost))
{
throw new ArgumentException(null, nameof(destinationHost));
}
throw new ArgumentOutOfRangeException(nameof(destinationPort));
}

if (!PortHelper.ValidateTcpPort(destinationPort))
{
throw new ArgumentOutOfRangeException(nameof(destinationPort));
}
if (client is not { Connected: true })
{
throw new SocketException();
}

if (client == null || !client.Connected)
{
throw new SocketException();
}
HttpStatusCode statusCode;

HttpStatusCode statusCode;
try
{
var nStream = client.GetStream();

try
{
var nStream = client.GetStream();
await RequestConnectionAsync(nStream, destinationHost, destinationPort, cancellationToken).ConfigureAwait(false);
statusCode = await ReceiveResponseAsync(nStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
client.Close();

await RequestConnectionAsync(nStream, destinationHost, destinationPort, cancellationToken).ConfigureAwait(false);
statusCode = await ReceiveResponseAsync(nStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
if (ex is IOException || ex is SocketException)
{
client.Close();

if (ex is IOException || ex is SocketException)
{
throw new ProxyException("Error while working with proxy", ex);
}

throw;
throw new ProxyException("Error while working with proxy", ex);
}

if (statusCode != HttpStatusCode.OK)
{
client.Close();
throw new ProxyException("The proxy didn't reply with 200 OK");
}
throw;
}

private async Task RequestConnectionAsync(Stream nStream, string destinationHost, int destinationPort,
CancellationToken cancellationToken = default)
if (statusCode != HttpStatusCode.OK)
{
var commandBuilder = new StringBuilder();
client.Close();
throw new ProxyException("The proxy didn't reply with 200 OK");
}
}

commandBuilder.AppendFormat("CONNECT {0}:{1} HTTP/{2}\r\n{3}\r\n", destinationHost, destinationPort, ProtocolVersion, GenerateAuthorizationHeader());
private async Task RequestConnectionAsync(Stream nStream, string destinationHost, int destinationPort,
CancellationToken cancellationToken = default)
{
var commandBuilder = new StringBuilder();

var buffer = Encoding.ASCII.GetBytes(commandBuilder.ToString());
commandBuilder.AppendFormat(
"CONNECT {0}:{1} HTTP/{2}\r\n{3}\r\n",
destinationHost, destinationPort, ProtocolVersion, GenerateAuthorizationHeader());

await nStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
}

private string GenerateAuthorizationHeader()
{
if (Settings.Credentials == null || string.IsNullOrEmpty(Settings.Credentials.UserName))
{
return string.Empty;
}
var buffer = Encoding.ASCII.GetBytes(commandBuilder.ToString());

var data = Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{Settings.Credentials.UserName}:{Settings.Credentials.Password}"));
await nStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
}

return $"Proxy-Authorization: Basic {data}\r\n";
private string GenerateAuthorizationHeader()
{
if (Settings.Credentials == null || string.IsNullOrEmpty(Settings.Credentials.UserName))
{
return string.Empty;
}

private static async Task<HttpStatusCode> ReceiveResponseAsync(NetworkStream nStream, CancellationToken cancellationToken = default)
{
var buffer = new byte[50];
var responseBuilder = new StringBuilder();
var data = Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{Settings.Credentials.UserName}:{Settings.Credentials.Password}"));

using var waitCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(nStream.ReadTimeout));
return $"Proxy-Authorization: Basic {data}\r\n";
}

while (!nStream.DataAvailable)
{
// Throw default exception if the operation was cancelled by the user
cancellationToken.ThrowIfCancellationRequested();
private static async Task<HttpStatusCode> ReceiveResponseAsync(NetworkStream nStream, CancellationToken cancellationToken = default)
{
var buffer = new byte[50];
var responseBuilder = new StringBuilder();

// Throw a custom exception if we timed out
if (waitCts.Token.IsCancellationRequested)
throw new ProxyException("Timed out while waiting for data from proxy");
using var waitCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(nStream.ReadTimeout));

await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
while (!nStream.DataAvailable)
{
// Throw default exception if the operation was cancelled by the user
cancellationToken.ThrowIfCancellationRequested();

do
// Throw a custom exception if we timed out
if (waitCts.Token.IsCancellationRequested)
{
var bytesRead = await nStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead));
throw new ProxyException("Timed out while waiting for data from proxy");
}
while (nStream.DataAvailable);

var response = responseBuilder.ToString();
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}

if (response.Length == 0)
throw new ProxyException("Received empty response");
do
{
var bytesRead = await nStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead));
}
while (nStream.DataAvailable);

// Check if the response is a correct HTTP response
var match = Regex.Match(response, "HTTP/[0-9\\.]* ([0-9]{3})");
var response = responseBuilder.ToString();

if (!match.Success)
throw new ProxyException("Received wrong HTTP response from proxy");
if (response.Length == 0)
{
throw new ProxyException("Received empty response");
}

// Check if the response is a correct HTTP response
var match = Regex.Match(response, "HTTP/[0-9\\.]* ([0-9]{3})");

if (!Enum.TryParse(match.Groups[1].Value, out HttpStatusCode statusCode))
throw new ProxyException("Invalid HTTP status code");
if (!match.Success)
{
throw new ProxyException("Received wrong HTTP response from proxy");
}

return statusCode;
if (!Enum.TryParse(match.Groups[1].Value, out HttpStatusCode statusCode))
{
throw new ProxyException("Invalid HTTP status code");
}

return statusCode;
}
}
Loading

0 comments on commit 3ef3277

Please sign in to comment.