Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions Library/RSBot.Core/Extensions/SocketExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public struct ProxyConfig
public byte Version;
public int TimeOut;

public static bool TryGetProxy(out ProxyConfig config)
{
return TryGetProxy(null, 0, out config);
}

public static bool TryGetProxy(string ip, int port, out ProxyConfig config)
{
config = new ProxyConfig
Expand Down Expand Up @@ -53,6 +58,17 @@ public static bool TryGetProxy(string ip, int port, out ProxyConfig config)

return true;
}

public readonly WebProxy CreateWebProxy()
{
string scheme = Version == 5 ? "socks5" : "socks4";
var proxy = new WebProxy(new UriBuilder(scheme, ProxyIp, ProxyPort).Uri);

if (!string.IsNullOrEmpty(UserName) || !string.IsNullOrEmpty(Password))
proxy.Credentials = new NetworkCredential(UserName ?? string.Empty, Password ?? string.Empty);

return proxy;
}
}

public static class SocketExtensions
Expand All @@ -72,16 +88,21 @@ public static void Connect(this Socket socket, string host, int port, int timeou
throw new SocketException(10060); // Connection timed out.
}

public static async Task<bool> ConnectViaProxy(this Socket socket, ProxyConfig config)
public static async Task<bool> ConnectViaProxy(this Socket socket, ProxyConfig config, bool logConnection = true)
{
Log.Notify(
$"Trying to connect to proxy server [{config.ProxyIp}:{config.ProxyPort} / Version: {config.Version}]..."
);
if (logConnection)
{
Log.Notify(
$"Trying to connect to proxy server [{config.ProxyIp}:{config.ProxyPort} / Version: {config.Version}]..."
);
}

await socket.ConnectAsync(config.ProxyIp, config.ProxyPort);
if (!socket.Connected)
return false;

Log.Notify("Successfully connected to proxy server...");
if (logConnection)
Log.Notify("Successfully connected to proxy server...");

var portBuffer = new byte[2] { Convert.ToByte(config.Port / 256), Convert.ToByte(config.Port % 256) };

Expand Down Expand Up @@ -180,6 +201,13 @@ await socket.SendAsync(
// +----+----+----+----+----+----+----+----+----+----+....+----+
// 1 1 2 4 variable 1

if (addressType == 3)
{
addressBytes = Dns.GetHostAddresses(config.Ip)
.First(address => address.AddressFamily == AddressFamily.InterNetwork)
.GetAddressBytes();
}

var userIdBytes = Encoding.ASCII.GetBytes(config.UserName);

var request = new byte[9 + userIdBytes.Length];
Expand Down
234 changes: 234 additions & 0 deletions Plugins/RSBot.General/Components/ConfiguredProxyBridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RSBot.Core.Extensions;

namespace RSBot.General.Components;

/// <summary>
/// Exposes a loopback HTTP proxy for browser components that cannot authenticate with SOCKS proxies.
/// Each outgoing connection uses the current RSBot.Network.Proxy configuration.
/// </summary>
internal sealed class ConfiguredProxyBridge
{
private const int MaxHeaderSize = 64 * 1024;
private static readonly Lazy<ConfiguredProxyBridge> LazyInstance = new(() => new ConfiguredProxyBridge());

private readonly TcpListener _listener;
private readonly object _proxyConfigLock = new();
private ProxyConfig _proxyConfig;

private ConfiguredProxyBridge()
{
_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
Address = new Uri($"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}");
_ = AcceptConnectionsAsync();
}

public static ConfiguredProxyBridge Instance => LazyInstance.Value;

public Uri Address { get; }

public Uri Configure(ProxyConfig proxyConfig)
{
lock (_proxyConfigLock)
_proxyConfig = proxyConfig;

return Address;
}

private async Task AcceptConnectionsAsync()
{
while (true)
{
TcpClient client;
try
{
client = await _listener.AcceptTcpClientAsync();
}
catch (ObjectDisposedException)
{
return;
}
catch (SocketException)
{
return;
}

_ = HandleClientAsync(client);
}
}

private async Task HandleClientAsync(TcpClient client)
{
using (client)
{
try
{
NetworkStream clientStream = client.GetStream();
byte[] request = await ReadRequestHeaderAsync(clientStream);
if (
!TryParseDestination(request, out string host, out int port, out bool isConnect, out byte[] payload)
)
{
await WriteErrorAsync(clientStream, "400 Bad Request");
return;
}

ProxyConfig proxyConfig;
lock (_proxyConfigLock)
{
proxyConfig = _proxyConfig;
}

proxyConfig.Ip = host;
proxyConfig.Port = port;

using var destination = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
if (!await destination.ConnectViaProxy(proxyConfig, logConnection: false))
{
await WriteErrorAsync(clientStream, "502 Bad Gateway");
return;
}

using var destinationStream = new NetworkStream(destination, ownsSocket: false);
if (isConnect)
{
byte[] established = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n");
await clientStream.WriteAsync(established);
}
else
{
await destinationStream.WriteAsync(payload);
}

Task upload = clientStream.CopyToAsync(destinationStream);
Task download = destinationStream.CopyToAsync(clientStream);
await Task.WhenAny(upload, download);
}
catch (Exception ex) when (ex is IOException or SocketException or SocketProxyException)
{
try
{
await WriteErrorAsync(client.GetStream(), "502 Bad Gateway");
}
catch
{
// The browser already closed the connection.
}
}
}
}

private static async Task<byte[]> ReadRequestHeaderAsync(NetworkStream stream)
{
using var request = new MemoryStream();
var buffer = new byte[4096];

while (request.Length < MaxHeaderSize)
{
int read = await stream.ReadAsync(buffer);
if (read == 0)
break;

request.Write(buffer, 0, read);
if (FindHeaderEnd(request.GetBuffer(), (int)request.Length) >= 0)
return request.ToArray();
}

throw new IOException("The local proxy received an incomplete or oversized HTTP request header.");
}

private static bool TryParseDestination(
byte[] request,
out string host,
out int port,
out bool isConnect,
out byte[] payload
)
{
host = null;
port = 0;
isConnect = false;
payload = null;

int headerEnd = FindHeaderEnd(request, request.Length);
if (headerEnd < 0)
return false;

string header = Encoding.ASCII.GetString(request, 0, headerEnd);
string[] lines = header.Split(["\r\n"], StringSplitOptions.None);
string[] requestLine = lines[0].Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
if (requestLine.Length != 3)
return false;

isConnect = string.Equals(requestLine[0], "CONNECT", StringComparison.OrdinalIgnoreCase);
if (isConnect)
{
if (!TryParseAuthority(requestLine[1], 443, out host, out port))
return false;

payload = Array.Empty<byte>();
return true;
}

if (!Uri.TryCreate(requestLine[1], UriKind.Absolute, out Uri requestUri))
return false;

host = requestUri.Host;
port = requestUri.Port;
string pathAndQuery = string.IsNullOrEmpty(requestUri.PathAndQuery) ? "/" : requestUri.PathAndQuery;
string rewrittenHeader =
$"{requestLine[0]} {pathAndQuery} {requestLine[2]}\r\n" + string.Join("\r\n", lines.Skip(1));
byte[] rewrittenHeaderBytes = Encoding.ASCII.GetBytes(rewrittenHeader);
int bodyLength = request.Length - headerEnd;
payload = new byte[rewrittenHeaderBytes.Length + bodyLength];
rewrittenHeaderBytes.CopyTo(payload, 0);
request.AsSpan(headerEnd, bodyLength).CopyTo(payload.AsSpan(rewrittenHeaderBytes.Length));
return true;
}

private static bool TryParseAuthority(string authority, int defaultPort, out string host, out int port)
{
host = null;
port = 0;
if (!Uri.TryCreate($"http://{authority}", UriKind.Absolute, out Uri uri))
return false;

host = uri.Host;
port = uri.IsDefaultPort ? defaultPort : uri.Port;
return !string.IsNullOrWhiteSpace(host) && port is > 0 and <= ushort.MaxValue;
}

private static int FindHeaderEnd(byte[] buffer, int length)
{
for (int index = 3; index < length; index++)
{
if (
buffer[index - 3] == '\r'
&& buffer[index - 2] == '\n'
&& buffer[index - 1] == '\r'
&& buffer[index] == '\n'
)
{
return index + 1;
}
}

return -1;
}

private static Task WriteErrorAsync(NetworkStream stream, string status)
{
byte[] response = Encoding.ASCII.GetBytes(
$"HTTP/1.1 {status}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
);
return stream.WriteAsync(response).AsTask();
}
}
4 changes: 4 additions & 0 deletions Plugins/RSBot.General/Components/JSROAuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using System.Windows.Forms;
using RSBot.Core;
using RSBot.Core.Extensions;

namespace RSBot.General.Components;

Expand All @@ -22,6 +23,9 @@ public static async Task<bool> GetTokenAsync()
AutomaticDecompression = DecompressionMethods.All,
};

if (ProxyConfig.TryGetProxy(out ProxyConfig proxyConfig))
handler.Proxy = proxyConfig.CreateWebProxy();

HttpClient client = new(handler);

client.DefaultRequestHeaders.TryAddWithoutValidation(
Expand Down
Loading
Loading