diff --git a/CHANGES.md b/CHANGES.md index 3bbc7d4..ef45fd1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +## 1.9.0 + +* Removed custom TLS stack +* Allowed option for callers of library to inject own http client + ## 1.8.1 * Upgrade System.Text.JSON @@ -9,7 +14,6 @@ * CVE-2024-43485 - Upgrade System.Text.JSON * Removed .NET 6 - ## 1.7.0 * Added .NET 8 as test target * Dropped .NET 6 diff --git a/src/Tinify/Client.cs b/src/Tinify/Client.cs index 7039ee9..d47fefd 100644 --- a/src/Tinify/Client.cs +++ b/src/Tinify/Client.cs @@ -26,21 +26,25 @@ internal sealed class ErrorData public static readonly Uri ApiEndpoint = new("https://api.tinify.com"); public static readonly short RetryCount = 1; - public static ushort RetryDelay { get; internal set; }= 500; + public static ushort RetryDelay { get; internal set; } = 500; public static readonly string UserAgent = Internal.Platform.UserAgent; private readonly HttpClient _client; + public Client(string key) + : this(key, (string)null, (string)null) + { + } + + public Client(string key, string appIdentifier = null) + : this(key, appIdentifier, (string)null) + { + } + public Client(string key, string appIdentifier = null, string proxy = null) { var handler = new HttpClientHandler(); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - // TLS is extremely spotty and differs per version on MacOS - handler.ServerCertificateCustomValidationCallback = Internal.SSL.ValidationCallback; - }; - if (proxy != null) { handler.Proxy = new Internal.Proxy(proxy); @@ -66,6 +70,37 @@ public Client(string key, string appIdentifier = null, string proxy = null) _client.DefaultRequestHeaders.Add("Authorization", "Basic " + credentials); } + public Client(string key, string appIdentifier = null, HttpClient customHttpClient = null) + { + + if (customHttpClient != null) + { + _client = customHttpClient; + _client.BaseAddress = ApiEndpoint; + } + else + { + _client = new HttpClient() + { + BaseAddress = ApiEndpoint, + Timeout = Timeout.InfiniteTimeSpan, + }; + } + + var userAgent = UserAgent; + if (appIdentifier != null) + { + userAgent += " " + appIdentifier; + } + + _client.DefaultRequestHeaders.Add("User-Agent", userAgent); + + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes("api:" + key)); + _client.DefaultRequestHeaders.Add("Authorization", "Basic " + credentials); + } + + + public Task Request(Method method, string url) { return Request(method, new Uri(url, UriKind.Relative)); @@ -155,7 +190,7 @@ public async Task Request(Method method, Uri url, HttpConte { var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); data = JsonSerializer.Deserialize(content) ?? - new ErrorData() {Message = "Response content was empty.", Error = "ParseError"}; + new ErrorData() { Message = "Response content was empty.", Error = "ParseError" }; } catch (Exception err) { diff --git a/src/Tinify/Internal/SSL.cs b/src/Tinify/Internal/SSL.cs deleted file mode 100644 index 01d75f8..0000000 --- a/src/Tinify/Internal/SSL.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Reflection; -using System.IO; - -namespace TinifyAPI.Internal -{ - internal static class SSL - { - public static bool ValidationCallback(HttpRequestMessage req, X509Certificate2 cert, X509Chain chain, SslPolicyErrors errors) - { - if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) return false; - if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) return false; - return new X509Chain() { ChainPolicy = policy }.Build(cert); - } - - private static X509ChainPolicy policy = createSSLChainPolicy(); - - private static X509ChainPolicy createSSLChainPolicy() - { - var policy = new X509ChainPolicy() - { - VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority - }; - - var header = "-----BEGIN CERTIFICATE-----"; - var footer = "-----END CERTIFICATE-----"; - - using (var stream = getBundleStream()) - using (var reader = new StreamReader(stream)) - { - var pem = reader.ReadToEnd(); - var start = 0; - var end = 0; - while (true) - { - start = pem.IndexOf(header, start, StringComparison.Ordinal); - if (start < 0) break; - - start += header.Length; - end = pem.IndexOf(footer, start, StringComparison.Ordinal); - if (end < 0) break; - - var bytes = Convert.FromBase64String(pem.Substring(start, end - start)); - policy.ExtraStore.Add(new X509Certificate2(bytes)); - } - } - - return policy; - } - - private static Stream getBundleStream() - { - var assembly = typeof(SSL).GetTypeInfo().Assembly; - return assembly.GetManifestResourceStream("Tinify.data.cacert.pem"); - } - } -} diff --git a/src/Tinify/Tinify.cs b/src/Tinify/Tinify.cs index 609b3f1..ad51f9c 100755 --- a/src/Tinify/Tinify.cs +++ b/src/Tinify/Tinify.cs @@ -1,3 +1,4 @@ +using System; using System.Net.Http; using System.Threading.Tasks; @@ -15,6 +16,25 @@ public class Tinify private static string key; private static string appIdentifier; private static string proxy; + private static HttpClient httpClient; + + public static HttpClient HttpClient + { + get + { + return httpClient; + } + + set + { + if (value != null && proxy != null) + { + throw new ArgumentException("Cannot set HttpClient when Proxy is already configured. Please set either HttpClient or Proxy, not both."); + } + httpClient = value; + ResetClient(); + } + } public static string Key { @@ -53,6 +73,10 @@ public static string Proxy set { + if (value != null && httpClient != null) + { + throw new ArgumentException("Cannot set Proxy when HttpClient is already configured. Please set either HttpClient or Proxy, not both."); + } proxy = value; ResetClient(); } @@ -88,7 +112,10 @@ public static Client Client { if (client == null) { - client = new Client(key, appIdentifier, proxy); + if (httpClient != null) + client = new Client(key, appIdentifier, httpClient); + else + client = new Client(key, appIdentifier, proxy); } } return client; diff --git a/src/Tinify/Tinify.csproj b/src/Tinify/Tinify.csproj index 13eaafd..69ba867 100644 --- a/src/Tinify/Tinify.csproj +++ b/src/Tinify/Tinify.csproj @@ -3,13 +3,13 @@ .NET client for the Tinify API. Tinify compresses your images intelligently. Read more at https://tinify.com. Tinify - 1.8.1 + 1.9.0 Tinify netstandard2.0 Tinify Tinify Tinify - Copyright © 2017-2022 + Copyright © 2017-2025 Tinify tinify;tinypng;tinyjpg;compress;images;api tinifyicon.png diff --git a/test/Tinify.Tests/TinifyTest.cs b/test/Tinify.Tests/TinifyTest.cs index 4cbaaf9..c15e514 100755 --- a/test/Tinify.Tests/TinifyTest.cs +++ b/test/Tinify.Tests/TinifyTest.cs @@ -17,6 +17,7 @@ public void TearDown() { Tinify.Key = null; Tinify.Proxy = null; + Tinify.HttpClient = null; } } @@ -124,6 +125,70 @@ public void WithInvalidProxy_Should_ThrowException() } } + [TestFixture] + public class Tinify_ProxyHttpClientConflict : Reset + { + [Test] + public void WithProxyFirst_HttpClientSecond_Should_ThrowException() + { + Tinify.Proxy = "http://localhost:8080"; + + var httpClient = new HttpClient(); + var error = Assert.Throws(() => + { + Tinify.HttpClient = httpClient; + }); + + Assert.AreEqual( + "Cannot set HttpClient when Proxy is already configured. Please set either HttpClient or Proxy, not both.", + error?.Message + ); + } + + [Test] + public void WithHttpClientFirst_ProxySecond_Should_ThrowException() + { + var httpClient = new HttpClient(); + Tinify.HttpClient = httpClient; + + var error = Assert.Throws(() => + { + Tinify.Proxy = "http://localhost:8080"; + }); + + Assert.AreEqual( + "Cannot set Proxy when HttpClient is already configured. Please set either HttpClient or Proxy, not both.", + error?.Message + ); + } + + [Test] + public void CanSetProxyAfterHttpClientIsCleared() + { + var httpClient = new HttpClient(); + Tinify.HttpClient = httpClient; + Tinify.HttpClient = null; + + Assert.DoesNotThrow(() => + { + Tinify.Proxy = "http://localhost:8080"; + }); + } + + [Test] + public void CanSetHttpClientAfterProxyIsCleared() + { + Tinify.Proxy = "http://localhost:8080"; + Tinify.Proxy = null; + + var httpClient = new HttpClient(); + Assert.DoesNotThrow(() => + { + Tinify.HttpClient = httpClient; + }); + } + } + [TestFixture] public class Tinify_Validate : Reset { @@ -148,7 +213,7 @@ public void WithLimitedKey_Should_ReturnTrue() Helper.MockClient(Tinify.Client); Helper.MockHandler.Expect("https://api.tinify.com/shrink").Respond( - (HttpStatusCode) 429, + (HttpStatusCode)429, new StringContent("{\"error\":\"Too may requests\",\"message\":\"Your monthly limit has been exceeded\"}") );