From 37fe6263252334d743bda2d83f029479854e47e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:44:35 +0000 Subject: [PATCH] Replace hand-rolled ProxiedWebRequest with HttpClient/SocketsHttpHandler - Rewrite ProxiedWebRequest to use System.Net.Http.HttpClient internally - Use SocketsHttpHandler with native SOCKS4/4a/5 and HTTP proxy support - Remove 250+ lines of manual HTTP/1.1 parsing, chunked transfer, TLS pinning - Proper TLS negotiation (no longer pinned to TLS 1.2) - Remove unused ITcpFactory interface and isProxied flag - Maintain same public API (Get(), Post(), Response class) for all consumers Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/milutinke/Minecraft-Console-Client/sessions/afcd1b7b-ea23-4a0d-bb46-a90b623406fc --- MinecraftClient/Protocol/ProxiedWebRequest.cs | 459 +++++------------- 1 file changed, 117 insertions(+), 342 deletions(-) diff --git a/MinecraftClient/Protocol/ProxiedWebRequest.cs b/MinecraftClient/Protocol/ProxiedWebRequest.cs index 06e69a9d..247a65e9 100644 --- a/MinecraftClient/Protocol/ProxiedWebRequest.cs +++ b/MinecraftClient/Protocol/ProxiedWebRequest.cs @@ -1,413 +1,202 @@ -using System; -using System.Collections.Generic; +using System; using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; +using System.Net; +using System.Net.Http; using System.Text; -using System.Threading; using MinecraftClient.Proxy; namespace MinecraftClient.Protocol { /// - /// Create a new http request and optionally with proxy according to setting + /// HTTP client with automatic proxy support based on application settings. + /// Backed by System.Net.Http.HttpClient with SocketsHttpHandler. /// public class ProxiedWebRequest { - public interface ITcpFactory + private readonly Uri _uri; + + public NameValueCollection Headers { get; } = new(); + + public string UserAgent { - TcpClient CreateTcpClient(string host, int port); - }; + get => Headers.Get("User-Agent") ?? string.Empty; + set => Headers.Set("User-Agent", value); + } - private readonly string httpVersion = "HTTP/1.1"; + public string Accept + { + get => Headers.Get("Accept") ?? string.Empty; + set => Headers.Set("Accept", value); + } - private ITcpFactory? tcpFactory; - private bool isProxied = false; // Send absolute Url in request if true + public string Cookie + { + set => Headers.Set("Cookie", value); + } - private readonly Uri uri; - private string Host { get { return uri.Host; } } - private int Port { get { return uri.Port; } } - private string Path { get { return uri.PathAndQuery; } } - private string AbsoluteUrl { get { return uri.AbsoluteUri; } } - private bool IsSecure { get { return uri.Scheme == "https"; } } - - public NameValueCollection Headers = new(); - - public string UserAgent { get { return Headers.Get("User-Agent") ?? String.Empty; } set { Headers.Set("User-Agent", value); } } - public string Accept { get { return Headers.Get("Accept") ?? String.Empty; } set { Headers.Set("Accept", value); } } - public string Cookie { set { Headers.Set("Cookie", value); } } + public bool Debug => Settings.Config.Logging.DebugMessages; /// - /// Set to true to tell the http client proxy is enabled - /// - public bool IsProxy { get { return isProxied; } set { isProxied = value; } } - public bool Debug { get { return Settings.Config.Logging.DebugMessages; } } - - /// - /// Create a new http request + /// Create a new HTTP request /// /// Target URL public ProxiedWebRequest(string url) { - uri = new Uri(url); + _uri = new Uri(url); SetupBasicHeaders(); } /// - /// Create a new http request with cookies + /// Create a new HTTP request with cookies /// /// Target URL - /// Cookies to use + /// Cookies to include in the request public ProxiedWebRequest(string url, NameValueCollection cookies) { - uri = new Uri(url); + _uri = new Uri(url); Headers.Add("Cookie", GetCookieString(cookies)); SetupBasicHeaders(); } - /// - /// Create a new http request with custom tcp client - /// - /// Tcp factory to be used - /// Target URL - public ProxiedWebRequest(ITcpFactory tcpFactory, string url) : this(url) - { - this.tcpFactory = tcpFactory; - } - - /// - /// Create a new http request with custom tcp client and cookies - /// - /// Tcp factory to be used - /// Target URL - /// Cookies to use - public ProxiedWebRequest(ITcpFactory tcpFactory, string url, NameValueCollection cookies) : this(url, cookies) - { - this.tcpFactory = tcpFactory; - } - - /// - /// Setup some basic headers - /// private void SetupBasicHeaders() { - Headers.Add("Host", Host); + Headers.Add("Host", _uri.Host); Headers.Add("User-Agent", "MCC/1.0"); Headers.Add("Accept", "*/*"); - Headers.Add("Connection", "close"); } /// - /// Perform GET request and get the response. Proxy is handled automatically + /// Perform GET request. Proxy is handled automatically. /// - /// - public Response Get() - { - return Send("GET"); - } + public Response Get() => Send(HttpMethod.Get); /// - /// Perform POST request and get the response. Proxy is handled automatically + /// Perform POST request. Proxy is handled automatically. /// /// The content type of request body /// Request body - /// - public Response Post(string contentType, string body) - { - Headers.Add("Content-Type", contentType); - // Calculate length - Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString()); - return Send("POST", body); - } + public Response Post(string contentType, string body) => Send(HttpMethod.Post, contentType, body); /// - /// Send a http request to the server. Proxy is handled automatically + /// Send an HTTP request. Proxy is configured automatically from Settings. /// - /// Method in string representation - /// Optional request body - /// - private Response Send(string method, string body = "") + private Response Send(HttpMethod method, string? contentType = null, string? body = null) { - List requestMessage = new() + using var handler = CreateHandler(); + using var client = new HttpClient(handler); + + using var request = new HttpRequestMessage(method, _uri); + + // Apply custom headers (skip content-level headers) + foreach (string key in Headers) { - string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line - }; - foreach (string key in Headers) // Headers - { - var value = Headers[key]; - requestMessage.Add(string.Format("{0}: {1}", key, value)); + if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || + key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) || + key.Equals("Host", StringComparison.OrdinalIgnoreCase)) + continue; + + request.Headers.TryAddWithoutValidation(key, Headers[key]); } - requestMessage.Add(""); // - if (body != "") + + if (body is not null) { - requestMessage.Add(body); + request.Content = new StringContent(body, Encoding.UTF8, contentType ?? "text/plain"); } - else requestMessage.Add(""); // + if (Debug) { - foreach (string l in requestMessage) - { - ConsoleIO.WriteLine("< " + l); - } + ConsoleIO.WriteLine($"< {method} {_uri}"); + foreach (string key in Headers) + ConsoleIO.WriteLine($"< {key}: {Headers[key]}"); } - Response response = Response.Empty(); - - // FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class - // TcpClient client = tcpFactory.CreateTcpClient(Host, Port); - TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true); - Stream stream; - if (IsSecure) - { - stream = new SslStream(client.GetStream()); - ((SslStream)stream).AuthenticateAsClient(Host, null, SslProtocols.Tls12, true); // Enable TLS 1.2. Hotfix for #1774 - } - else - { - stream = client.GetStream(); - } - string h = string.Join("\r\n", requestMessage.ToArray()); - byte[] data = Encoding.ASCII.GetBytes(h); - stream.Write(data, 0, data.Length); - stream.Flush(); - - // Read response - int statusCode = ReadHttpStatus(stream); - var headers = ReadHeader(stream); - string? rbody; - if (headers.Get("transfer-encoding") == "chunked") - { - rbody = ReadBodyChunked(stream); - } - else - { - rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0")); - } - if (headers.Get("set-cookie") != null) - { - response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty()); - } - response.Body = rbody ?? ""; - response.StatusCode = statusCode; - response.Headers = headers; try { - stream.Close(); - client.Close(); - } - catch { } + using var httpResponse = client.Send(request); + string responseBody = httpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return response; - } + var responseHeaders = new NameValueCollection(); + foreach (var header in httpResponse.Headers) + foreach (var val in header.Value) + responseHeaders.Add(header.Key.ToLowerInvariant(), val); + foreach (var header in httpResponse.Content.Headers) + foreach (var val in header.Value) + responseHeaders.Add(header.Key.ToLowerInvariant(), val); - /// - /// Read HTTP response line from a Stream - /// - /// Stream to read - /// - /// If server return unknown data - private static int ReadHttpStatus(Stream s) - { - var httpHeader = ReadLine(s); // http header line - if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0")) - { - return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); - } - else - { - throw new InvalidDataException("Unexpect data from server"); - } - } - - /// - /// Read HTTP headers from a Stream - /// - /// Stream to read - /// Headers in lower-case - private static NameValueCollection ReadHeader(Stream s) - { - var headers = new NameValueCollection(); - // Read headers - string header; - do - { - header = ReadLine(s); - if (!String.IsNullOrEmpty(header)) + var cookies = new NameValueCollection(); + foreach (System.Net.Cookie cookie in handler.CookieContainer.GetCookies(_uri)) { - var tmp = header.Split(new char[] { ':' }, 2); - var name = tmp[0].ToLower(); - var value = tmp[1].Trim(); - headers.Add(name, value); + if (!cookie.Expired) + cookies.Add(cookie.Name, cookie.Value); } + + return new Response((int)httpResponse.StatusCode, responseBody, responseHeaders, cookies); + } + catch (HttpRequestException ex) + { + if (Debug) + ConsoleIO.WriteLine("HTTP error: " + ex.Message); + return Response.Empty(); } - while (!String.IsNullOrEmpty(header)); - return headers; } /// - /// Read HTTP body from a Stream + /// Create a SocketsHttpHandler with proxy support from ProxyHandler settings. /// - /// Stream to read - /// Length of the body (the Content-Length header) - /// Body or null if length is zero - private static string? ReadBody(Stream s, int length) + private static SocketsHttpHandler CreateHandler() { - if (length > 0) + var handler = new SocketsHttpHandler { - byte[] buffer = new byte[length]; - int r = 0; - while (r < length) + UseCookies = true, + CookieContainer = new CookieContainer(), + AllowAutoRedirect = false, + ConnectTimeout = TimeSpan.FromSeconds(30), + }; + + if (ProxyHandler.Config.Enabled_Login) + { + string proxyScheme = ProxyHandler.Config.Proxy_Type switch { - var read = s.Read(buffer, r, length - r); - r += read; - Thread.Sleep(50); + ProxyHandler.Configs.ProxyType.SOCKS4 => "socks4", + ProxyHandler.Configs.ProxyType.SOCKS4a => "socks4a", + ProxyHandler.Configs.ProxyType.SOCKS5 => "socks5", + _ => "http" + }; + + var proxyUri = new Uri($"{proxyScheme}://{ProxyHandler.Config.Server.Host}:{ProxyHandler.Config.Server.Port}"); + var proxy = new WebProxy(proxyUri); + + if (!string.IsNullOrWhiteSpace(ProxyHandler.Config.Username) && + !string.IsNullOrWhiteSpace(ProxyHandler.Config.Password)) + { + proxy.Credentials = new NetworkCredential( + ProxyHandler.Config.Username, + ProxyHandler.Config.Password); } - return Encoding.UTF8.GetString(buffer); - } - else - { - return null; + + handler.Proxy = proxy; + handler.UseProxy = true; } + + return handler; } /// - /// Read HTTP chunked body from a Stream + /// Build a cookie header value from a NameValueCollection. /// - /// Stream to read - /// Body or empty string if nothing is received - private static string ReadBodyChunked(Stream s) - { - List buffer1 = new(); - while (true) - { - string l = ReadLine(s); - int size = Int32.Parse(l, NumberStyles.HexNumber); - if (size == 0) - break; - byte[] buffer2 = new byte[size]; - int r = 0; - while (r < size) - { - var read = s.Read(buffer2, r, size - r); - r += read; - Thread.Sleep(50); - } - ReadLine(s); - buffer1.AddRange(buffer2); - } - return Encoding.UTF8.GetString(buffer1.ToArray()); - } - - /// - /// Parse the Set-Cookie header value into NameValueCollection. Cookie options are ignored - /// - /// Array of value strings - /// Parsed cookies - private static NameValueCollection ParseSetCookie(IEnumerable headerValue) - { - NameValueCollection cookies = new(); - foreach (var value in headerValue) - { - string[] cookie = value.Split(';'); // cookie options are ignored - string[] tmp = cookie[0].Split(new char[] { '=' }, 2); // Split first '=' only - string[] options = cookie[1..]; - string cname = tmp[0].Trim(); - string cvalue = tmp[1].Trim(); - // Check expire - bool isExpired = false; - foreach (var option in options) - { - var tmp2 = option.Trim().Split(new char[] { '=' }, 2); - // Check for Expires= and Max-Age= - if (tmp2.Length == 2) - { - var optName = tmp2[0].Trim().ToLower(); - var optValue = tmp2[1].Trim(); - switch (optName) - { - case "expires": - { - if (DateTime.TryParse(optValue, out var expDate)) - { - if (expDate < DateTime.Now) - isExpired = true; - } - break; - } - case "max-age": - { - if (int.TryParse(optValue, out var expInt)) - { - if (expInt <= 0) - isExpired = true; - } - break; - } - } - } - if (isExpired) - break; - } - if (!isExpired) - cookies.Add(cname, cvalue); - } - return cookies; - } - - /// - /// Read a line from a Stream - /// - /// - /// Line break by \r\n and they are not included in returned string - /// - /// Stream to read - /// String - private static string ReadLine(Stream s) - { - List buffer = new(); - byte c; - while (true) - { - int b = s.ReadByte(); - if (b == -1) - break; - c = (byte)b; - if (c == '\n') - { - if (buffer.Last() == '\r') - { - buffer.RemoveAt(buffer.Count - 1); - break; - } - } - buffer.Add(c); - } - return Encoding.UTF8.GetString(buffer.ToArray()); - } - - /// - /// Get the cookie string representation to use in header - /// - /// - /// private static string GetCookieString(NameValueCollection cookies) { var sb = new StringBuilder(); foreach (string key in cookies) { - var value = cookies[key]; - sb.Append(string.Format("{0}={1}; ", key, value)); + sb.Append($"{key}={cookies[key]}; "); } string result = sb.ToString(); - return result.Remove(result.Length - 2); // Remove "; " at the end + return result.Length >= 2 ? result[..^2] : result; } /// - /// Basic response object + /// Basic HTTP response object. /// public class Response { @@ -424,14 +213,8 @@ namespace MinecraftClient.Protocol Cookies = cookies; } - /// - /// Get an empty response object - /// - /// - public static Response Empty() - { - return new Response(204 /* No content */, "", new NameValueCollection(), new NameValueCollection()); - } + public static Response Empty() => + new(204, "", new NameValueCollection(), new NameValueCollection()); public override string ToString() { @@ -439,26 +222,18 @@ namespace MinecraftClient.Protocol sb.AppendLine("Status code: " + StatusCode); sb.AppendLine("Headers:"); foreach (string key in Headers) - { - sb.AppendLine(string.Format(" {0}: {1}", key, Headers[key])); - } + sb.AppendLine($" {key}: {Headers[key]}"); if (Cookies.Count > 0) { sb.AppendLine(); sb.AppendLine("Cookies: "); foreach (string key in Cookies) - { - sb.AppendLine(string.Format(" {0}={1}", key, Cookies[key])); - } + sb.AppendLine($" {key}={Cookies[key]}"); } if (Body != "") { sb.AppendLine(); - if (Body.Length > 200) - { - sb.AppendLine("Body: (Truncated to 200 characters)"); - } - else sb.AppendLine("Body: "); + sb.AppendLine(Body.Length > 200 ? "Body: (Truncated to 200 characters)" : "Body: "); sb.AppendLine(Body.Length > 200 ? Body[..200] + "..." : Body); } return sb.ToString();