diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 0e4f215..6b658ec 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -2,9 +2,7 @@ name: Publish .NET Package on: push: - branches: [ master ] pull_request: - branches: [ master ] jobs: build: @@ -14,7 +12,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: '7.0.x' # Change this to the .NET version you are using + dotnet-version: '8.0.x' # Change this to the .NET version you are using - name: Restore dependencies run: dotnet restore - name: Build @@ -23,8 +21,10 @@ jobs: run: dotnet test --no-restore --verbosity normal - name: Publish run: dotnet pack --configuration Release --no-build --output ./artifacts + if: github.ref == 'refs/heads/master' - name: Push to GitHub Packages run: dotnet nuget push ./artifacts/*.nupkg -k ${{ secrets.GITHUB_TOKEN }} -s https://nuget.pkg.github.com/top-gg/index.json --skip-duplicate + if: github.ref == 'refs/heads/master' env: DOTNET_NOLOGO: true diff --git a/ProxyCheck.sln b/ProxyCheck.sln index 7ae2a70..a5e2d3a 100644 --- a/ProxyCheck.sln +++ b/ProxyCheck.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.26403.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxyCheckUtil", "ProxyCheck\ProxyCheckUtil.csproj", "{234ED760-6E95-4C53-B4EE-D4612A9E5BAD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProxyCheckUtil.Tests", "ProxyCheckUtil.Tests\ProxyCheckUtil.Tests.csproj", "{D768DB67-D857-4CA3-A8BA-88CA0F5E2306}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {234ED760-6E95-4C53-B4EE-D4612A9E5BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU {234ED760-6E95-4C53-B4EE-D4612A9E5BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {234ED760-6E95-4C53-B4EE-D4612A9E5BAD}.Release|Any CPU.Build.0 = Release|Any CPU + {D768DB67-D857-4CA3-A8BA-88CA0F5E2306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D768DB67-D857-4CA3-A8BA-88CA0F5E2306}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D768DB67-D857-4CA3-A8BA-88CA0F5E2306}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D768DB67-D857-4CA3-A8BA-88CA0F5E2306}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ProxyCheck/Http/DefaultHttpClientFactory.cs b/ProxyCheck/Http/DefaultHttpClientFactory.cs new file mode 100644 index 0000000..12516a6 --- /dev/null +++ b/ProxyCheck/Http/DefaultHttpClientFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using JetBrains.Annotations; + +namespace ProxyCheckUtil +{ + /// + /// Simple implementation of that creates a single instance. + /// Not recommended for production use, since it doesn't respect DNS changes. + /// + /// Creating a new instance for each request will lead to connection pool exhaustion. + /// Use the pooling from the 'Microsoft.Extensions.Http' package instead. + /// + internal class DefaultHttpClientFactory : IHttpClientFactory + { + private static readonly HttpClient HttpClient = new NonDisposableHttpClient(); + + public HttpClient CreateClient(string name) + { + return HttpClient; + } + + private class NonDisposableHttpClient : HttpClient + { + protected override void Dispose(bool disposing) + { + // Do nothing, since we want to keep the HttpClient instance alive. + } + } + } +} diff --git a/ProxyCheck/IProxyCheckCacheProvider.cs b/ProxyCheck/IProxyCheckCacheProvider.cs index 24539f1..60c64e1 100644 --- a/ProxyCheck/IProxyCheckCacheProvider.cs +++ b/ProxyCheck/IProxyCheckCacheProvider.cs @@ -5,7 +5,7 @@ namespace ProxyCheckUtil { public interface IProxyCheckCacheProvider { - ProxyCheckResult.IpResult GetCacheRecord(IPAddress ip, ProxyCheckRequestOptions options); + ProxyCheckResult.IpResult? GetCacheRecord(IPAddress ip, ProxyCheckRequestOptions options); IDictionary GetCacheRecords(IPAddress[] ipAddress, ProxyCheckRequestOptions options); diff --git a/ProxyCheck/Json/IpResultDictionary.cs b/ProxyCheck/Json/IpResultDictionary.cs new file mode 100644 index 0000000..ddf5ef7 --- /dev/null +++ b/ProxyCheck/Json/IpResultDictionary.cs @@ -0,0 +1,145 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.Json; + +namespace ProxyCheckUtil +{ + /// + /// Dictionary that deserializes IP addresses keys as and values as . + /// + internal class IpResultDictionary : IDictionary + { + private readonly Dictionary _extensionData = new(); + private readonly Dictionary _results; + + public IpResultDictionary(Dictionary results) + { + _results = results; + } + + public IEnumerator> GetEnumerator() + { + foreach (var kvp in _results) + { + yield return new KeyValuePair(kvp.Key.ToString(), JsonSerializer.SerializeToDocument(kvp.Value, ProxyJsonContext.Default.IpResult).RootElement); + } + + foreach (var kvp in _extensionData) + { + yield return kvp; + } + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Clear() + { + _results.Clear(); + _extensionData.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kvp in _results) + { + array[arrayIndex++] = new KeyValuePair(kvp.Key.ToString(), JsonSerializer.SerializeToDocument(kvp.Value, ProxyJsonContext.Default.IpResult).RootElement); + } + + foreach (var kvp in _extensionData) + { + array[arrayIndex++] = kvp; + } + } + + public bool Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + public int Count => _results.Count + _extensionData.Count; + + public bool IsReadOnly => false; + + public void Add(string key, JsonElement value) + { + if (IPAddress.TryParse(key, out var ip)) + { + _results.Add(ip, value.Deserialize(ProxyJsonContext.Default.IpResult)!); + } + else + { + _extensionData.Add(key, value); + } + } + + public bool ContainsKey(string key) + { + return IPAddress.TryParse(key, out var ip) + ? _results.ContainsKey(ip) + : _extensionData.ContainsKey(key); + } + + public bool Remove(string key) + { + return IPAddress.TryParse(key, out var ip) + ? _results.Remove(ip) + : _extensionData.Remove(key); + } + + public bool TryGetValue(string key, out JsonElement value) + { + if (_results.TryGetValue(IPAddress.Parse(key), out var result)) + { + value = JsonSerializer.SerializeToDocument(result, ProxyJsonContext.Default.IpResult).RootElement; + return true; + } + + value = default; + return false; + } + + public JsonElement this[string key] + { + get => IPAddress.TryParse(key, out var ip) + ? JsonSerializer.SerializeToDocument(_results[ip], ProxyJsonContext.Default.IpResult).RootElement + : _extensionData[key]; + + set + { + if (IPAddress.TryParse(key, out var ip)) + { + _results[ip] = value.Deserialize(ProxyJsonContext.Default.IpResult)!; + } + else + { + _extensionData[key] = value; + } + } + } + + ICollection IDictionary.Keys => _results.Keys + .Select(ip => ip.ToString()) + .Concat(_extensionData.Keys) + .ToList(); + + ICollection IDictionary.Values => _results.Values + .Select(result => JsonSerializer.SerializeToDocument(result, ProxyJsonContext.Default.IpResult).RootElement) + .Concat(_extensionData.Values) + .ToList(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/ProxyCheck/Json/ProxyJsonContext.cs b/ProxyCheck/Json/ProxyJsonContext.cs new file mode 100644 index 0000000..8aecc16 --- /dev/null +++ b/ProxyCheck/Json/ProxyJsonContext.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace ProxyCheckUtil +{ + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + Converters = new [] + { + typeof(JsonStringEnumConverter), + typeof(JsonStringEnumConverter) + } + )] + [JsonSerializable(typeof(ProxyCheckResult))] + [JsonSerializable(typeof(ProxyCheckResult.IpResult))] + internal partial class ProxyJsonContext : JsonSerializerContext + { + } +} diff --git a/ProxyCheck/Json/YesNoJsonConverter.cs b/ProxyCheck/Json/YesNoJsonConverter.cs new file mode 100644 index 0000000..f109b76 --- /dev/null +++ b/ProxyCheck/Json/YesNoJsonConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ProxyCheckUtil +{ + internal class YesNoJsonConverter : JsonConverter + { + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString() == "yes"; + } + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteStringValue(value ? "yes" : "no"); + } + } +} diff --git a/ProxyCheck/ProxyCheck.cs b/ProxyCheck/ProxyCheck.cs index 61f37c1..c200fd8 100644 --- a/ProxyCheck/ProxyCheck.cs +++ b/ProxyCheck/ProxyCheck.cs @@ -32,10 +32,9 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ProxyCheckUtil { @@ -45,27 +44,47 @@ public class ProxyCheck /// /// Creates the ProxyCheck object with optional API key and cache provider /// + /// + /// This constructor is obsolete and will be removed in the future. + /// Without the HttpClientFactory, the library will use a single HttpClient instance for all requests. + /// This is not recommended for production use, since it doesn't respect DNS changes. + /// /// API key to use /// Cache provider to use - public ProxyCheck(string apiKey = "", IProxyCheckCacheProvider cacheProvider = null) + [Obsolete("Use the constructor with IHttpClientFactory")] + public ProxyCheck(string apiKey = "", IProxyCheckCacheProvider? cacheProvider = null) + : this(new DefaultHttpClientFactory(), apiKey, cacheProvider) + { + } + + /// + /// Creates the ProxyCheck object with optional API key and cache provider + /// + /// HttpClient factory to use + /// API key to use + /// Cache provider to use + public ProxyCheck(IHttpClientFactory clientFactory, string? apiKey = "", IProxyCheckCacheProvider? cacheProvider = null) { if (apiKey == null) apiKey = string.Empty; ApiKey = apiKey; CacheProvider = cacheProvider; + _clientFactory = clientFactory; } + [Obsolete("Use the constructor with IHttpClientFactory")] public ProxyCheck(IProxyCheckCacheProvider cacheProvider) + : this("", cacheProvider) { - CacheProvider = cacheProvider; } private const string ProxyCheckUrl = "proxycheck.io/v2"; + + private ProxyCheckRequestOptions _options = new(); + private readonly IHttpClientFactory _clientFactory; - private ProxyCheckRequestOptions _options = new ProxyCheckRequestOptions(); - - public IProxyCheckCacheProvider CacheProvider { get; set; } + public IProxyCheckCacheProvider? CacheProvider { get; set; } /// /// The API key to use with the query @@ -181,7 +200,7 @@ public async Task QueryAsync(string ipAddress, string tag = "" if (ipAddress == null) throw new ArgumentNullException(nameof(ipAddress)); - if (!IPAddress.TryParse(ipAddress, out IPAddress ip)) + if (!IPAddress.TryParse(ipAddress, out var ip)) throw new ArgumentException("Must be a valid IP", nameof(ipAddress)); return await QueryAsync(ip, tag); @@ -217,7 +236,7 @@ public async Task QueryAsync(string[] ipAddresses, string tag List ips = new List(ipAddresses.Length); foreach (var ipString in ipAddresses) { - if (!IPAddress.TryParse(ipString, out IPAddress ip)) + if (!IPAddress.TryParse(ipString, out var ip)) throw new ArgumentException($"Invalid IP address provided. `{ipString}` is not a valid IP"); ips.Add(ip); @@ -241,7 +260,7 @@ public async Task QueryAsync(IPAddress[] ipAddresses, string t if (!ipAddresses.Any()) throw new ArgumentException("Must contain at least 1 IP Address", nameof(ipAddresses)); - IDictionary ipResults = null; + IDictionary? ipResults = null; if (CacheProvider != null) { sw.Start(); @@ -294,7 +313,7 @@ public async Task QueryAsync(IPAddress[] ipAddresses, string t .Append($"&days={Convert.ToInt32(DayLimit)}") .Append($"&risk={Convert.ToInt32(RiskLevel)}"); - using (var client = new HttpClient()) + using (var client = _clientFactory.CreateClient()) { Dictionary postData = new Dictionary(); @@ -309,10 +328,15 @@ public async Task QueryAsync(IPAddress[] ipAddresses, string t try { var response = await client.PostAsync(url.ToString(), content); + ProxyCheckResult? result; - string json = await response.Content.ReadAsStringAsync(); + using (var stream = await response.Content.ReadAsStreamAsync()) + { + result = await JsonSerializer.DeserializeAsync(stream, ProxyJsonContext.Default.ProxyCheckResult); + } - ProxyCheckResult result = ParseJson(json); + if (result == null) + throw new ProxyCheckException("No result from server"); // We want to update the cache now CacheProvider?.SetCacheRecord(result.Results, _options); @@ -335,7 +359,7 @@ public async Task QueryAsync(IPAddress[] ipAddresses, string t { throw new ProxyCheckException("URL should not be NULL", e); } - catch (JsonReaderException e) + catch (JsonException e) { throw new ProxyCheckException("Bad JSON from server", e); } @@ -344,123 +368,6 @@ public async Task QueryAsync(IPAddress[] ipAddresses, string t throw new ProxyCheckException("Unknown state please check the inner exception.", e); } } - - } - - /// - /// Parses the servers JSON response - /// - /// JSON to Parse - /// The parsed JSON - private ProxyCheckResult ParseJson(string json) - { - ProxyCheckResult res = new ProxyCheckResult(); - - JObject obj = JObject.Parse(json); - - foreach (var token in obj) - { - switch (token.Key) - { - case "status": - if (Enum.TryParse((string) token.Value, true, out StatusResult statusResult)) - { - res.Status = statusResult; - } - - break; - - case "node": - res.Node = (string) token.Value; - break; - - case "query time": - double secs = Convert.ToDouble(((string) token.Value).Substring(0, ((string) token.Value).Length - 1)); - TimeSpan ts = TimeSpan.FromSeconds(secs); - res.QueryTime = ts; - break; - - default: - if (IPAddress.TryParse(token.Key, out IPAddress ip)) - { - ProxyCheckResult.IpResult ipResult = new ProxyCheckResult.IpResult(); - - foreach (var innerToken in (JObject) token.Value) - { - switch (innerToken.Key) - { - case "asn": - ipResult.ASN = (string) innerToken.Value; - break; - - case "provider": - ipResult.Provider = (string) innerToken.Value; - break; - - case "country": - ipResult.Country = (string) innerToken.Value; - break; - - case "latitude": - ipResult.Latitude = Convert.ToDouble((string) innerToken.Value); - break; - - case "longitude": - ipResult.Longitude = Convert.ToDouble((string) innerToken.Value); - break; - - case "isocode": - ipResult.ISOCode = (string) innerToken.Value; - break; - - case "city": - ipResult.City = (string) innerToken.Value; - break; - - case "proxy": - string isProxy = (string) innerToken.Value; - ipResult.IsProxy = isProxy.Equals("yes", StringComparison.OrdinalIgnoreCase); - break; - - case "risk": - ipResult.RiskScore = Convert.ToInt32((string)innerToken.Value); - break; - - case "type": - ipResult.ProxyType = (string)innerToken.Value; - break; - - case "port": - ipResult.Port = Convert.ToInt32((string) innerToken.Value); - break; - - case "last seen human": - ipResult.LastSeenHuman = (string) innerToken.Value; - break; - - case "last seen unix": - ipResult.LastSeenUnix = Convert.ToInt64((string) innerToken.Value); - break; - - case "error": - ipResult.ErrorMessage = (string) innerToken.Value; - break; - - default: - Debug.WriteLine( - $"Unknown item present Key: {innerToken.Key}, Value:{innerToken.Value}"); - break; - } - } - - res.Results.Add(ip, ipResult); - } - - break; - } - } - - return res; } } } diff --git a/ProxyCheck/ProxyCheckRequestOptions.cs b/ProxyCheck/ProxyCheckRequestOptions.cs index 837cfe8..a57f68b 100644 --- a/ProxyCheck/ProxyCheckRequestOptions.cs +++ b/ProxyCheck/ProxyCheckRequestOptions.cs @@ -48,7 +48,7 @@ public class ProxyCheckRequestOptions /// public RiskLevel? RiskLevel { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (!(obj is ProxyCheckRequestOptions o)) return false; @@ -73,5 +73,18 @@ public override bool Equals(object obj) return IncludeLastSeen == o.IncludeLastSeen; } + + public override int GetHashCode() + { + var hash = 17; + hash = hash * 23 + IncludeVPN.GetHashCode(); + hash = hash * 23 + UseTLS.GetHashCode(); + hash = hash * 23 + IncludeASN.GetHashCode(); + hash = hash * 23 + UseInference.GetHashCode(); + hash = hash * 23 + IncludePort.GetHashCode(); + hash = hash * 23 + IncludeLastSeen.GetHashCode(); + hash = hash * 23 + RiskLevel.GetHashCode(); + return hash; + } } } \ No newline at end of file diff --git a/ProxyCheck/ProxyCheckResult.cs b/ProxyCheck/ProxyCheckResult.cs index 9a7cbb7..f971141 100644 --- a/ProxyCheck/ProxyCheckResult.cs +++ b/ProxyCheck/ProxyCheckResult.cs @@ -28,6 +28,8 @@ using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace ProxyCheckUtil @@ -35,24 +37,51 @@ namespace ProxyCheckUtil [PublicAPI] public class ProxyCheckResult { + private IpResultDictionary _resultsDictionary; + + public ProxyCheckResult() + { + Results = new Dictionary(); + _resultsDictionary = new IpResultDictionary(Results); + } + /// /// API status result /// + [JsonPropertyName("status")] public StatusResult Status { get; set; } /// /// Answering node /// - public string Node { get; set; } + [JsonPropertyName("node")] + public string? Node { get; set; } /// /// Dictionary of results for the IP address(es) provided /// - public Dictionary Results { get; internal set; } = new Dictionary(); + [JsonIgnore] + public Dictionary Results { get; internal set; } + + [JsonExtensionData] + public IDictionary ExtensionData + { + get => _resultsDictionary; + set + { + _resultsDictionary.Clear(); + + foreach (var kv in value) + { + _resultsDictionary.Add(kv); + } + } + } /// /// The amount of time the query took on the server /// + [JsonPropertyName("query time")] public TimeSpan? QueryTime { get; set; } [PublicAPI] @@ -61,17 +90,20 @@ public class IpResult /// /// The ASN the IP address belongs to /// - public string ASN { get; set; } + [JsonPropertyName("asn")] + public string? ASN { get; set; } /// /// The provider the IP address belongs to /// - public string Provider { get; set; } + [JsonPropertyName("provider")] + public string? Provider { get; set; } /// /// The country the IP address is in. /// - public string Country { get; set; } + [JsonPropertyName("country")] + public string? Country { get; set; } /// /// The latitude of the IP address @@ -79,56 +111,69 @@ public class IpResult /// /// This is not the exact location of the IP address /// + [JsonPropertyName("latitude")] public double? Latitude { get; set; } + /// /// The longitude of the IP address /// /// /// This is not the exact location of the IP address /// + [JsonPropertyName("longitude")] public double? Longitude { get; set; } + /// /// The city the of the IP address /// /// /// This may not be the exact city /// - public string City { get; set; } + [JsonPropertyName("city")] + public string? City { get; set; } /// /// ISO Country code of the IP address country /// - public string ISOCode { get; set; } + [JsonPropertyName("isocode")] + public string? ISOCode { get; set; } /// /// True if the IP is detected as proxy /// False otherwise /// + [JsonPropertyName("proxy")] + [JsonConverter(typeof(YesNoJsonConverter))] public bool IsProxy { get; set; } /// /// The type of proxy detected /// - public string ProxyType { get; set; } + [JsonPropertyName("type")] + public string ProxyType { get; set; } = ""; /// /// The port the proxy server is operating on /// + [JsonPropertyName("port")] public int? Port { get; set; } /// /// Not null when risk is > 0, the risk score of the IP address /// + [JsonPropertyName("risk")] public int? RiskScore { get; set; } /// /// The last time the proxy server was seen in human readable format. /// - public string LastSeenHuman { get; set; } + [JsonPropertyName("last seen human")] + public string? LastSeenHuman { get; set; } /// /// The last time the proxy server was seen in Unix time stamp /// + [JsonPropertyName("last seen unix")] public long? LastSeenUnix { get; set; } /// @@ -149,7 +194,7 @@ public DateTimeOffset? LastSeen /// /// If not `null` the description of the error that occured /// - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } /// /// True if this item was retrieved from cache diff --git a/ProxyCheck/ProxyCheckUtil.csproj b/ProxyCheck/ProxyCheckUtil.csproj index 394bc18..23196c1 100644 --- a/ProxyCheck/ProxyCheckUtil.csproj +++ b/ProxyCheck/ProxyCheckUtil.csproj @@ -5,9 +5,11 @@ - netstandard2.0 + netstandard2.0;net8.0 + enable exe + 9.0 true false @@ -15,7 +17,7 @@ Checks if an IP address is a public proxy or not using ProxyCheck.IO Public Domain - https://github.com/hollow87/ProxyCheck/blob/master/LICENSE + https://github.com/hollow87/ProxyCheck/blob/master/LICENSE https://github.com/hollow87/ProxyCheck https://github.com/hollow87/ProxyCheck git @@ -31,14 +33,15 @@ 2.0.1.0 2.0.1.0 - - - C:\Users\Mikey\Source\Repos\ProxyCheck\ProxyCheck\ProxyCheckUtil.xml + + + true - + + \ No newline at end of file diff --git a/ProxyCheck/ProxyCheckUtil.csproj.DotSettings b/ProxyCheck/ProxyCheckUtil.csproj.DotSettings new file mode 100644 index 0000000..02310b8 --- /dev/null +++ b/ProxyCheck/ProxyCheckUtil.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/ProxyCheck/SimpleInMemoryCache.cs b/ProxyCheck/SimpleInMemoryCache.cs index 7a2f9d2..70a526f 100644 --- a/ProxyCheck/SimpleInMemoryCache.cs +++ b/ProxyCheck/SimpleInMemoryCache.cs @@ -12,10 +12,10 @@ public class SimpleInMemoryCache : IProxyCheckCacheProvider private class CacheItem { - public IPAddress IPAddress { get; set; } - public ProxyCheckRequestOptions Options { get; set; } + public IPAddress IPAddress { get; set; } = null!; + public ProxyCheckRequestOptions Options { get; set; } = null!; - public ProxyCheckResult.IpResult Result { get; set; } + public ProxyCheckResult.IpResult Result { get; set; } = null!; public DateTimeOffset Time { get; set; } @@ -31,7 +31,7 @@ public SimpleInMemoryCache(TimeSpan maxCacheAge) _maxCacheAge = maxCacheAge; } - public ProxyCheckResult.IpResult GetCacheRecord(IPAddress ip, ProxyCheckRequestOptions options) + public ProxyCheckResult.IpResult? GetCacheRecord(IPAddress ip, ProxyCheckRequestOptions options) { var results = GetCacheRecords(new[] {ip}, options); diff --git a/ProxyCheckUtil.Tests/DeserializeTest.cs b/ProxyCheckUtil.Tests/DeserializeTest.cs new file mode 100644 index 0000000..39e103d --- /dev/null +++ b/ProxyCheckUtil.Tests/DeserializeTest.cs @@ -0,0 +1,114 @@ +using System.Net; +using NSubstitute; + +namespace ProxyCheckUtil.Tests; + +public class DeserializeTest +{ + [Fact] + public async Task TestSuccess() + { + const string json = + """ + { + "status": "ok", + "104.16.255.200": { + "asn": "AS13335", + "range": "104.16.0.0/16", + "provider": "CLOUDFLARENET - Cloudflare, Inc., US", + "organisation": "Cloudflare, Inc.", + "continent": "North America", + "continentcode": "NA", + "country": "Canada", + "isocode": "CA", + "region": "Ontario", + "regioncode": "ON", + "timezone": "America/Toronto", + "city": "Toronto", + "postcode": "M5A", + "latitude": 43.6532, + "longitude": -79.3832, + "currency": { + "code": "CAD", + "name": "Dollar", + "symbol": "CA$" + }, + "devices": { + "address": 0, + "subnet": 0 + }, + "proxy": "no", + "type": "Business" + } + } + """; + + // Arrange + var handler = Substitute.For(); + handler + .SetupRequest(HttpMethod.Post, "https://proxycheck.io/v2/&vpn=0&asn=0&node=0&time=0&inf=1&port=0&seen=0&days=7&risk=0") + .ReturnsResponse(HttpStatusCode.OK, json); + + using var client = new HttpClient(handler); + + var httpClientFactory = Substitute.For(); + httpClientFactory.CreateClient().Returns(client); + + var proxyCheck = new ProxyCheck(httpClientFactory) + { + UseTLS = true + }; + + // Act + var result = await proxyCheck.QueryAsync("104.16.255.200"); + + // Assert + Assert.Equal(StatusResult.OK, result.Status); + Assert.Single(result.Results); + + var ipResult = result.Results.First().Value; + Assert.Equal("AS13335", ipResult.ASN); + Assert.Equal("CLOUDFLARENET - Cloudflare, Inc., US", ipResult.Provider); + Assert.Equal("Canada", ipResult.Country); + Assert.Equal(43.6532, ipResult.Latitude); + Assert.Equal(-79.3832, ipResult.Longitude); + Assert.Equal("Toronto", ipResult.City); + Assert.Equal("CA", ipResult.ISOCode); + Assert.False(ipResult.IsProxy); + Assert.Equal("Business", ipResult.ProxyType); + } + + [Fact] + public async Task ExtensionsTest() + { + const string json = + """ + { + "foo": "bar" + } + """; + + // Arrange + var handler = Substitute.For(); + handler + .SetupRequest(HttpMethod.Post, "https://proxycheck.io/v2/&vpn=0&asn=0&node=0&time=0&inf=1&port=0&seen=0&days=7&risk=0") + .ReturnsResponse(HttpStatusCode.OK, json); + + using var client = new HttpClient(handler); + + var httpClientFactory = Substitute.For(); + httpClientFactory.CreateClient().Returns(client); + + var proxyCheck = new ProxyCheck(httpClientFactory) + { + UseTLS = true + }; + + // Act + var result = await proxyCheck.QueryAsync("104.16.255.200"); + + // Assert + Assert.Single(result.ExtensionData); + Assert.Equal("bar", result.ExtensionData["foo"].GetString()); + } +} diff --git a/ProxyCheckUtil.Tests/NSubstituteExtensions.cs b/ProxyCheckUtil.Tests/NSubstituteExtensions.cs new file mode 100644 index 0000000..fe53168 --- /dev/null +++ b/ProxyCheckUtil.Tests/NSubstituteExtensions.cs @@ -0,0 +1,52 @@ +using System.Net; +using System.Net.Http.Json; +using System.Reflection; +using NSubstitute; +using NSubstitute.Core; + +namespace ProxyCheckUtil.Tests; + +public static class NSubstituteExtensions +{ + public static HttpMessageHandler SetupRequest(this HttpMessageHandler handler, HttpMethod method, string requestUri) + { + handler + .GetType() + .GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)! + .Invoke(handler, [ + Arg.Is(x => + x.Method == method && + x.RequestUri != null && + x.RequestUri.ToString() == requestUri), + Arg.Any() + ]); + + return handler; + } + + public static ConfiguredCall ReturnsResponse(this HttpMessageHandler handler, HttpStatusCode statusCode, string? jsonContent = null) + { + return ((object)handler).Returns( + Task.FromResult(new HttpResponseMessage() + { + StatusCode = statusCode, + Content = jsonContent != null ? new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json") : null + }) + ); + } + + public static void ShouldHaveReceived(this HttpMessageHandler handler, HttpMethod requestMethod, string requestUri, int timesCalled = 1) + { + var calls = handler.ReceivedCalls() + .Where(call => call.GetMethodInfo().Name == "SendAsync") + .Select(call => call.GetOriginalArguments().First()) + .Cast() + .Where(request => + request.Method == requestMethod && + request.RequestUri != null && + request.RequestUri.ToString() == requestUri + ); + + Assert.Equal(timesCalled, calls.Count()); + } +} \ No newline at end of file diff --git a/ProxyCheckUtil.Tests/ProxyCheckUtil.Tests.csproj b/ProxyCheckUtil.Tests/ProxyCheckUtil.Tests.csproj new file mode 100644 index 0000000..8aac9cf --- /dev/null +++ b/ProxyCheckUtil.Tests/ProxyCheckUtil.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + +