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();