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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+