diff --git a/RuriLib.Http.Tests/ProxyClientHandlerTests.cs b/RuriLib.Http.Tests/ProxyClientHandlerTests.cs index fbcd5b8ed..af83f9eae 100644 --- a/RuriLib.Http.Tests/ProxyClientHandlerTests.cs +++ b/RuriLib.Http.Tests/ProxyClientHandlerTests.cs @@ -8,244 +8,242 @@ using System.Threading.Tasks; using Xunit; -namespace RuriLib.Http.Tests +namespace RuriLib.Http.Tests; + +public class ProxyClientHandlerTests { - public class ProxyClientHandlerTests + [Fact] + public async Task SendAsync_Get_Headers() { - [Fact] - public async Task SendAsync_Get_Headers() + const string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + + var message = new HttpRequestMessage { - var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/user-agent") + }; - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/user-agent") - }; + message.Headers.Add("User-Agent", userAgent); - message.Headers.Add("User-Agent", userAgent); + var response = await RequestAsync(message); + var userAgentActual = await GetJsonStringValueAsync(response, "user-agent"); - var response = await RequestAsync(message); - var userAgentActual = await GetJsonStringValueAsync(response, "user-agent"); + Assert.NotEmpty(userAgentActual); + Assert.Equal(userAgent, userAgentActual); + } - Assert.NotEmpty(userAgentActual); - Assert.Equal(userAgent, userAgentActual); - } + [Fact] + public async Task SendAsync_Get_Query() + { + const string key = "key"; + const string value = "value"; - [Fact] - public async Task SendAsync_Get_Query() + var message = new HttpRequestMessage { - var key = "key"; - var value = "value"; + Method = HttpMethod.Get, + RequestUri = new Uri($"http://httpbin.org/get?{key}={value}") + }; - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"http://httpbin.org/get?{key}={value}") - }; + var response = await RequestAsync(message); + var actual = await GetJsonDictionaryValueAsync(response, "args"); - var response = await RequestAsync(message); - var actual = await GetJsonDictionaryValueAsync(response, "args"); + Assert.NotNull(actual); + Assert.True(actual.ContainsKey(key)); + Assert.True(actual.ContainsValue(value)); + } - Assert.True(actual.ContainsKey(key)); - Assert.True(actual.ContainsValue(value)); - } + [Fact] + public async Task SendAsync_Get_UTF8() + { + const string expected = "∮"; - [Fact] - public async Task SendAsync_Get_UTF8() + var message = new HttpRequestMessage { - var expected = "∮"; + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/encoding/utf8") + }; - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/encoding/utf8") - }; + var response = await RequestAsync(message); + var actual = await response.Content.ReadAsStringAsync(); - var response = await RequestAsync(message); - var actual = await response.Content.ReadAsStringAsync(); + Assert.Contains(expected, actual); + } - Assert.Contains(expected, actual); - } + [Fact] + public async Task SendAsync_Get_HTML() + { + const long expectedLength = 3741; + const string contentType = "text/html"; + const string charSet = "utf-8"; - [Fact] - public async Task SendAsync_Get_HTML() + var message = new HttpRequestMessage { - long expectedLength = 3741; - var contentType = "text/html"; - var charSet = "utf-8"; - - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/html") - }; + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/html") + }; - var response = await RequestAsync(message); + var response = await RequestAsync(message); - var content = response.Content; - Assert.NotNull(content); + var content = response.Content; + Assert.NotNull(content); - var headers = response.Content.Headers; - Assert.NotNull(headers); + var headers = response.Content.Headers; + Assert.NotNull(headers); - Assert.NotNull(headers.ContentLength); - Assert.Equal(expectedLength, headers.ContentLength.Value); - Assert.NotNull(headers.ContentType); - Assert.Equal(contentType, headers.ContentType.MediaType); - Assert.Equal(charSet, headers.ContentType.CharSet); - } + Assert.NotNull(headers.ContentLength); + Assert.Equal(expectedLength, headers.ContentLength.Value); + Assert.NotNull(headers.ContentType); + Assert.Equal(contentType, headers.ContentType.MediaType); + Assert.Equal(charSet, headers.ContentType.CharSet); + } - [Fact] - public async Task SendAsync_Get_Delay() + [Fact] + public async Task SendAsync_Get_Delay() + { + var message = new HttpRequestMessage { - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/delay/4") - }; + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/delay/4") + }; - var response = await RequestAsync(message); - var source = response.Content.ReadAsStringAsync(); + var response = await RequestAsync(message); + var source = response.Content.ReadAsStringAsync(); - Assert.NotNull(response); - Assert.NotNull(source); - } + Assert.NotNull(response); + Assert.NotNull(source); + } - [Fact] - public async Task SendAsync_Get_Stream() + [Fact] + public async Task SendAsync_Get_Stream() + { + var message = new HttpRequestMessage { - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/stream/20") - }; + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/stream/20") + }; - var response = await RequestAsync(message); - var source = response.Content.ReadAsStringAsync(); + var response = await RequestAsync(message); + var source = response.Content.ReadAsStringAsync(); - Assert.NotNull(response); - Assert.NotNull(source); - } + Assert.NotNull(response); + Assert.NotNull(source); + } - [Fact] - public async Task SendAsync_Get_Gzip() - { - var expected = "gzip, deflate"; + [Fact] + public async Task SendAsync_Get_Gzip() + { + const string expected = "gzip, deflate"; - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("http://httpbin.org/gzip") - }; + var message = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri("http://httpbin.org/gzip") + }; - message.Headers.TryAddWithoutValidation("Accept-Encoding", expected); + message.Headers.TryAddWithoutValidation("Accept-Encoding", expected); - var response = await RequestAsync(message); - var actual = await GetJsonDictionaryValueAsync(response, "headers"); + var response = await RequestAsync(message); + var actual = await GetJsonDictionaryValueAsync(response, "headers"); + + Assert.NotNull(actual); + Assert.Equal(expected, actual["Accept-Encoding"]); + } - Assert.Equal(expected, actual["Accept-Encoding"]); - } + [Fact] + public async Task SendAsync_Get_Cookies() + { + const string name = "name"; + const string value = "value"; - [Fact] - public async Task SendAsync_Get_Cookies() + var message = new HttpRequestMessage { - var name = "name"; - var value = "value"; - - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"http://httpbin.org/cookies/set?{name}={value}") - }; - - var settings = new ProxySettings(); - var proxyClient = new NoProxyClient(settings); - var cookieContainer = new CookieContainer(); - using var proxyClientHandler = new ProxyClientHandler(proxyClient) - { - CookieContainer = cookieContainer - }; - - using var client = new HttpClient(proxyClientHandler); - var response = await client.SendAsync(message); - - var cookies = cookieContainer.GetCookies(new Uri("http://httpbin.org/")); - - Assert.Single(cookies); - var cookie = cookies[name]; - Assert.Equal(name, cookie.Name); - Assert.Equal(value, cookie.Value); - - client.Dispose(); - } - - [Fact] - public async Task SendAsync_Get_StatusCode() + Method = HttpMethod.Get, + RequestUri = new Uri($"http://httpbin.org/cookies/set?{name}={value}") + }; + + var settings = new ProxySettings(); + var proxyClient = new NoProxyClient(settings); + var cookieContainer = new CookieContainer(); + using var proxyClientHandler = new ProxyClientHandler(proxyClient) { - var code = "404"; - var expected = "NotFound"; + CookieContainer = cookieContainer + }; - var message = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"http://httpbin.org/status/{code}") - }; + using var client = new HttpClient(proxyClientHandler); + await client.SendAsync(message); - var response = await RequestAsync(message); + var cookies = cookieContainer.GetCookies(new Uri("http://httpbin.org/")); - Assert.NotNull(response); - Assert.Equal(expected, response.StatusCode.ToString()); - } + Assert.Single(cookies); + var cookie = cookies[name]; + Assert.NotNull(cookie); + Assert.Equal(name, cookie.Name); + Assert.Equal(value, cookie.Value); + } - [Fact] - public async Task SendAsync_Get_ExplicitHostHeader() + [Fact] + public async Task SendAsync_Get_StatusCode() + { + const string code = "404"; + const string expected = "NotFound"; + + var message = new HttpRequestMessage { - var message = new HttpRequestMessage(HttpMethod.Get, "https://httpbin.org/headers"); - message.Headers.Host = "httpbin.org"; + Method = HttpMethod.Get, + RequestUri = new Uri($"http://httpbin.org/status/{code}") + }; - var response = await RequestAsync(message); + var response = await RequestAsync(message); - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + Assert.NotNull(response); + Assert.Equal(expected, response.StatusCode.ToString()); + } - private static async Task RequestAsync(HttpRequestMessage request) - { - var settings = new ProxySettings(); - var proxyClient = new NoProxyClient(settings); - using var proxyClientHandler = new ProxyClientHandler(proxyClient) - { - CookieContainer = new CookieContainer() - }; - - using var client = new HttpClient(proxyClientHandler); - return await client.SendAsync(request); - } - - private static async Task GetJsonStringValueAsync(HttpResponseMessage response, string valueName) - { - var source = await response.Content.ReadAsStringAsync(); - var obj = JObject.Parse(source); + [Fact] + public async Task SendAsync_Get_ExplicitHostHeader() + { + var message = new HttpRequestMessage(HttpMethod.Get, "https://httpbin.org/headers"); + message.Headers.Host = message.RequestUri!.Host; + + var response = await RequestAsync(message); - var result = obj.TryGetValue(valueName, out var token); + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - return result - ? token.Value() - : string.Empty; - } + private static async Task RequestAsync(HttpRequestMessage request) + { + var settings = new ProxySettings(); + var proxyClient = new NoProxyClient(settings); + using var proxyClientHandler = new ProxyClientHandler(proxyClient); + proxyClientHandler.CookieContainer = new CookieContainer(); - private static async Task> GetJsonDictionaryValueAsync(HttpResponseMessage response, string valueName) - { - var source = await response.Content.ReadAsStringAsync(); - var obj = JObject.Parse(source); + using var client = new HttpClient(proxyClientHandler); + return await client.SendAsync(request); + } + + private static async Task GetJsonStringValueAsync(HttpResponseMessage response, string valueName) + { + var source = await response.Content.ReadAsStringAsync(); + var obj = JObject.Parse(source); + + var result = obj.TryGetValue(valueName, out var token); + + return result + ? token!.Value()! + : string.Empty; + } + + private static async Task?> GetJsonDictionaryValueAsync(HttpResponseMessage response, string valueName) + { + var source = await response.Content.ReadAsStringAsync(); + var obj = JObject.Parse(source); - var result = obj.TryGetValue(valueName, out var token); + var result = obj.TryGetValue(valueName, out var token); - return result - ? token.ToObject>() - : null; - } + return result + ? token!.ToObject>() + : null; } } diff --git a/RuriLib.Http.Tests/RLHttpClientTests.cs b/RuriLib.Http.Tests/RLHttpClientTests.cs index 62a8dce3e..b44f1f56e 100644 --- a/RuriLib.Http.Tests/RLHttpClientTests.cs +++ b/RuriLib.Http.Tests/RLHttpClientTests.cs @@ -9,265 +9,272 @@ using System.Threading.Tasks; using Xunit; -namespace RuriLib.Http.Tests +namespace RuriLib.Http.Tests; + +public class RLHttpClientTests { - public class RLHttpClientTests + [Fact] + public async Task SendAsync_Get_Headers() { - [Fact] - public async Task SendAsync_Get_Headers() + const string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + + var message = new HttpRequest { - var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/user-agent") + }; - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/user-agent") - }; + message.Headers.Add("User-Agent", userAgent); - message.Headers.Add("User-Agent", userAgent); + var response = await RequestAsync(message); + var userAgentActual = await GetJsonValueAsync(response, "user-agent"); - var response = await RequestAsync(message); - var userAgentActual = await GetJsonValueAsync(response, "user-agent"); + Assert.NotNull(userAgentActual); + Assert.NotEmpty(userAgentActual); + Assert.Equal(userAgent, userAgentActual); + } - Assert.NotEmpty(userAgentActual); - Assert.Equal(userAgent, userAgentActual); - } + [Fact] + public async Task SendAsync_Get_Query() + { + const string key = "key"; + const string value = "value"; - [Fact] - public async Task SendAsync_Get_Query() + var message = new HttpRequest { - var key = "key"; - var value = "value"; + Method = HttpMethod.Get, + Uri = new Uri($"http://httpbin.org/get?{key}={value}") + }; - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri($"http://httpbin.org/get?{key}={value}") - }; + var response = await RequestAsync(message); + var actual = await GetJsonDictionaryValueAsync(response, "args"); - var response = await RequestAsync(message); - var actual = await GetJsonDictionaryValueAsync(response, "args"); + Assert.NotNull(actual); + Assert.True(actual.ContainsKey(key)); + Assert.True(actual.ContainsValue(value)); + } - Assert.True(actual.ContainsKey(key)); - Assert.True(actual.ContainsValue(value)); - } + [Fact] + public async Task SendAsync_Get_UTF8() + { + const string expected = "∮"; - [Fact] - public async Task SendAsync_Get_UTF8() + var message = new HttpRequest { - var expected = "∮"; + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/encoding/utf8") + }; - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/encoding/utf8") - }; + var response = await RequestAsync(message); + Assert.NotNull(response.Content); + var actual = await response.Content.ReadAsStringAsync(); - var response = await RequestAsync(message); - var actual = await response.Content.ReadAsStringAsync(); + Assert.Contains(expected, actual); + } - Assert.Contains(expected, actual); - } + [Fact] + public async Task SendAsync_Get_HTML() + { + const long expectedLength = 3741; + const string contentType = "text/html"; + const string charSet = "utf-8"; - [Fact] - public async Task SendAsync_Get_HTML() + var message = new HttpRequest { - long expectedLength = 3741; - var contentType = "text/html"; - var charSet = "utf-8"; - - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/html") - }; + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/html") + }; - var response = await RequestAsync(message); + var response = await RequestAsync(message); - var content = response.Content; - Assert.NotNull(content); + var content = response.Content; + Assert.NotNull(content); - var headers = response.Content.Headers; - Assert.NotNull(headers); + var headers = content.Headers; + Assert.NotNull(headers); - Assert.NotNull(headers.ContentLength); - Assert.Equal(expectedLength, headers.ContentLength.Value); - Assert.NotNull(headers.ContentType); - Assert.Equal(contentType, headers.ContentType.MediaType); - Assert.Equal(charSet, headers.ContentType.CharSet); - } + Assert.NotNull(headers.ContentLength); + Assert.Equal(expectedLength, headers.ContentLength.Value); + Assert.NotNull(headers.ContentType); + Assert.Equal(contentType, headers.ContentType.MediaType); + Assert.Equal(charSet, headers.ContentType.CharSet); + } - [Fact] - public async Task SendAsync_Get_Delay() + [Fact] + public async Task SendAsync_Get_Delay() + { + var message = new HttpRequest { - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/delay/4") - }; + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/delay/4") + }; - var response = await RequestAsync(message); - var source = response.Content.ReadAsStringAsync(); + var response = await RequestAsync(message); + Assert.NotNull(response.Content); + var source = response.Content.ReadAsStringAsync(); - Assert.NotNull(response); - Assert.NotNull(source); - } + Assert.NotNull(response); + Assert.NotNull(source); + } - [Fact] - public async Task SendAsync_Get_Stream() + [Fact] + public async Task SendAsync_Get_Stream() + { + var message = new HttpRequest { - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/stream/20") - }; - - var response = await RequestAsync(message); - var source = response.Content.ReadAsStringAsync(); + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/stream/20") + }; - Assert.NotNull(response); - Assert.NotNull(source); - } + var response = await RequestAsync(message); + Assert.NotNull(response.Content); + var source = response.Content.ReadAsStringAsync(); - [Fact] - public async Task SendAsync_Get_Gzip() - { - var expected = "gzip, deflate"; + Assert.NotNull(response); + Assert.NotNull(source); + } - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("http://httpbin.org/gzip") - }; + [Fact] + public async Task SendAsync_Get_Gzip() + { + const string expected = "gzip, deflate"; - message.Headers["Accept-Encoding"] = expected; + var message = new HttpRequest + { + Method = HttpMethod.Get, + Uri = new Uri("http://httpbin.org/gzip") + }; - var response = await RequestAsync(message); - var actual = await GetJsonDictionaryValueAsync(response, "headers"); + message.Headers["Accept-Encoding"] = expected; - Assert.Equal(expected, actual["Accept-Encoding"]); - } + var response = await RequestAsync(message); + var actual = await GetJsonDictionaryValueAsync(response, "headers"); - [Fact] - public async Task SendAsync_Get_Cookies() - { - var name = "name"; - var value = "value"; + Assert.NotNull(actual); + Assert.Equal(expected, actual["Accept-Encoding"]); + } - var cookies = new Dictionary(); + [Fact] + public async Task SendAsync_Get_Cookies() + { + const string name = "name"; + const string value = "value"; - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri($"http://httpbin.org/cookies/set?{name}={value}"), - Cookies = cookies - }; + var cookies = new Dictionary(); - var settings = new ProxySettings(); - var proxyClient = new NoProxyClient(settings); - using var client = new RLHttpClient(proxyClient); + var message = new HttpRequest + { + Method = HttpMethod.Get, + Uri = new Uri($"http://httpbin.org/cookies/set?{name}={value}"), + Cookies = cookies + }; + + var settings = new ProxySettings(); + var proxyClient = new NoProxyClient(settings); + using var client = new RLHttpClient(proxyClient); - var response = await client.SendAsync(message); + await client.SendAsync(message); - Assert.Single(cookies); - Assert.Equal(value, cookies[name]); - } + Assert.Single(cookies); + Assert.Equal(value, cookies[name]); + } - [Fact] - public async Task SendAsync_Get_StatusCode() - { - var code = "404"; - var expected = "NotFound"; + [Fact] + public async Task SendAsync_Get_StatusCode() + { + const string code = "404"; + const string expected = "NotFound"; - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri($"http://httpbin.org/status/{code}") - }; + var message = new HttpRequest + { + Method = HttpMethod.Get, + Uri = new Uri($"http://httpbin.org/status/{code}") + }; - var response = await RequestAsync(message); + var response = await RequestAsync(message); - Assert.NotNull(response); - Assert.Equal(expected, response.StatusCode.ToString()); - } + Assert.NotNull(response); + Assert.Equal(expected, response.StatusCode.ToString()); + } - [Fact] - public async Task SendAsync_Get_ExplicitHostHeader() + [Fact] + public async Task SendAsync_Get_ExplicitHostHeader() + { + var message = new HttpRequest { - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("https://httpbin.org/headers") - }; - message.Headers["Host"] = "httpbin.org"; + Method = HttpMethod.Get, + Uri = new Uri("https://httpbin.org/headers") + }; + message.Headers["Host"] = message.Uri.Host; - var response = await RequestAsync(message); + var response = await RequestAsync(message); - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - [Fact] - public async Task SendAsync_GZip_Decompress() + [Fact] + public async Task SendAsync_GZip_Decompress() + { + var message = new HttpRequest { - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("https://nghttp2.org/httpbin/gzip") - }; + Method = HttpMethod.Get, + Uri = new Uri("https://nghttp2.org/httpbin/gzip") + }; - var response = await RequestAsync(message); - var actual = await GetJsonValueAsync(response, "gzipped"); + var response = await RequestAsync(message); + var actual = await GetJsonValueAsync(response, "gzipped"); - Assert.True(actual); - } + Assert.True(actual); + } - [Fact] - public async Task SendAsync_Brotli_Decompress() + [Fact] + public async Task SendAsync_Brotli_Decompress() + { + var message = new HttpRequest { - var message = new HttpRequest - { - Method = HttpMethod.Get, - Uri = new Uri("https://nghttp2.org/httpbin/brotli") - }; + Method = HttpMethod.Get, + Uri = new Uri("https://nghttp2.org/httpbin/brotli") + }; - var response = await RequestAsync(message); - var actual = await GetJsonValueAsync(response, "brotli"); + var response = await RequestAsync(message); + var actual = await GetJsonValueAsync(response, "brotli"); - Assert.True(actual); - } + Assert.True(actual); + } - private static async Task RequestAsync(HttpRequest request) - { - var settings = new ProxySettings(); - var proxyClient = new NoProxyClient(settings); + private static async Task RequestAsync(HttpRequest request) + { + var settings = new ProxySettings(); + var proxyClient = new NoProxyClient(settings); - using var client = new RLHttpClient(proxyClient); - return await client.SendAsync(request); - } + using var client = new RLHttpClient(proxyClient); + return await client.SendAsync(request); + } - private static async Task GetJsonValueAsync(HttpResponse response, string valueName) - { - var source = await response.Content.ReadAsStringAsync(); - var obj = JObject.Parse(source); + private static async Task GetJsonValueAsync(HttpResponse response, string valueName) + { + Assert.NotNull(response.Content); + var source = await response.Content.ReadAsStringAsync(); + var obj = JObject.Parse(source); - var result = obj.TryGetValue(valueName, out var token); + var result = obj.TryGetValue(valueName, out var token); - return result - ? token.Value() - : default; - } + return result + ? token!.Value() + : default; + } - private static async Task> GetJsonDictionaryValueAsync(HttpResponse response, string valueName) - { - var source = await response.Content.ReadAsStringAsync(); - var obj = JObject.Parse(source); + private static async Task?> GetJsonDictionaryValueAsync(HttpResponse response, string valueName) + { + Assert.NotNull(response.Content); + var source = await response.Content.ReadAsStringAsync(); + var obj = JObject.Parse(source); - var result = obj.TryGetValue(valueName, out var token); + var result = obj.TryGetValue(valueName, out var token); - return result - ? token.ToObject>() - : null; - } + return result + ? token!.ToObject>() + : null; } } diff --git a/RuriLib.Http.Tests/RuriLib.Http.Tests.csproj b/RuriLib.Http.Tests/RuriLib.Http.Tests.csproj index 98ea5c279..ea9ddddfb 100644 --- a/RuriLib.Http.Tests/RuriLib.Http.Tests.csproj +++ b/RuriLib.Http.Tests/RuriLib.Http.Tests.csproj @@ -4,6 +4,7 @@ net8.0 false + enable diff --git a/RuriLib.Http/Exceptions/RLHttpException.cs b/RuriLib.Http/Exceptions/RLHttpException.cs new file mode 100644 index 000000000..012ba875e --- /dev/null +++ b/RuriLib.Http/Exceptions/RLHttpException.cs @@ -0,0 +1,14 @@ +using System; + +namespace RuriLib.Http.Exceptions; + +/// +/// An exception that is thrown when an HTTP request fails. +/// +public class RLHttpException : Exception +{ + /// + /// Creates an RLHttpException with a . + /// + public RLHttpException(string message) : base(message) { } +} diff --git a/RuriLib.Http/Extensions/IListExtensions.cs b/RuriLib.Http/Extensions/IListExtensions.cs index 7cc18f7dd..80b84ba99 100644 --- a/RuriLib.Http/Extensions/IListExtensions.cs +++ b/RuriLib.Http/Extensions/IListExtensions.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; -namespace RuriLib.Http.Extensions +namespace RuriLib.Http.Extensions; + +internal static class ListExtensions { - static internal class IListExtensions - { - public static void Add(this IList> list, string key, object value) - => list.Add(new KeyValuePair(key, value.ToString())); - } + public static void Add(this IList> list, string key, object value) + => list.Add(new KeyValuePair(key, value.ToString()!)); } diff --git a/RuriLib.Http/Helpers/ChunkedDecoderOptimized.cs b/RuriLib.Http/Helpers/ChunkedDecoderOptimized.cs index 02f020141..b8387f5e3 100644 --- a/RuriLib.Http/Helpers/ChunkedDecoderOptimized.cs +++ b/RuriLib.Http/Helpers/ChunkedDecoderOptimized.cs @@ -3,117 +3,110 @@ using System.IO; using System.Text; -namespace RuriLib.Http.Helpers +namespace RuriLib.Http.Helpers; + +internal class ChunkedDecoderOptimized : IDisposable { - internal class ChunkedDecoderOptimized : IDisposable - { - private long templength; - private static byte[] CRLF_Bytes = { 13, 10 }; - // private long remaningchunklength; - private bool Isnewchunk = true; - // private AutoResetEvent manualResetEvent = new AutoResetEvent(true); + private long _tempLength; + private static readonly byte[] _crlfBytes = "\r\n"u8.ToArray(); + private bool _isNewChunk = true; - public Stream DecodedStream { get; private set; } + public Stream DecodedStream { get; } = new MemoryStream(1024); - public bool Finished { get; private set; } + public bool Finished { get; private set; } - public ChunkedDecoderOptimized() + internal void Decode(ref ReadOnlySequence buff) => ParseNewChunk(ref buff); + + private void ParseNewChunk(ref ReadOnlySequence buff) + { + if (_isNewChunk) { - DecodedStream = new MemoryStream(1024); + _tempLength = GetChunkLength(ref buff); + _isNewChunk = false; + } + + if (_tempLength == 0 && buff.Length >= 2) + { + Finished = true; + buff = buff.Slice(2);//skip last crlf + return; } - internal void Decode(ref ReadOnlySequence buff) => ParseNewChunk(ref buff); + if (_tempLength == -1) + { + _isNewChunk = true; + return; + } + + if (buff.Length > _tempLength + 2) + { + var chunk = buff.Slice(buff.Start, _tempLength); + WriteToStream(chunk); + _isNewChunk = true; + buff = buff.Slice(chunk.End); + buff = buff.Slice(2); //skip CRLF + + ParseNewChunk(ref buff); + } + } - private void ParseNewChunk(ref ReadOnlySequence buff) + private int GetChunkLength(ref ReadOnlySequence buff) + { + if (buff.IsSingleSegment) { - if (Isnewchunk) + var index = -1; + var span = buff.FirstSpan; + index = span.IndexOf(_crlfBytes); + + if (index == -1) { - templength = GetChunkLength(ref buff); - Isnewchunk = false; + // Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}"); + return -1; } - if (templength == 0 && buff.Length >= 2) + + var line = span[..index]; + var pos = line.IndexOf((byte)';'); + if (pos != -1) { - Finished = true; - buff = buff.Slice(2);//skip last crlf - return; - } - else if (templength == -1) - { - Isnewchunk = true; - return; - } - if (buff.Length > templength + 2) - { - var chunk = buff.Slice(buff.Start, templength); - WritetoStream(chunk); - Isnewchunk = true; - buff = buff.Slice(chunk.End); - buff = buff.Slice(2); //skip CRLF - - ParseNewChunk(ref buff); + line = line[..pos]; } + buff = buff.Slice(index + 2); + return Convert.ToInt32(Encoding.ASCII.GetString(line), 16); } - - private int GetChunkLength(ref ReadOnlySequence buff) + else { - if (buff.IsSingleSegment) + var reader = new SequenceReader(buff); + + if (!reader.TryReadTo(out ReadOnlySpan line, _crlfBytes.AsSpan())) { - var index = -1; - var span = buff.FirstSpan; - index = span.IndexOf(CRLF_Bytes); - if (index != -1) - { - var line = span.Slice(0, index); - var pos = line.IndexOf((byte)';'); - if (pos != -1) - { - line = line.Slice(0, pos); - } - buff = buff.Slice(index + 2); - return Convert.ToInt32(Encoding.ASCII.GetString(line), 16); - } - else - { - // Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}"); - return -1; - } + // Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}"); + return -1; } - else + + var pos = line.IndexOf((byte)';'); + if (pos > 0) { - SequenceReader reader = new SequenceReader(buff); - if (reader.TryReadTo(out ReadOnlySpan line, CRLF_Bytes.AsSpan(), true)) - { - var pos = line.IndexOf((byte)';'); - if (pos > 0) - { - line = line.Slice(0, pos); - } - buff = buff.Slice(reader.Position); - return Convert.ToInt32(Encoding.ASCII.GetString(line), 16); - } - else - { - // Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}"); - return -1; - } + line = line[..pos]; } + buff = buff.Slice(reader.Position); + return Convert.ToInt32(Encoding.ASCII.GetString(line), 16); } + } - private void WritetoStream(ReadOnlySequence buff) + private void WriteToStream(ReadOnlySequence buff) + { + if (buff.IsSingleSegment) { - if (buff.IsSingleSegment) - { - DecodedStream.Write(buff.FirstSpan); - } - else + DecodedStream.Write(buff.FirstSpan); + } + else + { + foreach (var seg in buff) { - foreach (var seg in buff) - { - DecodedStream.Write(seg.Span); - } + DecodedStream.Write(seg.Span); } } - - public void Dispose() => DecodedStream?.Dispose(); } + + public void Dispose() => DecodedStream.Dispose(); } diff --git a/RuriLib.Http/Helpers/ContentHelper.cs b/RuriLib.Http/Helpers/ContentHelper.cs index 69197d7a8..6cc6f503d 100644 --- a/RuriLib.Http/Helpers/ContentHelper.cs +++ b/RuriLib.Http/Helpers/ContentHelper.cs @@ -1,25 +1,24 @@ using System; using System.Linq; -namespace RuriLib.Http.Helpers +namespace RuriLib.Http.Helpers; + +internal static class ContentHelper { - static internal class ContentHelper - { - //https://github.com/dotnet/corefx/blob/3e72ee5971db5d0bd46606fa672969adde29e307/src/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs - private static readonly string[] contentHeaders = new [] - { - "Last-Modified", - "Expires", - "Content-Type", - "Content-Range", - "Content-MD5", - "Content-Location", - "Content-Length", - "Content-Language", - "Content-Encoding", - "Allow" - }; + //https://github.com/dotnet/corefx/blob/3e72ee5971db5d0bd46606fa672969adde29e307/src/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs + private static readonly string[] _contentHeaders = + [ + "Last-Modified", + "Expires", + "Content-Type", + "Content-Range", + "Content-MD5", + "Content-Location", + "Content-Length", + "Content-Language", + "Content-Encoding", + "Allow" + ]; - public static bool IsContentHeader(string name) => contentHeaders.Any(h => h.Equals(name, StringComparison.OrdinalIgnoreCase)); - } + public static bool IsContentHeader(string name) => _contentHeaders.Any(h => h.Equals(name, StringComparison.OrdinalIgnoreCase)); } diff --git a/RuriLib.Http/Helpers/SpanHelpers.cs b/RuriLib.Http/Helpers/SpanHelpers.cs index 3c51dc0bb..f2cba74a8 100644 --- a/RuriLib.Http/Helpers/SpanHelpers.cs +++ b/RuriLib.Http/Helpers/SpanHelpers.cs @@ -1,86 +1,77 @@ using System; -namespace RuriLib.Http.Helpers +namespace RuriLib.Http.Helpers; + +internal static class SpanHelpers { - static internal class SpanHelpers + private static readonly byte[] _crlfBytes = "\r\n"u8.ToArray(); + + public static LineSplitEnumerator SplitLines(this ReadOnlySpan span) { - private static readonly byte[] CRLF_Bytes = { 13, 10 }; - - public static LineSplitEnumerator SplitLines(this Span span) - { - // LineSplitEnumerator is a struct so there is no allocation here - return new LineSplitEnumerator(span); - } + // LineSplitEnumerator is a struct so there is no allocation here + return new LineSplitEnumerator(span); + } - public static LineSplitEnumerator SplitLines(this ReadOnlySpan span) - { - // LineSplitEnumerator is a struct so there is no allocation here - return new LineSplitEnumerator(span); - } + // Must be a ref struct as it contains a ReadOnlySpan + public ref struct LineSplitEnumerator + { + private ReadOnlySpan _span; + public LineSplitEntry Current { get; private set; } - // Must be a ref struct as it contains a ReadOnlySpan - public ref struct LineSplitEnumerator + public LineSplitEnumerator(ReadOnlySpan span) { - private ReadOnlySpan _span; - public LineSplitEntry Current { get; private set; } - - public LineSplitEnumerator(ReadOnlySpan span) - { - _span = span; - Current = default; - } + _span = span; + Current = default; + } - // Needed to be compatible with the foreach operator - public LineSplitEnumerator GetEnumerator() => this; + // Needed to be compatible with the foreach operator + public LineSplitEnumerator GetEnumerator() => this; - public bool MoveNext() - { - var span = _span; + public bool MoveNext() + { + var span = _span; - if (span.Length == 0) // Reach the end of the string - return false; + if (span.Length == 0) // Reach the end of the string + return false; - var index = span.IndexOf(CRLF_Bytes); + var index = span.IndexOf(_crlfBytes); - if (index == -1) // The string is composed of only one line - { - _span = ReadOnlySpan.Empty; // The remaining string is an empty string - Current = new LineSplitEntry(span, ReadOnlySpan.Empty); - return true; - } - else - { - Current = new LineSplitEntry(span.Slice(0, index), span.Slice(index, 2)); - _span = span.Slice(index + 2); - return true; - } + if (index == -1) // The string is composed of only one line + { + _span = ReadOnlySpan.Empty; // The remaining string is an empty string + Current = new LineSplitEntry(span, ReadOnlySpan.Empty); + return true; } + + Current = new LineSplitEntry(span[..index], span.Slice(index, 2)); + _span = span[(index + 2)..]; + return true; } + } - public readonly ref struct LineSplitEntry + public readonly ref struct LineSplitEntry + { + public LineSplitEntry(ReadOnlySpan line, ReadOnlySpan separator) { - public LineSplitEntry(ReadOnlySpan line, ReadOnlySpan separator) - { - Line = line; - Separator = separator; - } - - public ReadOnlySpan Line { get; } - public ReadOnlySpan Separator { get; } + Line = line; + Separator = separator; + } - // This method allow to deconstruct the type, so you can write any of the following code - // foreach (var entry in str.SplitLines()) { _ = entry.Line; } - // foreach (var (line, endOfLine) in str.SplitLines()) { _ = line; } - // https://docs.microsoft.com/en-us/dotnet/csharp/deconstruct#deconstructing-user-defined-types - public void Deconstruct(out ReadOnlySpan line, out ReadOnlySpan separator) - { - line = Line; - separator = Separator; - } + public ReadOnlySpan Line { get; } + public ReadOnlySpan Separator { get; } - // This method allow to implicitly cast the type into a ReadOnlySpan, so you can write the following code - // foreach (ReadOnlySpan entry in str.SplitLines()) - public static implicit operator ReadOnlySpan(LineSplitEntry entry) => entry.Line; + // This method allow to deconstruct the type, so you can write any of the following code + // foreach (var entry in str.SplitLines()) { _ = entry.Line; } + // foreach (var (line, endOfLine) in str.SplitLines()) { _ = line; } + // https://docs.microsoft.com/en-us/dotnet/csharp/deconstruct#deconstructing-user-defined-types + public void Deconstruct(out ReadOnlySpan line, out ReadOnlySpan separator) + { + line = Line; + separator = Separator; } + + // This method allow to implicitly cast the type into a ReadOnlySpan, so you can write the following code + // foreach (ReadOnlySpan entry in str.SplitLines()) + public static implicit operator ReadOnlySpan(LineSplitEntry entry) => entry.Line; } } diff --git a/RuriLib.Http/HttpRequestMessageBuilder.cs b/RuriLib.Http/HttpRequestMessageBuilder.cs index 73ee9707e..5b4e68ea8 100644 --- a/RuriLib.Http/HttpRequestMessageBuilder.cs +++ b/RuriLib.Http/HttpRequestMessageBuilder.cs @@ -5,123 +5,135 @@ using System.Linq; using RuriLib.Http.Extensions; using System; +using RuriLib.Http.Exceptions; -namespace RuriLib.Http +namespace RuriLib.Http; + +internal static class HttpRequestMessageBuilder { - static internal class HttpRequestMessageBuilder - { - private static readonly string newLine = "\r\n"; - private static readonly string[] commaHeaders = new[] { "Accept", "Accept-Encoding" }; + private const string _newLine = "\r\n"; + private static readonly string[] _commaHeaders = ["Accept", "Accept-Encoding"]; - // Builds the first line, for example - // GET /resource HTTP/1.1 - public static string BuildFirstLine(HttpRequestMessage request) + // Builds the first line, for example + // GET /resource HTTP/1.1 + public static string BuildFirstLine(HttpRequestMessage request) + { + if (request.Version >= new Version(2, 0)) { - if (request.Version >= new Version(2, 0)) - throw new Exception($"HTTP/{request.Version.Major}.{request.Version.Minor} not supported yet"); - - return $"{request.Method.Method} {request.RequestUri.PathAndQuery} HTTP/{request.Version}{newLine}"; + throw new RLHttpException($"HTTP/{request.Version.Major}.{request.Version.Minor} not supported yet"); + } + + if (request.RequestUri is null) + { + throw new RLHttpException("Uri cannot be null"); } - // Builds the headers, for example - // Host: example.com - // Connection: Close - public static string BuildHeaders(HttpRequestMessage request, CookieContainer cookies = null) + return $"{request.Method.Method} {request.RequestUri.PathAndQuery} HTTP/{request.Version}{_newLine}"; + } + + // Builds the headers, for example + // Host: example.com + // Connection: Close + public static string BuildHeaders(HttpRequestMessage request, CookieContainer? cookies = null) + { + if (request.RequestUri is null) { - // NOTE: Do not use AppendLine because it appends \n instead of \r\n - // on Unix-like systems. - var sb = new StringBuilder(); - var headers = new List>(); + throw new RLHttpException("Uri cannot be null"); + } + + // NOTE: Do not use AppendLine because it appends \n instead of \r\n + // on Unix-like systems. + var sb = new StringBuilder(); + var headers = new List>(); + + // Add the Host header if not already provided + if (string.IsNullOrEmpty(request.Headers.Host)) + { + headers.Add("Host", request.RequestUri.Host); + } - // Add the Host header if not already provided - if (string.IsNullOrEmpty(request.Headers.Host)) - { - headers.Add("Host", request.RequestUri.Host); - } + // Add the Connection: Close header if none is present + if (request.Headers.Connection.Count == 0) + { + headers.Add("Connection", "Close"); + } - // Add the Connection: Close header if none is present - if (request.Headers.Connection.Count == 0) - { - headers.Add("Connection", "Close"); - } + // Add the non-content headers + foreach (var header in request.Headers) + { + headers.Add(header.Key, GetHeaderValue(header)); + } - // Add the non-content headers - foreach (var header in request.Headers) + // Add the Cookie header + if (cookies != null) + { + var cookiesCollection = cookies.GetCookies(request.RequestUri); + if (cookiesCollection.Count > 0) { - headers.Add(header.Key, GetHeaderValue(header)); - } + var cookieBuilder = new StringBuilder(); - // Add the Cookie header - if (cookies != null) - { - var cookiesCollection = cookies.GetCookies(request.RequestUri); - if (cookiesCollection.Count > 0) + foreach (var cookie in cookiesCollection) { - var cookieBuilder = new StringBuilder(); - - foreach (var cookie in cookiesCollection) - { - cookieBuilder - .Append(cookie) - .Append("; "); - } - - // Remove the last ; and space if not empty - if (cookieBuilder.Length > 2) - { - cookieBuilder.Remove(cookieBuilder.Length - 2, 2); - } - - headers.Add("Cookie", cookieBuilder); + cookieBuilder + .Append(cookie) + .Append("; "); } - } - // Add the content headers - if (request.Content != null) - { - foreach (var header in request.Content.Headers) + // Remove the last ; and space if not empty + if (cookieBuilder.Length > 2) { - headers.Add(header.Key, GetHeaderValue(header)); + cookieBuilder.Remove(cookieBuilder.Length - 2, 2); } - // Add the Content-Length header if not already present - if (!headers.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))) - { - var contentLength = request.Content.Headers.ContentLength; - - if (contentLength.HasValue && contentLength.Value > 0) - { - headers.Add("Content-Length", contentLength); - } - } + headers.Add("Cookie", cookieBuilder); } + } - // Write all non-empty headers to the StringBuilder - foreach (var header in headers.Where(h => !string.IsNullOrEmpty(h.Value))) + // Add the content headers + if (request.Content != null) + { + foreach (var header in request.Content.Headers) { - sb - .Append(header.Key) - .Append(": ") - .Append(header.Value) - .Append(newLine); + headers.Add(header.Key, GetHeaderValue(header)); } - // Write the final blank line after all headers - sb.Append(newLine); + // Add the Content-Length header if not already present + if (!headers.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))) + { + var contentLength = request.Content.Headers.ContentLength; - return sb.ToString(); + if (contentLength is > 0) + { + headers.Add("Content-Length", contentLength); + } + } } - private static string GetHeaderValue(KeyValuePair> header) + // Write all non-empty headers to the StringBuilder + foreach (var header in headers.Where(h => !string.IsNullOrEmpty(h.Value))) { - var values = header.Value.ToArray(); - - return values.Length switch - { - 0 => string.Empty, - 1 => values[0], - _ => string.Join(commaHeaders.Contains(header.Key) ? ", " : " ", values) - }; + sb + .Append(header.Key) + .Append(": ") + .Append(header.Value) + .Append(_newLine); } + + // Write the final blank line after all headers + sb.Append(_newLine); + + return sb.ToString(); + } + + private static string GetHeaderValue(KeyValuePair> header) + { + var values = header.Value.ToArray(); + + return values.Length switch + { + 0 => string.Empty, + 1 => values[0], + _ => string.Join(_commaHeaders.Contains(header.Key) ? ", " : " ", values) + }; } } diff --git a/RuriLib.Http/HttpResponseBuilder.cs b/RuriLib.Http/HttpResponseBuilder.cs index b6062220c..051d0acbd 100644 --- a/RuriLib.Http/HttpResponseBuilder.cs +++ b/RuriLib.Http/HttpResponseBuilder.cs @@ -6,7 +6,6 @@ using System.IO.Compression; using System.Net; using System.Net.Http; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,495 +13,495 @@ using System.Buffers; using System.Runtime.CompilerServices; -namespace RuriLib.Http +namespace RuriLib.Http; + +internal class HttpResponseBuilder { - internal class HttpResponseBuilder - { - private PipeReader reader; - private const string newLine = "\r\n"; - private readonly byte[] CRLF = Encoding.UTF8.GetBytes(newLine); - private static byte[] CRLFCRLF_Bytes = { 13, 10, 13, 10 }; - private HttpResponse response; + private PipeReader? _reader; + private const string _newLine = "\r\n"; + private readonly byte[] _crlf = Encoding.UTF8.GetBytes(_newLine); + private static readonly byte[] _doubleCrlfBytes = "\r\n\r\n"u8.ToArray(); + private HttpResponse? _response; - private Dictionary> contentHeaders; - private int contentLength = -1; + private Dictionary>? _contentHeaders; + private int _contentLength = -1; - internal TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10); + internal TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10); - internal HttpResponseBuilder() - { - // pipe = new Pipe(); - } + /// + /// Builds an HttpResponse by reading a network stream. + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveOptimization)] + internal async Task GetResponseAsync(HttpRequest request, Stream stream, + bool readResponseContent = true, CancellationToken cancellationToken = default) + { + _reader = PipeReader.Create(stream); - /// - /// Builds an HttpResponse by reading a network stream. - /// - [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveOptimization)] - async internal Task GetResponseAsync(HttpRequest request, Stream stream, - bool readResponseContent = true, CancellationToken cancellationToken = default) + _response = new HttpResponse { - reader = PipeReader.Create(stream); - - response = new HttpResponse - { - Request = request - }; + Request = request + }; - contentHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _contentHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); - try - { - await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false); - await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false); + try + { + await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false); + await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false); - if (request.Method != HttpMethod.Head) - { - await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(false); - } - } - catch + if (request.Method != HttpMethod.Head) { - response.Dispose(); - throw; + await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(false); } - - return response; } + catch + { + _response.Dispose(); + throw; + } + + return _response; + } - // Parses the first line, for example - // HTTP/1.1 200 OK - private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default) + // Parses the first line, for example + // HTTP/1.1 200 OK + private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default) + { + var startingLine = string.Empty; + + // Read the first line from the Network Stream + while (true) { - var startingLine = string.Empty; + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); - // Read the first line from the Network Stream - while (true) + var buff = res.Buffer; + var crlfIndex = buff.FirstSpan.IndexOf(_crlf); + if (crlfIndex > -1) { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - int crlfIndex = buff.FirstSpan.IndexOf(CRLF); - if (crlfIndex > -1) + try { - try - { - startingLine = Encoding.UTF8.GetString(res.Buffer.FirstSpan.Slice(0, crlfIndex)); - var fields = startingLine.Split(' '); - response.Version = Version.Parse(fields[0].Trim()[5..]); - response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1]); - buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF - reader.AdvanceTo(buff.End); // advance to the consumed position - break; - } - catch - { - throw new FormatException($"Invalid first line of the HTTP response: {startingLine}"); - } - } - else - { - // the responce is incomplete ex. (HTTP/1.1 200 O) - reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined loop and read more. + startingLine = Encoding.UTF8.GetString(res.Buffer.FirstSpan.Slice(0, crlfIndex)); + var fields = startingLine.Split(' '); + _response!.Version = Version.Parse(fields[0].Trim()[5..]); + _response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1]); + buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF + _reader.AdvanceTo(buff.End); // advance to the consumed position + break; } - if (res.IsCanceled || res.IsCompleted) + catch { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - break; + throw new FormatException($"Invalid first line of the HTTP response: {startingLine}"); } } - } - // Parses the headers - private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default) - { + // the response is incomplete ex. (HTTP/1.1 200 O) + _reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined loop and read more. - while (true) + if (res is { IsCanceled: false, IsCompleted: false }) { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - if (buff.IsSingleSegment) - { - if (ReadHeadersFastPath(ref buff)) - { - reader.AdvanceTo(buff.Start); - break; - } - - } - else - { - if (ReadHeadersSlowerPath(ref buff)) - { - reader.AdvanceTo(buff.Start); - break; - } - } - reader.AdvanceTo(buff.Start, buff.End); // not adding this line might result in infinit loop. - if (res.IsCanceled || res.IsCompleted) - { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - break; - } + continue; } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + break; } + } + // Parses the headers + private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default) + { - /// - /// Reads all Header Lines using For High Perfromace Parsing. - /// - /// Buffered Data From Pipe - private bool ReadHeadersFastPath(ref ReadOnlySequence buff) + while (true) { - int endofheadersindex; + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); - if ((endofheadersindex = buff.FirstSpan.IndexOf(CRLFCRLF_Bytes)) > -1) + var buff = res.Buffer; + if (buff.IsSingleSegment) { - var spanLines = buff.FirstSpan.Slice(0, endofheadersindex + 4); - var Lines = spanLines.SplitLines();// we use spanHelper class here to make a for each loop. - - foreach (var Line in Lines) + if (ReadHeadersFastPath(ref buff)) { - ProcessHeaderLine(Line); + _reader.AdvanceTo(buff.Start); + break; } - buff = buff.Slice(endofheadersindex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe back in the calling method - return true; } - - return false; - } - /// - /// Reads all Header Lines using SequenceReader. - /// - /// Buffered Data From Pipe - private bool ReadHeadersSlowerPath(ref ReadOnlySequence buff) - { - var reader = new SequenceReader(buff); - - while (reader.TryReadTo(out ReadOnlySpan Line, CRLF, true)) + else { - if (Line.Length == 0)// reached last crlf (empty line) + if (ReadHeadersSlowerPath(ref buff)) { - buff = buff.Slice(reader.Position); - return true;// all headers received + _reader.AdvanceTo(buff.Start); + break; } - ProcessHeaderLine(Line); } + _reader.AdvanceTo(buff.Start, buff.End); // not adding this line might result in infinit loop. + + if (res is { IsCanceled: false, IsCompleted: false }) + { + continue; + } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + break; + } + } + - buff = buff.Slice(reader.Position); - return false;// empty line not found need more data + /// + /// Reads all Header Lines using For High Perfromace Parsing. + /// + /// Buffered Data From Pipe + private bool ReadHeadersFastPath(ref ReadOnlySequence buff) + { + int endOfHeadersIndex; + + if ((endOfHeadersIndex = buff.FirstSpan.IndexOf(_doubleCrlfBytes)) <= -1) + { + return false; } + + var spanLines = buff.FirstSpan[..(endOfHeadersIndex + 4)]; + var lines = spanLines.SplitLines(); // we use spanHelper class here to make a for each loop. - private void ProcessHeaderLine(ReadOnlySpan header) + foreach (var line in lines) { - if (header.Length == 0) - { - return; - } - // changed to use span directly to decrease the number of strings allocated (less GC activity) - var separatorPos = header.IndexOf((byte)':'); + ProcessHeaderLine(line); + } + + buff = buff.Slice(endOfHeadersIndex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe back in the calling method + return true; + + } + /// + /// Reads all Header Lines using SequenceReader. + /// + /// Buffered Data From Pipe + private bool ReadHeadersSlowerPath(ref ReadOnlySequence buff) + { + var reader = new SequenceReader(buff); - // If not found, don't do anything because the header is not valid - // Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe the buffer - // is not advanced properly) so it can cause an exception. - if (separatorPos == -1) + while (reader.TryReadTo(out ReadOnlySpan line, _crlf)) + { + if (line.Length == 0) // reached last crlf (empty line) { - return; + buff = buff.Slice(reader.Position); + return true; // all headers received } + ProcessHeaderLine(line); + } - var headerName = Encoding.UTF8.GetString(header.Slice(0, separatorPos)); - var headerValuespan = header.Slice(separatorPos + 1); // skip ':' - var headerValue = headerValuespan[0] == (byte)' ' ? Encoding.UTF8.GetString(headerValuespan.Slice(1)) : Encoding.UTF8.GetString(headerValuespan); // trim the white space + buff = buff.Slice(reader.Position); + return false; // empty line not found need more data + } - // If the header is Set-Cookie, add the cookie - if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) || - headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase)) + private void ProcessHeaderLine(ReadOnlySpan header) + { + if (header.Length == 0) + { + return; + } + + // changed to use span directly to decrease the number of strings allocated (less GC activity) + var separatorPos = header.IndexOf((byte)':'); + + // If not found, don't do anything because the header is not valid + // Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe the buffer + // is not advanced properly) so it can cause an exception. + if (separatorPos == -1) + { + return; + } + + var headerName = Encoding.UTF8.GetString(header[..separatorPos]); + var headerValueSpan = header[(separatorPos + 1)..]; // skip ':' + var headerValue = headerValueSpan[0] == (byte)' ' ? Encoding.UTF8.GetString(headerValueSpan[1..]) : Encoding.UTF8.GetString(headerValueSpan); // trim the white space + + // If the header is Set-Cookie, add the cookie + if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) || + headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase)) + { + SetCookies(headerValue, _response!); + } + // If it's a content header + else if (ContentHelper.IsContentHeader(headerName)) + { + if (_contentHeaders!.TryGetValue(headerName, out var values)) { - SetCookie(response, headerValue); + values.Add(headerValue); } - // If it's a content header - else if (ContentHelper.IsContentHeader(headerName)) + else { - if (contentHeaders.TryGetValue(headerName, out var values)) - { - values.Add(headerValue); - } - else + values = new List { - values = new List - { - headerValue - }; + headerValue + }; - contentHeaders.Add(headerName, values); - } - } - else - { - response.Headers[headerName] = headerValue; + _contentHeaders.Add(headerName, values); } } + else + { + _response!.Headers[headerName] = headerValue; + } + } + + /// + /// Sets a list of comma-separated cookies. + /// + internal static void SetCookies(string value, HttpResponse response) + { + // Cookie values, as per the RFC, cannot contain commas. A comma is used + // to separate multiple cookies in the same Set-Cookie header. So, we split + // the header by commas and set each cookie individually. + foreach (var cookie in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + SetCookie(cookie, response); + } + } - // Sets the value of a cookie - private static void SetCookie(HttpResponse response, string value) + // Sets the value of a cookie + private static void SetCookie(string value, HttpResponse response) + { + if (value.Length == 0) { - if (value.Length == 0) - { - return; - } + return; + } - var endCookiePos = value.IndexOf(';'); - var separatorPos = value.IndexOf('='); + var endCookiePos = value.IndexOf(';'); + var separatorPos = value.IndexOf('='); - if (separatorPos == -1) - { - // Invalid cookie, simply don't add it - return; - } + if (separatorPos == -1) + { + // Invalid cookie, simply don't add it + return; + } - string cookieValue; - var cookieName = value.Substring(0, separatorPos); + var cookieName = value[..separatorPos]; + + var cookieValue = endCookiePos == -1 + ? value[(separatorPos + 1)..] + : value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1); + + response.Request!.Cookies[cookieName] = cookieValue; + } - if (endCookiePos == -1) + private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken cancellationToken = default) + { + // If there are content headers + if (_contentHeaders!.Count != 0) + { + _contentLength = GetContentLength(); + + if (readResponseContent) { - cookieValue = value[(separatorPos + 1)..]; + // Try to get the body and write it to a MemoryStream + var finalResponseStream = await GetMessageBodySource(cancellationToken).ConfigureAwait(false); + + // Rewind the stream and set the content of the response and its headers + finalResponseStream.Seek(0, SeekOrigin.Begin); + _response!.Content = new StreamContent(finalResponseStream); } else { - cookieValue = value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1); + _response!.Content = new ByteArrayContent([]); } + + foreach (var pair in _contentHeaders) + { + _response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value); + } + } + } - response.Request.Cookies[cookieName] = cookieValue; + private Task GetMessageBodySource(CancellationToken cancellationToken) + { + if (_response!.Headers.ContainsKey("Transfer-Encoding")) + { + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetChunkedDecompressedStream(cancellationToken) + : ReceiveMessageBodyChunked(cancellationToken); } - private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken cancellationToken = default) + if (_contentLength > -1) { - // If there are content headers - if (contentHeaders.Count != 0) - { - contentLength = GetContentLength(); + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetContentLengthDecompressedStream(cancellationToken) + : ReceiveContentLength(cancellationToken); + } - if (readResponseContent) - { - // Try to get the body and write it to a MemoryStream - var finaleResponceStream = await GetMessageBodySource(cancellationToken).ConfigureAwait(false); + // handle the case where sever never sent chunked encoding nor content-length headrs (that is not allowed by rfc but whatever) + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetResponseStreamUntilCloseDecompressed(cancellationToken) + : GetResponseStreamUntilClose(cancellationToken); + } - // Rewind the stream and set the content of the response and its headers - finaleResponceStream.Seek(0, SeekOrigin.Begin); - response.Content = new StreamContent(finaleResponceStream); - } - else - { - response.Content = new ByteArrayContent(Array.Empty()); - } - - foreach (var pair in contentHeaders) - { - response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value); - } + + private async Task GetResponseStreamUntilClose(CancellationToken cancellationToken) + { + var responseStream = new MemoryStream(); + while (true) + { + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); + + if (res.IsCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); } - } + var buff = res.Buffer; - private Task GetMessageBodySource(CancellationToken cancellationToken) - { - if (response.Headers.ContainsKey("Transfer-Encoding")) + if (buff.IsSingleSegment) { - if (contentHeaders.ContainsKey("Content-Encoding")) - { - return GetChunkedDecompressedStream(cancellationToken); - } - else - { - return ReceiveMessageBodyChunked(cancellationToken); - } + responseStream.Write(buff.FirstSpan); } - else if (contentLength > -1) + else { - if (contentHeaders.ContainsKey("Content-Encoding")) + foreach (var seg in buff) { - return GetContentLengthDecompressedStream(cancellationToken); - } - else - { - return ReciveContentLength(cancellationToken); - + responseStream.Write(seg.Span); } } - else // handle the case where sever never sent chunked encoding nor content-length headrs (that is not allowed by rfc but whatever) + _reader.AdvanceTo(buff.End); + if (res.IsCompleted || res.Buffer.Length == 0) // here the pipe will be complete if the server closes the connection or sends 0 length byte array { - if (contentHeaders.ContainsKey("Content-Encoding")) - { - return GetResponcestreamUntilCloseDecompressed(cancellationToken); - } - else - { - return GetResponcestreamUntilClose(cancellationToken); - } + break; } } + + return responseStream; + } - - private async Task GetResponcestreamUntilClose(CancellationToken cancellationToken) - { - var responcestream = new MemoryStream(); - while (true) - { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - if (res.IsCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - var buff = res.Buffer; + private async Task GetContentLengthDecompressedStream(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await ReceiveContentLength(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); + return decompressedStream; + } - if (buff.IsSingleSegment) - { - responcestream.Write(buff.FirstSpan); - } - else - { - foreach (var seg in buff) - { - responcestream.Write(seg.Span); - } - } - reader.AdvanceTo(buff.End); - if (res.IsCompleted || res.Buffer.Length == 0)// here the pipe will be complete if the server closes the connection or sends 0 length byte array - { - break; - } - } - - return responcestream; - } + private async Task GetChunkedDecompressedStream(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); + return decompressedStream; + } + private async Task GetResponseStreamUntilCloseDecompressed(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await GetResponseStreamUntilClose(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); + return decompressedStream; + } - private async Task GetContentLengthDecompressedStream(CancellationToken cancellationToken) + private async Task ReceiveContentLength(CancellationToken cancellationToken) + { + var contentLengthStream = new MemoryStream(_contentLength == -1 ? 0 : _contentLength); + if (_contentLength == 0) { - using var compressedStream = GetZipStream(await ReciveContentLength(cancellationToken).ConfigureAwait(false)); - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); - return decompressedStream; + return contentLengthStream; } - private async Task GetChunkedDecompressedStream(CancellationToken cancellationToken) - { - using var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellationToken).ConfigureAwait(false)); - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); - return decompressedStream; - } - private async Task GetResponcestreamUntilCloseDecompressed(CancellationToken cancellationToken) + while (true) { - using var compressedStream = GetZipStream(await GetResponcestreamUntilClose(cancellationToken).ConfigureAwait(false)); - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); - return decompressedStream; - } + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); - private async Task ReciveContentLength(CancellationToken cancellationToken) - { - var contentlenghtStream = new MemoryStream(contentLength == -1 ? 0 : contentLength); - if (contentLength == 0) + var buff = res.Buffer; + if (buff.IsSingleSegment) { - return contentlenghtStream; + contentLengthStream.Write(buff.FirstSpan); } - - while (true) + else { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - if (buff.IsSingleSegment) + foreach (var seg in buff) { - contentlenghtStream.Write(buff.FirstSpan); - } - else - { - foreach (var seg in buff) - { - contentlenghtStream.Write(seg.Span); - } - } - reader.AdvanceTo(buff.End); - if (contentlenghtStream.Length >= contentLength) - { - return contentlenghtStream; - } - if (res.IsCanceled || res.IsCompleted) - { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - break; + contentLengthStream.Write(seg.Span); } } - return contentlenghtStream; - } + _reader.AdvanceTo(buff.End); + if (contentLengthStream.Length >= _contentLength) + { + return contentLengthStream; + } - private int GetContentLength() - { - if (contentHeaders.TryGetValue("Content-Length", out var values)) + if (res is { IsCanceled: false, IsCompleted: false }) { - if (int.TryParse(values[0], out var length)) - { - return length; - } + continue; } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + break; + } + return contentLengthStream; + } + private int GetContentLength() + { + if (!_contentHeaders!.TryGetValue("Content-Length", out var values)) + { return -1; } - - private string GetContentEncoding() + + if (int.TryParse(values[0], out var length)) { - var encoding = ""; + return length; + } - if (contentHeaders.TryGetValue("Content-Encoding", out var values)) - { - encoding = values[0]; - } + return -1; + } + + private string GetContentEncoding() + { + var encoding = ""; - return encoding; + if (_contentHeaders!.TryGetValue("Content-Encoding", out var values)) + { + encoding = values[0]; } - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private async Task ReceiveMessageBodyChunked(CancellationToken cancellationToken) + return encoding; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private async Task ReceiveMessageBodyChunked(CancellationToken cancellationToken) + { + var chunkedDecoder = new ChunkedDecoderOptimized(); + while (true) { - var chunkedDecoder = new ChunkedDecoderOptimized(); - while (true) + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); + + var buff = res.Buffer; + chunkedDecoder.Decode(ref buff); + _reader.AdvanceTo(buff.Start, buff.End); + if (chunkedDecoder.Finished) { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + return chunkedDecoder.DecodedStream; + } - var buff = res.Buffer; - chunkedDecoder.Decode(ref buff); - reader.AdvanceTo(buff.Start, buff.End); - if (chunkedDecoder.Finished) - { - return chunkedDecoder.DecodedStream; - } - if (res.IsCanceled || res.IsCompleted) - { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - break; - } + if (res is { IsCanceled: false, IsCompleted: false }) + { + continue; } - return chunkedDecoder.DecodedStream; + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + break; } + return chunkedDecoder.DecodedStream; + } - private Stream GetZipStream(Stream stream) + private Stream GetZipStream(Stream stream) + { + var contentEncoding = GetContentEncoding().ToLower(); + stream.Seek(0, SeekOrigin.Begin); + return contentEncoding switch { - var contentEncoding = GetContentEncoding().ToLower(); - stream.Seek(0, SeekOrigin.Begin); - return contentEncoding switch - { - "gzip" => new GZipStream(stream, CompressionMode.Decompress, false), - "deflate" => new DeflateStream(stream, CompressionMode.Decompress, false), - "br" => new BrotliStream(stream, CompressionMode.Decompress, false), - "utf-8" => stream, - _ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding format"), - }; - } + "gzip" => new GZipStream(stream, CompressionMode.Decompress, false), + "deflate" => new DeflateStream(stream, CompressionMode.Decompress, false), + "br" => new BrotliStream(stream, CompressionMode.Decompress, false), + "utf-8" => stream, + _ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding format"), + }; } } diff --git a/RuriLib.Http/HttpResponseMessageBuilder.cs b/RuriLib.Http/HttpResponseMessageBuilder.cs index 074abafd7..396187503 100644 --- a/RuriLib.Http/HttpResponseMessageBuilder.cs +++ b/RuriLib.Http/HttpResponseMessageBuilder.cs @@ -11,563 +11,532 @@ using System.IO.Pipelines; using System.Buffers; -namespace RuriLib.Http -{ - internal class HttpResponseMessageBuilder - { - private PipeReader reader; - private const string newLine = "\r\n"; - private readonly byte[] CRLF = Encoding.UTF8.GetBytes(newLine); - private static byte[] CRLFCRLF_Bytes = { 13, 10, 13, 10 }; - - private int contentLength = -1; +namespace RuriLib.Http; - //private NetworkStream networkStream; - //private Stream commonStream; +internal class HttpResponseMessageBuilder +{ + private PipeReader? _reader; + private const string _newLine = "\r\n"; + private readonly byte[] _crlf = Encoding.UTF8.GetBytes(_newLine); + private static readonly byte[] _doubleCrlfBytes = "\r\n\r\n"u8.ToArray(); - private HttpResponseMessage response; - private Dictionary> contentHeaders; + private int _contentLength = -1; - private readonly CookieContainer cookies; - private readonly Uri uri; + private HttpResponseMessage? _response; + private Dictionary>? _contentHeaders; - // private readonly ReceiveHelper receiveHelper; + private readonly CookieContainer _cookies; + private readonly Uri? _uri; - public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10); - public HttpResponseMessageBuilder(int bufferSize, CookieContainer cookies = null, Uri uri = null) - { - // this.bufferSize = bufferSize; - this.cookies = cookies; - this.uri = uri; + public HttpResponseMessageBuilder(int bufferSize, CookieContainer? cookies = null, Uri? uri = null) + { + // this.bufferSize = bufferSize; + this._cookies = cookies ?? new CookieContainer(); + this._uri = uri; - // receiveHelper = new ReceiveHelper(bufferSize); - } + // receiveHelper = new ReceiveHelper(bufferSize); + } - public async Task GetResponseAsync(HttpRequestMessage request, Stream stream, - bool readResponseContent = true, CancellationToken cancellationToken = default) { + public async Task GetResponseAsync(HttpRequestMessage request, Stream stream, + bool readResponseContent = true, CancellationToken cancellationToken = default) { - reader = PipeReader.Create(stream); + _reader = PipeReader.Create(stream); - response = new HttpResponseMessage(); - contentHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _response = new HttpResponseMessage(); + _contentHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); - response.RequestMessage = request; + _response.RequestMessage = request; - try - { - await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false); - await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false); - await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(false); - } - catch - { - response.Dispose(); - throw; - } - - return response; + try + { + await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false); + await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false); + await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(false); } - - // Parses the first line, for example - // HTTP/1.1 200 OK - private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default) + catch { - var startingLine = string.Empty; + _response.Dispose(); + throw; + } - // Read the first line from the Network Stream - while (true) - { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + return _response; + } + + // Parses the first line, for example + // HTTP/1.1 200 OK + private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default) + { + var startingLine = string.Empty; + + // Read the first line from the Network Stream + while (true) + { + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); - var buff = res.Buffer; - int crlfIndex = buff.FirstSpan.IndexOf(CRLF); - if (crlfIndex > -1) - { - try - { - startingLine = Encoding.UTF8.GetString(buff.FirstSpan.Slice(0, crlfIndex)); - var fields = startingLine.Split(' '); - response.Version = Version.Parse(fields[0].Trim()[5..]); - response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1]); - buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF - reader.AdvanceTo(buff.End); // advance to the consumed position - break; - } - catch - { - throw new FormatException($"Invalid first line of the HTTP response: {startingLine}"); - } - } - else + var buff = res.Buffer; + var crlfIndex = buff.FirstSpan.IndexOf(_crlf); + if (crlfIndex > -1) + { + try { - // the responce is incomplete ex. (HTTP/1.1 200 O) - reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined loop and read more. + startingLine = Encoding.UTF8.GetString(buff.FirstSpan[..crlfIndex]); + var fields = startingLine.Split(' '); + _response!.Version = Version.Parse(fields[0].Trim()[5..]); + _response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1]); + buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF + _reader.AdvanceTo(buff.End); // advance to the consumed position + break; } - if (res.IsCanceled || res.IsCompleted) + catch { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); + throw new FormatException($"Invalid first line of the HTTP response: {startingLine}"); } } + // the response is incomplete ex. (HTTP/1.1 200 O) + _reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined loop and read more. + if (res is { IsCanceled: false, IsCompleted: false }) + { + continue; + } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); } - // Parses the headers - private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default) + + } + + // Parses the headers + private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default) + { + while (true) { - while (true) - { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - if (buff.IsSingleSegment) - { - if (ReadHeadersFastPath(ref buff)) - { - reader.AdvanceTo(buff.Start); - break; - } + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); - } - else - { - if (ReadHeadersSlowerPath(ref buff)) - { - reader.AdvanceTo(buff.Start); - break; - } - } - reader.AdvanceTo(buff.Start, buff.End);// not adding ghis linw might result in infinit loop - if (res.IsCanceled || res.IsCompleted) + var buff = res.Buffer; + if (buff.IsSingleSegment) + { + if (ReadHeadersFastPath(ref buff)) { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); + _reader.AdvanceTo(buff.Start); + break; } } - } - /// - /// Reads all Header Lines using For High Perfromace Parsing. - /// - /// Buffered Data From Pipe - private bool ReadHeadersFastPath(ref ReadOnlySequence buff) - { - int endofheadersindex; - if ((endofheadersindex = buff.FirstSpan.IndexOf(CRLFCRLF_Bytes)) > -1) + else { - var spanLines = buff.FirstSpan.Slice(0, endofheadersindex + 4); - var Lines = spanLines.SplitLines();// we use spanHelper class here to make a for each loop. - foreach (var Line in Lines) + if (ReadHeadersSlowerPath(ref buff)) { - - ProcessHeaderLine(Line); + _reader.AdvanceTo(buff.Start); + break; } - - buff = buff.Slice(endofheadersindex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe back in the calling method - return true; } + _reader.AdvanceTo(buff.Start, buff.End); // not adding this line might result in an infinite loop + + if (res is { IsCanceled: false, IsCompleted: false }) + { + continue; + } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + + } + } + /// + /// Reads all Header Lines using For High Perfromace Parsing. + /// + /// Buffered Data From Pipe + private bool ReadHeadersFastPath(ref ReadOnlySequence buff) + { + int endOfHeadersIndex; + + if ((endOfHeadersIndex = buff.FirstSpan.IndexOf(_doubleCrlfBytes)) <= -1) + { return false; } - /// - /// Reads all Header Lines using SequenceReader. - /// - /// Buffered Data From Pipe - private bool ReadHeadersSlowerPath(ref ReadOnlySequence buff) + + var spanLines = buff.FirstSpan[..(endOfHeadersIndex + 4)]; + var lines = spanLines.SplitLines(); // we use spanHelper class here to make a for each loop. + foreach (var line in lines) { - var reader = new SequenceReader(buff); + + ProcessHeaderLine(line); + } - while (reader.TryReadTo(out ReadOnlySpan Line, CRLF, true)) + buff = buff.Slice(endOfHeadersIndex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe back in the calling method + return true; + } + /// + /// Reads all Header Lines using SequenceReader. + /// + /// Buffered Data From Pipe + private bool ReadHeadersSlowerPath(ref ReadOnlySequence buff) + { + var reader = new SequenceReader(buff); + + while (reader.TryReadTo(out ReadOnlySpan line, _crlf)) + { + if (line.Length == 0) // reached last crlf (empty line) { - if (Line.Length == 0)// reached last crlf (empty line) - { - buff = buff.Slice(reader.Position); - return true;// all headers received - } - ProcessHeaderLine(Line); + buff = buff.Slice(reader.Position); + return true; // all headers received } - buff = buff.Slice(reader.Position); - return false;// empty line not found need more data + ProcessHeaderLine(line); } + buff = buff.Slice(reader.Position); + return false; // empty line not found need more data + } - private void ProcessHeaderLine(ReadOnlySpan header) + private void ProcessHeaderLine(ReadOnlySpan header) + { + if (header.Length == 0) { - if (header.Length == 0) - { - return; - } + return; + } - // changed to use span directly to decrease the number of strings allocated (less GC activity) - var separatorPos = header.IndexOf((byte)':'); + // changed to use span directly to decrease the number of strings allocated (less GC activity) + var separatorPos = header.IndexOf((byte)':'); - // If not found, don't do anything because the header is not valid - // Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe the buffer - // is not advanced properly) so it can cause an exception. - if (separatorPos == -1) - { - return; - } + // If not found, don't do anything because the header is not valid + // Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe the buffer + // is not advanced properly) so it can cause an exception. + if (separatorPos == -1) + { + return; + } - var headerName = Encoding.UTF8.GetString(header.Slice(0, separatorPos)); - var headerValuespan = header.Slice(separatorPos + 1); // skip ':' - var headerValue = headerValuespan[0] == (byte)' ' ? Encoding.UTF8.GetString(headerValuespan.Slice(1)) : Encoding.UTF8.GetString(headerValuespan); // trim the wight space + var headerName = Encoding.UTF8.GetString(header[..separatorPos]); + var headerValueSpan = header[(separatorPos + 1)..]; // skip ':' + var headerValue = headerValueSpan[0] == (byte)' ' + ? Encoding.UTF8.GetString(headerValueSpan[1..]) + : Encoding.UTF8.GetString(headerValueSpan); // trim the white space - // If the header is Set-Cookie, add the cookie - if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) || - headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase)) + // If the header is Set-Cookie, add the cookie + if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) || + headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase)) + { + SetCookies(headerValue, _cookies, _uri!); + } + // If it's a content header + else if (ContentHelper.IsContentHeader(headerName)) + { + if (_contentHeaders!.TryGetValue(headerName, out var values)) { - SetCookies(headerValue, cookies, uri); + values.Add(headerValue); } - // If it's a content header - else if (ContentHelper.IsContentHeader(headerName)) + else { - if (contentHeaders.TryGetValue(headerName, out var values)) - { - values.Add(headerValue); - } - else + values = new List { - values = new List - { - headerValue - }; + headerValue + }; - contentHeaders.Add(headerName, values); - } - } - else - { - response.Headers.TryAddWithoutValidation(headerName, headerValue); + _contentHeaders.Add(headerName, values); } } + else + { + _response!.Headers.TryAddWithoutValidation(headerName, headerValue); + } + } - /// - /// Sets a list of comma-separated cookies. - /// - internal static void SetCookies(string value, CookieContainer cookies, Uri uri) + /// + /// Sets a list of comma-separated cookies. + /// + internal static void SetCookies(string value, CookieContainer cookies, Uri uri) + { + // Cookie values, as per the RFC, cannot contain commas. A comma is used + // to separate multiple cookies in the same Set-Cookie header. So, we split + // the header by commas and set each cookie individually. + foreach (var cookie in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - // Cookie values, as per the RFC, cannot contain commas. A comma is used - // to separate multiple cookies in the same Set-Cookie header. So, we split - // the header by commas and set each cookie individually. - foreach (var cookie in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - SetCookie(cookie, cookies, uri); - } + SetCookie(cookie, cookies, uri); } + } - /// - /// Sets a single cookie. - /// - internal static void SetCookie(string value, CookieContainer cookies, Uri uri) + /// + /// Sets a single cookie. + /// + internal static void SetCookie(string value, CookieContainer cookies, Uri uri) + { + if (value.Length == 0) { - if (value.Length == 0) - { - return; - } + return; + } - var endCookiePos = value.IndexOf(';'); - var separatorPos = value.IndexOf('='); + var endCookiePos = value.IndexOf(';'); + var separatorPos = value.IndexOf('='); - if (separatorPos == -1) - { - // Invalid cookie, simply don't add it - return; - } + if (separatorPos == -1) + { + // Invalid cookie, simply don't add it + return; + } - string cookieValue; - var cookieName = value[..separatorPos]; + string cookieValue; + var cookieName = value[..separatorPos]; - if (endCookiePos == -1) - { - cookieValue = value[(separatorPos + 1)..]; - } - else + if (endCookiePos == -1) + { + cookieValue = value[(separatorPos + 1)..]; + } + else + { + cookieValue = value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1); + + var expiresPos = value.IndexOf("expires=", StringComparison.OrdinalIgnoreCase); + + if (expiresPos != -1) { - cookieValue = value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1); + var endExpiresPos = value.IndexOf(';', expiresPos); - #region Get Expiration Time + expiresPos += 8; - var expiresPos = value.IndexOf("expires=", StringComparison.OrdinalIgnoreCase); + var expiresStr = endExpiresPos == -1 ? value[expiresPos..] : value[expiresPos..endExpiresPos]; - if (expiresPos != -1) + if (DateTime.TryParse(expiresStr, out var expires) && + expires < DateTime.Now) { - string expiresStr; - var endExpiresPos = value.IndexOf(';', expiresPos); - - expiresPos += 8; - - if (endExpiresPos == -1) - { - expiresStr = value[expiresPos..]; - } - else - { - expiresStr = value[expiresPos..endExpiresPos]; - } - - if (DateTime.TryParse(expiresStr, out var expires) && - expires < DateTime.Now) + var collection = cookies.GetCookies(uri); + + if (collection[cookieName] is not null) { - var collection = cookies.GetCookies(uri); - if (collection[cookieName] != null) - collection[cookieName].Expired = true; + collection[cookieName]!.Expired = true; } } + } + } - #endregion + if (cookieValue.Length == 0 || + cookieValue.Equals("deleted", StringComparison.OrdinalIgnoreCase)) + { + var collection = cookies.GetCookies(uri); + + if (collection[cookieName] is not null) + { + collection[cookieName]!.Expired = true; } + } + else + { + cookies.Add(new Cookie(cookieName, cookieValue, "/", uri.Host)); + } + } - if (cookieValue.Length == 0 || - cookieValue.Equals("deleted", StringComparison.OrdinalIgnoreCase)) + private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken cancellationToken = default) + { + // If there are content headers + if (_contentHeaders!.Count != 0) + { + _contentLength = GetContentLength(); + + if (readResponseContent) { - var collection = cookies.GetCookies(uri); - if (collection[cookieName] != null) - collection[cookieName].Expired = true; + // Try to get the body and write it to a MemoryStream + var finaleResponseStream = await GetMessageBodySource(cancellationToken).ConfigureAwait(false); + + // Rewind the stream and set the content of the response and its headers + finaleResponseStream.Seek(0, SeekOrigin.Begin); + _response!.Content = new StreamContent(finaleResponseStream); } else { - cookies.Add(new Cookie(cookieName, cookieValue, "/", uri.Host)); + _response!.Content = new ByteArrayContent([]); } - } - // TODO: Make this async (need to refactor the mess below) - private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken cancellationToken = default) - { - // If there are content headers - if (contentHeaders.Count != 0) + foreach (var pair in _contentHeaders) { - contentLength = GetContentLength(); - - if (readResponseContent) - { - // Try to get the body and write it to a MemoryStream - var finaleResponceStream = await GetMessageBodySource(cancellationToken).ConfigureAwait(false); + _response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value); + } + } + } - // Rewind the stream and set the content of the response and its headers - finaleResponceStream.Seek(0, SeekOrigin.Begin); - response.Content = new StreamContent(finaleResponceStream); - } - else - { - response.Content = new ByteArrayContent(Array.Empty()); - } + private Task GetMessageBodySource(CancellationToken cancellationToken) + { + if (_response!.Headers.Contains("Transfer-Encoding")) + { + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetChunkedDecompressedStream(cancellationToken) + : ReceiveMessageBodyChunked(cancellationToken); + } - foreach (var pair in contentHeaders) - { - response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value); - } - } + if (_contentLength > -1) + { + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetContentLengthDecompressedStream(cancellationToken) + : ReceiveContentLength(cancellationToken); } - private Task GetMessageBodySource(CancellationToken cancellationToken) + // handle the case where sever never sent chunked encoding nor content-length headrs (that is not allowed by rfc but whatever) + return _contentHeaders!.ContainsKey("Content-Encoding") + ? GetResponseStreamUntilCloseDecompressed(cancellationToken) + : GetResponseStreamUntilClose(cancellationToken); + + } + private async Task GetResponseStreamUntilClose(CancellationToken cancellationToken) + { + var responseStream = new MemoryStream(); + while (true) { - if (response.Headers.Contains("Transfer-Encoding")) + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); + + if (res.IsCanceled) { - if (contentHeaders.ContainsKey("Content-Encoding")) - { - return GetChunkedDecompressedStream(cancellationToken); - } - else - { - return ReceiveMessageBodyChunked(cancellationToken); - } + cancellationToken.ThrowIfCancellationRequested(); } - else if (contentLength > -1) - { - if (contentHeaders.ContainsKey("Content-Encoding")) - { - return GetContentLengthDecompressedStream(cancellationToken); - } - else - { - return ReciveContentLength(cancellationToken); + var buff = res.Buffer; - } - } - else // handle the case where sever never sent chunked encoding nor content-length headrs (that is not allowed by rfc but whatever) + if (buff.IsSingleSegment) { - if (contentHeaders.ContainsKey("Content-Encoding")) - { - return GetResponcestreamUntilCloseDecompressed(cancellationToken); - } - else - { - return GetResponcestreamUntilClose(cancellationToken); - } + responseStream.Write(buff.FirstSpan); } - - } - private async Task GetResponcestreamUntilClose(CancellationToken cancellationToken) - { - var responcestream = new MemoryStream(); - while (true) + else { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - if (res.IsCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - var buff = res.Buffer; - - if (buff.IsSingleSegment) - { - responcestream.Write(buff.FirstSpan); - } - else - { - foreach (var seg in buff) - { - responcestream.Write(seg.Span); - } - } - reader.AdvanceTo(buff.End); - if (res.IsCompleted || res.Buffer.Length == 0)// here the pipe will be complete if the server closes the connection or sends 0 length byte array + foreach (var seg in buff) { - break; + responseStream.Write(seg.Span); } } - return responcestream; - } - private async Task GetContentLengthDecompressedStream(CancellationToken cancellationToken) - { - using (var compressedStream = GetZipStream(await ReciveContentLength(cancellationToken).ConfigureAwait(false))) + _reader.AdvanceTo(buff.End); + if (res.IsCompleted || res.Buffer.Length == 0)// here the pipe will be complete if the server closes the connection or sends 0 length byte array { - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken); - return decompressedStream; + break; } } + return responseStream; + } + private async Task GetContentLengthDecompressedStream(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await ReceiveContentLength(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken); + return decompressedStream; + } - private async Task GetChunkedDecompressedStream(CancellationToken cancellationToken) - { - using (var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellationToken).ConfigureAwait(false))) - { - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); - return decompressedStream; - } - } - private async Task GetResponcestreamUntilCloseDecompressed(CancellationToken cancellationToken) + private async Task GetChunkedDecompressedStream(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); + return decompressedStream; + } + private async Task GetResponseStreamUntilCloseDecompressed(CancellationToken cancellationToken) + { + await using var compressedStream = GetZipStream(await GetResponseStreamUntilClose(cancellationToken).ConfigureAwait(false)); + var decompressedStream = new MemoryStream(); + await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); + return decompressedStream; + } + private async Task ReceiveContentLength(CancellationToken cancellationToken) + { + var contentLengthStream = new MemoryStream(_contentLength); + + if (_contentLength == 0) { - using var compressedStream = GetZipStream(await GetResponcestreamUntilClose(cancellationToken).ConfigureAwait(false)); - var decompressedStream = new MemoryStream(); - await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureAwait(false); - return decompressedStream; + return contentLengthStream; } - private async Task ReciveContentLength(CancellationToken cancellationToken) + + while (true) { - MemoryStream contentlenghtStream = new MemoryStream(contentLength); - if (contentLength == 0) + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); + + var buff = res.Buffer; + if (buff.IsSingleSegment) { - return contentlenghtStream; + contentLengthStream.Write(buff.FirstSpan); } - - while (true) + else { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - if (buff.IsSingleSegment) - { - contentlenghtStream.Write(buff.FirstSpan); - } - else - { - foreach (var seg in buff) - { - contentlenghtStream.Write(seg.Span); - } - } - reader.AdvanceTo(buff.End); - - if (contentlenghtStream.Length >= contentLength) + foreach (var seg in buff) { - return contentlenghtStream; + contentLengthStream.Write(seg.Span); } - - if (res.IsCanceled || res.IsCompleted) - { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - } - } - } - - + _reader.AdvanceTo(buff.End); + if (contentLengthStream.Length >= _contentLength) + { + return contentLengthStream; + } - private int GetContentLength() - { - if (contentHeaders.TryGetValue("Content-Length", out var values)) + if (res is { IsCanceled: false, IsCompleted: false }) { - if (int.TryParse(values[0], out var length)) - { - return length; - } + continue; } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private int GetContentLength() + { + if (!_contentHeaders!.TryGetValue("Content-Length", out var values)) + { return -1; } - - private string GetContentEncoding() + + if (int.TryParse(values[0], out var length)) { - var encoding = ""; - - if (contentHeaders.TryGetValue("Content-Encoding", out var values)) - { - encoding = values[0]; - } - - return encoding; + return length; } + return -1; + } + private string GetContentEncoding() + { + var encoding = ""; - - private async Task ReceiveMessageBodyChunked(CancellationToken cancellationToken) + if (_contentHeaders!.TryGetValue("Content-Encoding", out var values)) { - var chunkedDecoder = new ChunkedDecoderOptimized(); - while (true) - { - var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var buff = res.Buffer; - chunkedDecoder.Decode(ref buff); - reader.AdvanceTo(buff.Start, buff.End); - if (chunkedDecoder.Finished) - { - return chunkedDecoder.DecodedStream; - } - if (res.IsCanceled || res.IsCompleted) - { - reader.Complete(); - cancellationToken.ThrowIfCancellationRequested(); - } - } + encoding = values[0]; } + return encoding; + } + + private async Task ReceiveMessageBodyChunked(CancellationToken cancellationToken) + { + var chunkedDecoder = new ChunkedDecoderOptimized(); + while (true) + { + var res = await _reader!.ReadAsync(cancellationToken).ConfigureAwait(false); + var buff = res.Buffer; + chunkedDecoder.Decode(ref buff); + _reader.AdvanceTo(buff.Start, buff.End); + if (chunkedDecoder.Finished) + { + return chunkedDecoder.DecodedStream; + } - - - private Stream GetZipStream(Stream stream) - { - var contentEncoding = GetContentEncoding().ToLower(); - stream.Seek(0, SeekOrigin.Begin); - return contentEncoding switch + if (res is { IsCanceled: false, IsCompleted: false }) { - "gzip" => new GZipStream(stream, CompressionMode.Decompress, false), - "deflate" => new DeflateStream(stream, CompressionMode.Decompress, false), - "br" => new BrotliStream(stream, CompressionMode.Decompress, false), - _ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding format"), - }; + continue; + } + + await _reader.CompleteAsync(); + cancellationToken.ThrowIfCancellationRequested(); } + } - - - + private Stream GetZipStream(Stream stream) + { + var contentEncoding = GetContentEncoding().ToLower(); + stream.Seek(0, SeekOrigin.Begin); + return contentEncoding switch + { + "gzip" => new GZipStream(stream, CompressionMode.Decompress, false), + "deflate" => new DeflateStream(stream, CompressionMode.Decompress, false), + "br" => new BrotliStream(stream, CompressionMode.Decompress, false), + _ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding format"), + }; } } diff --git a/RuriLib.Http/Models/HttpRequest.cs b/RuriLib.Http/Models/HttpRequest.cs index 0985f2011..16f7cd64b 100644 --- a/RuriLib.Http/Models/HttpRequest.cs +++ b/RuriLib.Http/Models/HttpRequest.cs @@ -7,196 +7,209 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using RuriLib.Http.Exceptions; -namespace RuriLib.Http.Models +namespace RuriLib.Http.Models; + +/// +/// An HTTP request that can be sent using a . +/// +public class HttpRequest : IDisposable { /// - /// An HTTP request that can be sent using a . + /// Whether to write the absolute URI in the first line of the request instead of + /// the relative path (e.g. https://example.com/abc instead of /abc) + /// + public bool AbsoluteUriInFirstLine { get; set; } = false; + + /// + /// The HTTP version to use. + /// + public Version Version { get; set; } = new(1, 1); + + /// + /// The HTTP method to use. + /// + public HttpMethod Method { get; set; } = HttpMethod.Get; + + /// + /// The URI of the remote resource. /// - public class HttpRequest : IDisposable + public Uri? Uri { get; set; } + + /// + /// The cookies to send inside the Cookie header of this request. + /// + public Dictionary Cookies { get; set; } = new(); + + /// + /// The headers of this request. + /// + public Dictionary Headers { get; set; } = new(); + + /// + /// The content of this request. + /// + public HttpContent? Content { get; set; } + + /// + /// Gets the raw bytes that will be sent on the network stream. + /// + /// The token to cancel the operation + public async Task GetBytesAsync(CancellationToken cancellationToken = default) { - /// - /// Whether to write the absolute URI in the first line of the request instead of - /// the relative path (e.g. https://example.com/abc instead of /abc) - /// - public bool AbsoluteUriInFirstLine { get; set; } = false; - - /// - /// The HTTP version to use. - /// - public Version Version { get; set; } = new(1, 1); - - /// - /// The HTTP method to use. - /// - public HttpMethod Method { get; set; } = HttpMethod.Get; - - /// - /// The URI of the remote resource. - /// - public Uri Uri { get; set; } - - /// - /// The cookies to send inside the Cookie header of this request. - /// - public Dictionary Cookies { get; set; } = new(); - - /// - /// The headers of this request. - /// - public Dictionary Headers { get; set; } = new(); - - /// - /// The content of this request. - /// - public HttpContent Content { get; set; } - - /// - /// Gets the raw bytes that will be sent on the network stream. - /// - /// The token to cancel the operation - public async Task GetBytesAsync(CancellationToken cancellationToken = default) + using var ms = new MemoryStream(); + ms.Write(Encoding.ASCII.GetBytes(BuildFirstLine())); + ms.Write(Encoding.ASCII.GetBytes(BuildHeaders())); + + if (Content != null) { - using var ms = new MemoryStream(); - ms.Write(Encoding.ASCII.GetBytes(BuildFirstLine())); - ms.Write(Encoding.ASCII.GetBytes(BuildHeaders())); + ms.Write(await Content.ReadAsByteArrayAsync(cancellationToken)); + } - if (Content != null) - { - ms.Write(await Content.ReadAsByteArrayAsync(cancellationToken)); - } + return ms.ToArray(); + } - return ms.ToArray(); - } + private const string _newLine = "\r\n"; - private static readonly string newLine = "\r\n"; + /// + /// Safely adds a header to the dictionary. + /// + public void AddHeader(string name, string value) + { + // Make sure Host is written properly otherwise it won't get picked up below + if (name.Equals("Host", StringComparison.OrdinalIgnoreCase)) + { + Headers["Host"] = value; + } + else + { + Headers[name] = value; + } + } - /// - /// Safely adds a header to the dictionary. - /// - public void AddHeader(string name, string value) + // Builds the first line, for example + // GET /resource HTTP/1.1 + private string BuildFirstLine() + { + if (Version >= new Version(2, 0)) { - // Make sure Host is written properly otherwise it won't get picked up below - if (name.Equals("Host", StringComparison.OrdinalIgnoreCase)) - { - Headers["Host"] = value; - } - else - { - Headers[name] = value; - } + throw new RLHttpException($"HTTP/{Version.Major}.{Version.Minor} not supported yet"); } + + if (Uri is null) + { + throw new RLHttpException("Uri cannot be null"); + } + + return $"{Method.Method} {(AbsoluteUriInFirstLine ? Uri.AbsoluteUri : Uri.PathAndQuery)} HTTP/{Version}{_newLine}"; + } - // Builds the first line, for example - // GET /resource HTTP/1.1 - private string BuildFirstLine() + // Builds the headers, for example + // Host: example.com + // Connection: Close + private string BuildHeaders() + { + if (Uri is null) { - if (Version >= new Version(2, 0)) - throw new Exception($"HTTP/{Version.Major}.{Version.Minor} not supported yet"); + throw new RLHttpException("Uri cannot be null"); + } + + // NOTE: Do not use AppendLine because it appends \n instead of \r\n + // on Unix-like systems. + var sb = new StringBuilder(); + var finalHeaders = new List>(); + + // Add the Host header if not already provided + if (!HeaderExists("Host", out _)) + { + finalHeaders.Add("Host", Uri.Host); + } - return $"{Method.Method} {(AbsoluteUriInFirstLine ? Uri.AbsoluteUri : Uri.PathAndQuery)} HTTP/{Version}{newLine}"; + // If there is no Connection header, add it + if (!HeaderExists("Connection", out _)) + { + finalHeaders.Add("Connection", "Close"); } - // Builds the headers, for example - // Host: example.com - // Connection: Close - private string BuildHeaders() + // Add the non-content headers + finalHeaders.AddRange(Headers); + + // Add the Cookie header if not set manually and container not null + if (!HeaderExists("Cookie", out _) && Cookies.Count != 0) { - // NOTE: Do not use AppendLine because it appends \n instead of \r\n - // on Unix-like systems. - var sb = new StringBuilder(); - var finalHeaders = new List>(); + var cookieBuilder = new StringBuilder(); - // Add the Host header if not already provided - if (!HeaderExists("Host", out _)) + foreach (var cookie in Cookies) { - finalHeaders.Add("Host", Uri.Host); + cookieBuilder + .Append($"{cookie.Key}={cookie.Value}; "); } - // If there is no Connection header, add it - if (!HeaderExists("Connection", out var connectionHeaderName)) + // Remove the last ; and space if not empty + if (cookieBuilder.Length > 2) { - finalHeaders.Add("Connection", "Close"); + cookieBuilder.Remove(cookieBuilder.Length - 2, 2); } - // Add the non-content headers - foreach (var header in Headers) - { - finalHeaders.Add(header); - } + finalHeaders.Add("Cookie", cookieBuilder); + } - // Add the Cookie header if not set manually and container not null - if (!HeaderExists("Cookie", out _) && Cookies.Any()) + // Add the content headers + if (Content != null) + { + foreach (var header in Content.Headers) { - var cookieBuilder = new StringBuilder(); - - foreach (var cookie in Cookies) - { - cookieBuilder - .Append($"{cookie.Key}={cookie.Value}; "); - } - - // Remove the last ; and space if not empty - if (cookieBuilder.Length > 2) + // If it was already set, skip + if (!HeaderExists(header.Key, out _)) { - cookieBuilder.Remove(cookieBuilder.Length - 2, 2); + finalHeaders.Add(header.Key, string.Join(' ', header.Value)); } - - finalHeaders.Add("Cookie", cookieBuilder); } - // Add the content headers - if (Content != null) + // Add the Content-Length header if not already present + if (!finalHeaders.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))) { - foreach (var header in Content.Headers) - { - // If it was already set, skip - if (!HeaderExists(header.Key, out _)) - { - finalHeaders.Add(header.Key, string.Join(' ', header.Value)); - } - } + var contentLength = Content.Headers.ContentLength; - // Add the Content-Length header if not already present - if (!finalHeaders.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))) + if (contentLength is > 0) { - var contentLength = Content.Headers.ContentLength; - - if (contentLength.HasValue && contentLength.Value > 0) - { - finalHeaders.Add("Content-Length", contentLength); - } + finalHeaders.Add("Content-Length", contentLength); } } - - // Write all non-empty headers to the StringBuilder - foreach (var header in finalHeaders.Where(h => !string.IsNullOrEmpty(h.Value))) - { - sb - .Append(header.Key) - .Append(": ") - .Append(header.Value) - .Append(newLine); - } - - // Write the final blank line after all headers - sb.Append(newLine); - - return sb.ToString(); } - /// - /// Checks whether a header that matches a given exists. If it exists, - /// its original name will be written to . - /// - public bool HeaderExists(string name, out string actualName) + // Write all non-empty headers to the StringBuilder + foreach (var header in finalHeaders.Where(h => !string.IsNullOrEmpty(h.Value))) { - var key = Headers.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); - actualName = key; - return key != null; + sb + .Append(header.Key) + .Append(": ") + .Append(header.Value) + .Append(_newLine); } - /// - public void Dispose() => Content?.Dispose(); + // Write the final blank line after all headers + sb.Append(_newLine); + + return sb.ToString(); + } + + /// + /// Checks whether a header that matches a given exists. If it exists, + /// its original name will be written to . + /// + public bool HeaderExists(string name, out string? actualName) + { + var key = Headers.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); + actualName = key; + return key != null; + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Content?.Dispose(); } } diff --git a/RuriLib.Http/Models/HttpResponse.cs b/RuriLib.Http/Models/HttpResponse.cs index 4cb36c643..74714d7e3 100644 --- a/RuriLib.Http/Models/HttpResponse.cs +++ b/RuriLib.Http/Models/HttpResponse.cs @@ -3,39 +3,42 @@ using System.Net; using System.Net.Http; -namespace RuriLib.Http.Models +namespace RuriLib.Http.Models; + +/// +/// An HTTP response obtained with a . +/// +public class HttpResponse : IDisposable { /// - /// An HTTP response obtained with a . + /// The request that retrieved this response. /// - public class HttpResponse : IDisposable - { - /// - /// The request that retrieved this response. - /// - public HttpRequest Request { get; set; } + public HttpRequest? Request { get; set; } - /// - /// The HTTP version. - /// - public Version Version { get; set; } = new(1, 1); + /// + /// The HTTP version. + /// + public Version Version { get; set; } = new(1, 1); - /// - /// The status code of the response. - /// - public HttpStatusCode StatusCode { get; set; } + /// + /// The status code of the response. + /// + public HttpStatusCode StatusCode { get; set; } - /// - /// The headers of the response. - /// - public Dictionary Headers { get; set; } = new(StringComparer.InvariantCultureIgnoreCase); + /// + /// The headers of the response. + /// + public Dictionary Headers { get; set; } = new(StringComparer.InvariantCultureIgnoreCase); - /// - /// The content of the response. - /// - public HttpContent Content { get; set; } + /// + /// The content of the response. + /// + public HttpContent? Content { get; set; } - /// - public void Dispose() => Content?.Dispose(); + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Content?.Dispose(); } } diff --git a/RuriLib.Http/ProxyClientHandler.cs b/RuriLib.Http/ProxyClientHandler.cs index 8a2258cc1..2f97d0045 100644 --- a/RuriLib.Http/ProxyClientHandler.cs +++ b/RuriLib.Http/ProxyClientHandler.cs @@ -12,316 +12,313 @@ using System.Text; using RuriLib.Proxies.Exceptions; using System.Collections.Generic; +using System.Runtime.InteropServices; +using RuriLib.Http.Exceptions; -namespace RuriLib.Http +namespace RuriLib.Http; + +/// +/// Represents with a +/// to provide proxy support to the . +/// +public class ProxyClientHandler : HttpMessageHandler { + private TcpClient? _tcpClient; + private Stream? _connectionCommonStream; + private NetworkStream? _connectionNetworkStream; + + #region Properties + /// + /// The underlying proxy client. + /// + public ProxyClient ProxyClient { get; } + + /// + /// Gets the raw bytes of the last request that was sent. + /// + public List RawRequests { get; } = []; + /// - /// Represents with a - /// to provide proxy support to the . + /// Allow automatic redirection on 3xx reply. /// - public class ProxyClientHandler : HttpMessageHandler, IDisposable + public bool AllowAutoRedirect { get; set; } = true; + + /// + /// The maximum number of times a request will be redirected. + /// + public int MaxNumberOfRedirects { get; set; } = 8; + + /// + /// Whether to read the content of the response. Set to false if you're only interested + /// in headers. + /// + public bool ReadResponseContent { get; set; } = true; + + /// + /// The allowed SSL or TLS protocols. + /// + public SslProtocols SslProtocols { get; set; } = SslProtocols.None; + + /// + /// If true, will be used instead of the default ones. + /// + public bool UseCustomCipherSuites { get; set; } = false; + + /// + /// The cipher suites to send to the server during the TLS handshake, in order. + /// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020. + /// + public TlsCipherSuite[] AllowedCipherSuites { get; set; } = + [ + TlsCipherSuite.TLS_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA + ]; + + /// + /// Gets the type of decompression method used by the handler for automatic + /// decompression of the HTTP content response. + /// + /// + /// Support GZip and Deflate encoding automatically + /// + public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | DecompressionMethods.Deflate; + + /// + /// Gets or sets a value that indicates whether the handler uses the CookieContainer + /// property to store server cookies and uses these cookies when sending requests. + /// + public bool UseCookies { get; set; } = true; + + /// + /// Gets or sets the cookie container used to store server cookies by the handler. + /// + public CookieContainer CookieContainer { get; set; } = new(); + + /// + /// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL) + /// certificate used for authentication. + /// + public RemoteCertificateValidationCallback? ServerCertificateCustomValidationCallback { get; set; } + + /// + /// Gets or sets the X509 certificate revocation mode. + /// + public X509RevocationMode CertRevocationMode { get; set; } + #endregion + + /// + /// Creates a new instance of given a . + /// + public ProxyClientHandler(ProxyClient proxyClient) { - private readonly ProxyClient proxyClient; - - private TcpClient tcpClient; - private Stream connectionCommonStream; - private NetworkStream connectionNetworkStream; - - #region Properties - /// - /// The underlying proxy client. - /// - public ProxyClient ProxyClient => proxyClient; - - /// - /// Gets the raw bytes of the last request that was sent. - /// - public List RawRequests { get; } = new(); - - /// - /// Allow automatic redirection on 3xx reply. - /// - public bool AllowAutoRedirect { get; set; } = true; - - /// - /// The maximum number of times a request will be redirected. - /// - public int MaxNumberOfRedirects { get; set; } = 8; - - /// - /// Whether to read the content of the response. Set to false if you're only interested - /// in headers. - /// - public bool ReadResponseContent { get; set; } = true; - - /// - /// The allowed SSL or TLS protocols. - /// - public SslProtocols SslProtocols { get; set; } = SslProtocols.None; - - /// - /// If true, will be used instead of the default ones. - /// - public bool UseCustomCipherSuites { get; set; } = false; - - /// - /// The cipher suites to send to the server during the TLS handshake, in order. - /// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020. - /// - public TlsCipherSuite[] AllowedCipherSuites { get; set; } = new TlsCipherSuite[] - { - TlsCipherSuite.TLS_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA - }; - - /// - /// Gets the type of decompression method used by the handler for automatic - /// decompression of the HTTP content response. - /// - /// - /// Support GZip and Deflate encoding automatically - /// - public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | DecompressionMethods.Deflate; - - /// - /// Gets or sets a value that indicates whether the handler uses the CookieContainer - /// property to store server cookies and uses these cookies when sending requests. - /// - public bool UseCookies { get; set; } = true; - - /// - /// Gets or sets the cookie container used to store server cookies by the handler. - /// - public CookieContainer CookieContainer { get; set; } - - /// - /// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL) - /// certificate used for authentication. - /// - public RemoteCertificateValidationCallback ServerCertificateCustomValidationCallback { get; set; } - - /// - /// Gets or sets the X509 certificate revocation mode. - /// - public X509RevocationMode CertRevocationMode { get; set; } - #endregion - - /// - /// Creates a new instance of given a . - /// - public ProxyClientHandler(ProxyClient proxyClient) - { - this.proxyClient = proxyClient ?? throw new ArgumentNullException(nameof(proxyClient)); - } + this.ProxyClient = proxyClient ?? throw new ArgumentNullException(nameof(proxyClient)); + } - /// - /// Asynchronously sends a and returns an . - /// - /// The request to send - /// A cancellation token to cancel the operation - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken = default) - => SendAsync(request, 0, cancellationToken); - - private async Task SendAsync(HttpRequestMessage request, - int redirects, CancellationToken cancellationToken = default) + /// + /// Asynchronously sends a and returns an . + /// + /// The request to send + /// A cancellation token to cancel the operation + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + => SendAsync(request, 0, cancellationToken); + + private async Task SendAsync(HttpRequestMessage request, + int redirects, CancellationToken cancellationToken = default) + { + if (redirects > MaxNumberOfRedirects) { - if (redirects > MaxNumberOfRedirects) - { - throw new Exception("Maximum number of redirects exceeded"); - } + throw new RLHttpException("Maximum number of redirects exceeded"); + } - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgumentNullException.ThrowIfNull(request); - if (UseCookies && CookieContainer == null) - { - throw new ArgumentNullException(nameof(CookieContainer)); - } - - await CreateConnection(request, cancellationToken).ConfigureAwait(false); - await SendDataAsync(request, cancellationToken).ConfigureAwait(false); + await CreateConnection(request, cancellationToken).ConfigureAwait(false); + await SendDataAsync(request, cancellationToken).ConfigureAwait(false); - var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait(false); + var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait(false); - try + try + { + // Optionally perform auto redirection on 3xx response + if ((int)responseMessage.StatusCode / 100 == 3 && AllowAutoRedirect) { - // Optionally perform auto redirection on 3xx response - if (((int)responseMessage.StatusCode) / 100 == 3 && AllowAutoRedirect) + if (!responseMessage.Headers.Contains("Location")) { - if (!responseMessage.Headers.Contains("Location")) - { - throw new Exception($"Status code was {(int)responseMessage.StatusCode} but no Location header received. " + - $"Disable auto redirect and try again."); - } - - // Compute the redirection URI - var redirectUri = responseMessage.Headers.Location.IsAbsoluteUri - ? responseMessage.Headers.Location - : new Uri(request.RequestUri, responseMessage.Headers.Location); + throw new RLHttpException($"Status code was {(int)responseMessage.StatusCode} but no Location header received. " + + $"Disable auto redirect and try again."); + } + + // Compute the redirection URI + var redirectUri = responseMessage.Headers.Location!.IsAbsoluteUri + ? responseMessage.Headers.Location + : new Uri(request.RequestUri!, responseMessage.Headers.Location); + + // If not 307, change the method to GET + if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb) + { + request.Method = HttpMethod.Get; + request.Content = null; + } - // If not 307, change the method to GET - if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb) + // Port over the cookies if the domains are different + if (request.RequestUri!.Host != redirectUri.Host) + { + var cookies = CookieContainer.GetCookies(request.RequestUri); + foreach (Cookie cookie in cookies) { - request.Method = HttpMethod.Get; - request.Content = null; + CookieContainer.Add(redirectUri, new Cookie(cookie.Name, cookie.Value)); } - // Port over the cookies if the domains are different - if (request.RequestUri.Host != redirectUri.Host) - { - var cookies = CookieContainer.GetCookies(request.RequestUri); - foreach (Cookie cookie in cookies) - { - CookieContainer.Add(redirectUri, new Cookie(cookie.Name, cookie.Value)); - } - - // This is needed otherwise if the Host header was set manually - // it will keep the previous one after a domain switch - request.Headers.Host = string.Empty; - - // Remove additional headers that could cause trouble - request.Headers.Remove("Origin"); - } + // This is needed otherwise if the Host header was set manually + // it will keep the previous one after a domain switch + request.Headers.Host = string.Empty; - // Set the new URI - request.RequestUri = redirectUri; + // Remove additional headers that could cause trouble + request.Headers.Remove("Origin"); + } - // Dispose the previous response - responseMessage.Dispose(); + // Set the new URI + request.RequestUri = redirectUri; - // Perform a new request - return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false); - } - } - catch - { + // Dispose the previous response responseMessage.Dispose(); - throw; - } - return responseMessage; + // Perform a new request + return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false); + } } - - private async Task SendDataAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + catch { - byte[] buffer; - using var ms = new MemoryStream(); + responseMessage.Dispose(); + throw; + } - // Send the first line - buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildFirstLine(request)); - ms.Write(buffer); - await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + return responseMessage; + } - // Send the headers - buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildHeaders(request, CookieContainer)); - ms.Write(buffer); - await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + private async Task SendDataAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + using var ms = new MemoryStream(); - // Optionally send the content - if (request.Content != null) - { - buffer = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); - ms.Write(buffer); - await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - } + // Send the first line + var buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildFirstLine(request)); + ms.Write(buffer); + await _connectionCommonStream!.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - ms.Seek(0, SeekOrigin.Begin); - RawRequests.Add(ms.ToArray()); - } + // Send the headers + buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildHeaders(request, CookieContainer)); + ms.Write(buffer); + await _connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - private Task ReceiveDataAsync(HttpRequestMessage request, - CancellationToken cancellationToken) + // Optionally send the content + if (request.Content != null) { - var responseBuilder = new HttpResponseMessageBuilder(1024, CookieContainer, request.RequestUri); - return responseBuilder.GetResponseAsync(request, connectionCommonStream, ReadResponseContent, cancellationToken); + buffer = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + ms.Write(buffer); + await _connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); } - private async Task CreateConnection(HttpRequestMessage request, CancellationToken cancellationToken) + ms.Seek(0, SeekOrigin.Begin); + RawRequests.Add(ms.ToArray()); + } + + private Task ReceiveDataAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + var responseBuilder = new HttpResponseMessageBuilder(1024, CookieContainer, request.RequestUri!); + return responseBuilder.GetResponseAsync(request, _connectionCommonStream!, ReadResponseContent, cancellationToken); + } + + private async Task CreateConnection(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Dispose of any previous connection (if we're coming from a redirect) + _tcpClient?.Close(); + + if (_connectionCommonStream is not null) { - // Dispose of any previous connection (if we're coming from a redirect) - tcpClient?.Close(); - connectionCommonStream?.Dispose(); - connectionNetworkStream?.Dispose(); - - // Get the stream from the proxies TcpClient - var uri = request.RequestUri; - tcpClient = await proxyClient.ConnectAsync(uri.Host, uri.Port, null, cancellationToken); - connectionNetworkStream = tcpClient.GetStream(); - - // If https, set up a TLS stream - if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) - { - try - { - var sslStream = new SslStream(connectionNetworkStream, false, ServerCertificateCustomValidationCallback); + await _connectionCommonStream.DisposeAsync().ConfigureAwait(false); + } + + if (_connectionNetworkStream is not null) + { + await _connectionNetworkStream.DisposeAsync().ConfigureAwait(false); + } - var sslOptions = new SslClientAuthenticationOptions - { - TargetHost = uri.Host, - EnabledSslProtocols = SslProtocols, - CertificateRevocationCheckMode = CertRevocationMode, - }; + // Get the stream from the proxies TcpClient + var uri = request.RequestUri; + _tcpClient = await ProxyClient.ConnectAsync(uri!.Host, uri.Port, null, cancellationToken); + _connectionNetworkStream = _tcpClient.GetStream(); - if (CertRevocationMode != X509RevocationMode.Online) - { - sslOptions.RemoteCertificateValidationCallback = - new RemoteCertificateValidationCallback((s, c, ch, e) => { return true; }); - } + // If https, set up a TLS stream + if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + try + { + var sslStream = new SslStream(_connectionNetworkStream, false, ServerCertificateCustomValidationCallback); - if (UseCustomCipherSuites) - { - sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites); - } + var sslOptions = new SslClientAuthenticationOptions + { + TargetHost = uri.Host, + EnabledSslProtocols = SslProtocols, + CertificateRevocationCheckMode = CertRevocationMode, + }; - connectionCommonStream = sslStream; - await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken); - } - catch (Exception ex) + if (CertRevocationMode != X509RevocationMode.Online) { - if (ex is IOException || ex is AuthenticationException) - { - throw new ProxyException("Failed SSL connect"); - } + sslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } - throw; + if (UseCustomCipherSuites && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites); } + + _connectionCommonStream = sslStream; + await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken); } - else + catch (Exception ex) { - connectionCommonStream = connectionNetworkStream; + if (ex is IOException or AuthenticationException) + { + throw new ProxyException("Failed SSL connect"); + } + + throw; } } - - /// - protected override void Dispose(bool disposing) + else { - if (disposing) - { - tcpClient?.Dispose(); - connectionCommonStream?.Dispose(); - connectionNetworkStream?.Dispose(); - } + _connectionCommonStream = _connectionNetworkStream; + } + } - base.Dispose(disposing); + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _tcpClient?.Dispose(); + _connectionCommonStream?.Dispose(); + _connectionNetworkStream?.Dispose(); } + + base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/RuriLib.Http/RLHttpClient.cs b/RuriLib.Http/RLHttpClient.cs index 79a980194..922a09dac 100644 --- a/RuriLib.Http/RLHttpClient.cs +++ b/RuriLib.Http/RLHttpClient.cs @@ -15,282 +15,294 @@ using RuriLib.Http.Models; using System.Linq; using System.Runtime.InteropServices; +using RuriLib.Http.Exceptions; -namespace RuriLib.Http +namespace RuriLib.Http; + +/// +/// Custom implementation of an HttpClient. +/// +public class RLHttpClient : IDisposable { + private TcpClient? _tcpClient; + private Stream? _connectionCommonStream; + private NetworkStream? _connectionNetworkStream; + + #region Properties + /// + /// The underlying proxy client. + /// + public ProxyClient ProxyClient { get; } + + /// + /// Gets the raw bytes of all the requests that were sent. + /// + public List RawRequests { get; } = []; + + /// + /// Allow automatic redirection on 3xx reply. + /// + public bool AllowAutoRedirect { get; set; } = true; + + /// + /// The maximum number of times a request will be redirected. + /// + public int MaxNumberOfRedirects { get; set; } = 8; + + /// + /// Whether to read the content of the response. Set to false if you're only interested + /// in headers. + /// + public bool ReadResponseContent { get; set; } = true; + + /// + /// The allowed SSL or TLS protocols. + /// + public SslProtocols SslProtocols { get; set; } = SslProtocols.None; + + /// + /// If true, will be used instead of the default ones. + /// + public bool UseCustomCipherSuites { get; set; } = false; + + /// + /// The cipher suites to send to the server during the TLS handshake, in order. + /// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020. + /// + public TlsCipherSuite[] AllowedCipherSuites { get; set; } = + [ + TlsCipherSuite.TLS_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, + TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA + ]; + + /// + /// Gets the type of decompression method used by the handler for automatic + /// decompression of the HTTP content response. + /// + /// + /// Support GZip and Deflate encoding automatically + /// + public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | DecompressionMethods.Deflate; + /// - /// Custom implementation of an HttpClient. + /// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL) + /// certificate used for authentication. /// - public class RLHttpClient : IDisposable + public RemoteCertificateValidationCallback? ServerCertificateCustomValidationCallback { get; set; } + + /// + /// Gets or sets the X509 certificate revocation mode. + /// + public X509RevocationMode CertRevocationMode { get; set; } + #endregion + + /// + /// Creates a new instance of given a . + /// If is null, will be used. + /// + public RLHttpClient(ProxyClient? proxyClient = null) { - private readonly ProxyClient proxyClient; - - private TcpClient tcpClient; - private Stream connectionCommonStream; - private NetworkStream connectionNetworkStream; - - #region Properties - /// - /// The underlying proxy client. - /// - public ProxyClient ProxyClient => proxyClient; - - /// - /// Gets the raw bytes of all the requests that were sent. - /// - public List RawRequests { get; } = new(); - - /// - /// Allow automatic redirection on 3xx reply. - /// - public bool AllowAutoRedirect { get; set; } = true; - - /// - /// The maximum number of times a request will be redirected. - /// - public int MaxNumberOfRedirects { get; set; } = 8; - - /// - /// Whether to read the content of the response. Set to false if you're only interested - /// in headers. - /// - public bool ReadResponseContent { get; set; } = true; - - /// - /// The allowed SSL or TLS protocols. - /// - public SslProtocols SslProtocols { get; set; } = SslProtocols.None; - - /// - /// If true, will be used instead of the default ones. - /// - public bool UseCustomCipherSuites { get; set; } = false; - - /// - /// The cipher suites to send to the server during the TLS handshake, in order. - /// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020. - /// - public TlsCipherSuite[] AllowedCipherSuites { get; set; } = new TlsCipherSuite[] - { - TlsCipherSuite.TLS_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, - TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, - TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, - TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA - }; - - /// - /// Gets the type of decompression method used by the handler for automatic - /// decompression of the HTTP content response. - /// - /// - /// Support GZip and Deflate encoding automatically - /// - public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | DecompressionMethods.Deflate; - - /// - /// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL) - /// certificate used for authentication. - /// - public RemoteCertificateValidationCallback ServerCertificateCustomValidationCallback { get; set; } - - /// - /// Gets or sets the X509 certificate revocation mode. - /// - public X509RevocationMode CertRevocationMode { get; set; } - #endregion - - /// - /// Creates a new instance of given a . - /// If is null, will be used. - /// - public RLHttpClient(ProxyClient proxyClient = null) + this.ProxyClient = proxyClient ?? new NoProxyClient(); + } + + /// + /// Asynchronously sends a and returns an . + /// + /// The request to send + /// A cancellation token to cancel the operation + public Task SendAsync(HttpRequest request, CancellationToken cancellationToken = default) + => SendAsync(request, 0, cancellationToken); + + private async Task SendAsync(HttpRequest request, int redirects, + CancellationToken cancellationToken = default) + { + if (redirects > MaxNumberOfRedirects) { - this.proxyClient = proxyClient ?? new NoProxyClient(); + throw new RLHttpException("Maximum number of redirects exceeded"); } - /// - /// Asynchronously sends a and returns an . - /// - /// The request to send - /// A cancellation token to cancel the operation - public Task SendAsync(HttpRequest request, CancellationToken cancellationToken = default) - => SendAsync(request, 0, cancellationToken); - - private async Task SendAsync(HttpRequest request, int redirects, - CancellationToken cancellationToken = default) + if (request.Uri is null) { - if (redirects > MaxNumberOfRedirects) - { - throw new Exception("Maximum number of redirects exceeded"); - } + throw new RLHttpException("The request URI is null"); + } - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgumentNullException.ThrowIfNull(request); - await CreateConnection(request, cancellationToken).ConfigureAwait(false); - await SendDataAsync(request, cancellationToken).ConfigureAwait(false); + await CreateConnection(request, cancellationToken).ConfigureAwait(false); + await SendDataAsync(request, cancellationToken).ConfigureAwait(false); - var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait(false); + var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait(false); - try + try + { + // Optionally perform auto redirection on 3xx response + if ((int)responseMessage.StatusCode / 100 == 3 && AllowAutoRedirect) { - // Optionally perform auto redirection on 3xx response - if (((int)responseMessage.StatusCode) / 100 == 3 && AllowAutoRedirect) - { - // Compute the redirection URI - var locationHeaderName = responseMessage.Headers.Keys - .FirstOrDefault(k => k.Equals("Location", StringComparison.OrdinalIgnoreCase)); + // Compute the redirection URI + var locationHeaderName = responseMessage.Headers.Keys + .FirstOrDefault(k => k.Equals("Location", StringComparison.OrdinalIgnoreCase)); - if (locationHeaderName is null) - { - throw new Exception($"Status code was {(int)responseMessage.StatusCode} but no Location header received. " + - $"Disable auto redirect and try again."); - } + if (locationHeaderName is null) + { + throw new Exception($"Status code was {(int)responseMessage.StatusCode} but no Location header received. " + + $"Disable auto redirect and try again."); + } - Uri.TryCreate(responseMessage.Headers[locationHeaderName], UriKind.RelativeOrAbsolute, out var newLocation); + Uri.TryCreate(responseMessage.Headers[locationHeaderName], UriKind.RelativeOrAbsolute, out var newLocation); - var redirectUri = newLocation.IsAbsoluteUri - ? newLocation - : new Uri(request.Uri, newLocation); + if (newLocation is null) + { + throw new Exception($"The Location header value '{responseMessage.Headers[locationHeaderName]}' is not a valid URI"); + } + + var redirectUri = newLocation.IsAbsoluteUri + ? newLocation + : new Uri(request.Uri, newLocation); - // If not 307, change the method to GET - if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb) - { - request.Method = HttpMethod.Get; - request.Content = null; - } + // If not 307, change the method to GET + if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb) + { + request.Method = HttpMethod.Get; + request.Content = null; + } - // Adjust the request if the host is different - if (request.Uri.Host != redirectUri.Host) + // Adjust the request if the host is different + if (request.Uri.Host != redirectUri.Host) + { + // This is needed otherwise if the Host header was set manually + // it will keep the previous one after a domain switch + if (request.HeaderExists("Host", out var hostHeaderName)) { - // This is needed otherwise if the Host header was set manually - // it will keep the previous one after a domain switch - if (request.HeaderExists("Host", out var hostHeaderName)) - { - request.Headers.Remove(hostHeaderName); - } - - // Remove additional headers that could cause trouble - request.Headers.Remove("Origin"); + request.Headers.Remove(hostHeaderName!); } - // Set the new URI - request.Uri = redirectUri; + // Remove additional headers that could cause trouble + request.Headers.Remove("Origin"); + } - // Dispose the previous response - responseMessage.Dispose(); + // Set the new URI + request.Uri = redirectUri; - // Perform a new request - return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false); - } - } - catch - { + // Dispose the previous response responseMessage.Dispose(); - throw; - } - return responseMessage; + // Perform a new request + return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false); + } } - - private async Task SendDataAsync(HttpRequest request, CancellationToken cancellationToken = default) + catch { - var buffer = await request.GetBytesAsync(cancellationToken); - await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - - RawRequests.Add(buffer); + responseMessage.Dispose(); + throw; } - private Task ReceiveDataAsync(HttpRequest request, - CancellationToken cancellationToken) => - new HttpResponseBuilder().GetResponseAsync(request, connectionCommonStream, ReadResponseContent, cancellationToken); + return responseMessage; + } - private async Task CreateConnection(HttpRequest request, CancellationToken cancellationToken) - { - // Dispose of any previous connection (if we're coming from a redirect) - tcpClient?.Close(); + private async Task SendDataAsync(HttpRequest request, CancellationToken cancellationToken = default) + { + var buffer = await request.GetBytesAsync(cancellationToken); + await _connectionCommonStream!.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - if (connectionCommonStream is not null) - { - await connectionCommonStream.DisposeAsync().ConfigureAwait(false); - } - - if (connectionNetworkStream is not null) - { - await connectionNetworkStream.DisposeAsync().ConfigureAwait(false); - } + RawRequests.Add(buffer); + } - // Get the stream from the proxies TcpClient - var uri = request.Uri; - tcpClient = await proxyClient.ConnectAsync(uri.Host, uri.Port, null, cancellationToken); - connectionNetworkStream = tcpClient.GetStream(); + private Task ReceiveDataAsync(HttpRequest request, + CancellationToken cancellationToken) => + new HttpResponseBuilder().GetResponseAsync(request, _connectionCommonStream!, ReadResponseContent, cancellationToken); - // If https, set up a TLS stream - if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) - { - try - { - var sslStream = new SslStream(connectionNetworkStream, false, ServerCertificateCustomValidationCallback); + private async Task CreateConnection(HttpRequest request, CancellationToken cancellationToken) + { + // Dispose of any previous connection (if we're coming from a redirect) + _tcpClient?.Close(); - var sslOptions = new SslClientAuthenticationOptions - { - TargetHost = uri.Host, - EnabledSslProtocols = SslProtocols, - CertificateRevocationCheckMode = CertRevocationMode - }; + if (_connectionCommonStream is not null) + { + await _connectionCommonStream.DisposeAsync().ConfigureAwait(false); + } + + if (_connectionNetworkStream is not null) + { + await _connectionNetworkStream.DisposeAsync().ConfigureAwait(false); + } - if (CertRevocationMode != X509RevocationMode.Online) - { - sslOptions.RemoteCertificateValidationCallback = - (_, _, _, _) => true; - } + var uri = request.Uri; + + if (uri is null) + { + throw new RLHttpException("The request URI is null"); + } + + // Get the stream from the proxies TcpClient + _tcpClient = await ProxyClient.ConnectAsync(uri.Host, uri.Port, null, cancellationToken); + _connectionNetworkStream = _tcpClient.GetStream(); - if (UseCustomCipherSuites && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites); - } + // If https, set up a TLS stream + if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + try + { + var sslStream = new SslStream(_connectionNetworkStream, false, ServerCertificateCustomValidationCallback); - connectionCommonStream = sslStream; - await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken); - } - catch (Exception ex) + var sslOptions = new SslClientAuthenticationOptions { - if (ex is IOException || ex is AuthenticationException) - { - throw new ProxyException("Failed SSL connect"); - } + TargetHost = uri.Host, + EnabledSslProtocols = SslProtocols, + CertificateRevocationCheckMode = CertRevocationMode + }; - throw; + if (CertRevocationMode != X509RevocationMode.Online) + { + sslOptions.RemoteCertificateValidationCallback = + (_, _, _, _) => true; } + + if (UseCustomCipherSuites && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites); + } + + _connectionCommonStream = sslStream; + await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken); } - else + catch (Exception ex) { - connectionCommonStream = connectionNetworkStream; + if (ex is IOException or AuthenticationException) + { + throw new ProxyException("Failed SSL connect"); + } + + throw; } } - - /// - public void Dispose() + else { - tcpClient?.Dispose(); - connectionCommonStream?.Dispose(); - connectionNetworkStream?.Dispose(); + _connectionCommonStream = _connectionNetworkStream; } } -} \ No newline at end of file + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + _tcpClient?.Dispose(); + _connectionCommonStream?.Dispose(); + _connectionNetworkStream?.Dispose(); + } +} diff --git a/RuriLib.Http/RuriLib.Http.csproj b/RuriLib.Http/RuriLib.Http.csproj index 5f459bbe3..6ab521839 100644 --- a/RuriLib.Http/RuriLib.Http.csproj +++ b/RuriLib.Http/RuriLib.Http.csproj @@ -9,6 +9,7 @@ Ruri 2022 https://github.com/openbullet/OpenBullet2/tree/master/RuriLib.Http http; proxy; socks; client; custom; + enable