From cc89def411e7c7987b370973e2282f1e9c92f9c5 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 16 Jan 2026 16:41:19 +0100 Subject: [PATCH] Add resproxy support --- IPinfo.Tests/IPApiTest.cs | 52 ++++++++++++--- src/IPinfo/Apis/IPApi.cs | 88 +++++++++++++++++++++++++ src/IPinfo/Models/IPResponseResproxy.cs | 39 +++++++++++ 3 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 src/IPinfo/Models/IPResponseResproxy.cs diff --git a/IPinfo.Tests/IPApiTest.cs b/IPinfo.Tests/IPApiTest.cs index d698056..69c5dd9 100644 --- a/IPinfo.Tests/IPApiTest.cs +++ b/IPinfo.Tests/IPApiTest.cs @@ -15,7 +15,7 @@ public void TestGetDetails() IPinfoClient client = new IPinfoClient.Builder() .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) .Build(); - + IPResponse actual = client.IPApi.GetDetails(ip); var expectations = new List>() @@ -43,7 +43,7 @@ public void TestGetDetails() Assert.False(actual.Privacy.Vpn); Assert.False(actual.Privacy.Tor); Assert.False(actual.Privacy.Relay); - Assert.True(actual.Privacy.Hosting); + Assert.True(actual.Privacy.Hosting); } [Fact] @@ -53,11 +53,11 @@ public void TestBogonIPV4() IPinfoClient client = new IPinfoClient.Builder() .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) .Build(); - + IPResponse actual = client.IPApi.GetDetails(ip); Assert.Equal("127.0.0.1", actual.IP); - Assert.True(actual.Bogon); + Assert.True(actual.Bogon); } [Fact] @@ -67,11 +67,11 @@ public void TestBogonIPV6() IPinfoClient client = new IPinfoClient.Builder() .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) .Build(); - + IPResponse actual = client.IPApi.GetDetails(ip); Assert.Equal("2001:0:c000:200::0:255:1", actual.IP); - Assert.True(actual.Bogon); + Assert.True(actual.Bogon); } [Fact] @@ -81,11 +81,11 @@ public void TestNonBogonIPV4() IPinfoClient client = new IPinfoClient.Builder() .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) .Build(); - + IPResponse actual = client.IPApi.GetDetails(ip); Assert.Equal("1.1.1.1", actual.IP); - Assert.False(actual.Bogon); + Assert.False(actual.Bogon); } [Fact] @@ -95,11 +95,43 @@ public void TestNonBogonIPV6() IPinfoClient client = new IPinfoClient.Builder() .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) .Build(); - + IPResponse actual = client.IPApi.GetDetails(ip); Assert.Equal("2a03:2880:f10a:83:face:b00c:0:25de", actual.IP); - Assert.False(actual.Bogon); + Assert.False(actual.Bogon); + } + + [Fact] + public void TestGetResproxy() + { + string ip = "175.107.211.204"; + IPinfoClient client = new IPinfoClient.Builder() + .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) + .Build(); + + IPResponseResproxy actual = client.IPApi.GetResproxy(ip); + + Assert.Equal("175.107.211.204", actual.IP); + Assert.NotNull(actual.LastSeen); + Assert.NotNull(actual.PercentDaysSeen); + Assert.NotNull(actual.Service); + } + + [Fact] + public void TestGetResproxyNotFound() + { + string ip = "8.8.8.8"; + IPinfoClient client = new IPinfoClient.Builder() + .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN")) + .Build(); + + IPResponseResproxy actual = client.IPApi.GetResproxy(ip); + + Assert.Null(actual.IP); + Assert.Null(actual.LastSeen); + Assert.Null(actual.PercentDaysSeen); + Assert.Null(actual.Service); } } } diff --git a/src/IPinfo/Apis/IPApi.cs b/src/IPinfo/Apis/IPApi.cs index 8dce008..df54656 100644 --- a/src/IPinfo/Apis/IPApi.cs +++ b/src/IPinfo/Apis/IPApi.cs @@ -223,5 +223,93 @@ public Models.IPResponse GetDetailsV6(string ipv6Address) return responseModel; } + /// + /// Retrieves residential proxy details for an IP address. + /// + /// The IP address to check for residential proxy information. + /// Returns the Models.IPResponseResproxy response from the API call. + public Models.IPResponseResproxy GetResproxy( + IPAddress ipAddress) + { + string ipString = ipAddress?.ToString(); + return this.GetResproxy(ipString); + } + + /// + /// Retrieves residential proxy details for an IP address. + /// + /// The IP address to check for residential proxy information. + /// Returns the Models.IPResponseResproxy response from the API call. + public Models.IPResponseResproxy GetResproxy( + string ipAddress) + { + Task t = this.GetResproxyAsync(ipAddress); + ApiHelper.RunTaskSynchronously(t); + return t.Result; + } + + /// + /// Retrieves residential proxy details for an IP address. + /// + /// The IP address to check for residential proxy information. + /// Cancellation token if the request is cancelled. + /// Returns the Models.IPResponseResproxy response from the API call. + public Task GetResproxyAsync( + IPAddress ipAddress, + CancellationToken cancellationToken = default) + { + string ipString = ipAddress?.ToString(); + return this.GetResproxyAsync(ipString, cancellationToken); + } + + /// + /// Retrieves residential proxy details for an IP address. + /// + /// The IP address to check for residential proxy information. + /// Cancellation token if the request is cancelled. + /// Returns the Models.IPResponseResproxy response from the API call. + public async Task GetResproxyAsync( + string ipAddress, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(ipAddress)) + { + return null; + } + + string cacheKey = "resproxy:" + ipAddress; + + // first check the data in the cache if cache is available + IPResponseResproxy resproxyResponse = (IPResponseResproxy)GetFromCache(cacheKey); + if (resproxyResponse != null) + { + return resproxyResponse; + } + + // prepare query string for API call. + StringBuilder queryBuilder = new StringBuilder(this.BaseUrl); + queryBuilder.Append("resproxy/{ip_address}"); + // process optional template parameters. + ApiHelper.AppendUrlWithTemplateParameters(queryBuilder, new Dictionary() + { + { "ip_address", ipAddress }, + }); + + // prepare the API call request to fetch the response. + HttpRequest httpRequest = this.CreateGetRequest(queryBuilder.ToString()); + + // invoke request and get response. + HttpStringResponse response = await this.GetClientInstance().ExecuteAsStringAsync(httpRequest, cancellationToken).ConfigureAwait(false); + HttpContext context = new HttpContext(httpRequest, response); + + // handle errors defined at the API level. + this.ValidateResponse(context); + + var responseModel = System.Text.Json.JsonSerializer.Deserialize(response.Body); + + SetInCache(cacheKey, responseModel); + return responseModel; + } + } } diff --git a/src/IPinfo/Models/IPResponseResproxy.cs b/src/IPinfo/Models/IPResponseResproxy.cs new file mode 100644 index 0000000..49fe1d1 --- /dev/null +++ b/src/IPinfo/Models/IPResponseResproxy.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace IPinfo.Models +{ + /// + /// Residential proxy detection response. + /// + public class IPResponseResproxy + { + /// + /// The IP address. + /// + [JsonPropertyName("ip")] + public string IP { get; set; } + + /// + /// The last time this IP was seen as a residential proxy. + /// + [JsonPropertyName("last_seen")] + public string LastSeen { get; set; } + + /// + /// The percentage of days seen as a residential proxy. + /// + [JsonPropertyName("percent_days_seen")] + public double? PercentDaysSeen { get; set; } + + /// + /// The residential proxy service name. + /// + [JsonPropertyName("service")] + public string Service { get; set; } + + // immutable type + [JsonConstructor] + public IPResponseResproxy(string ip, string lastSeen, double? percentDaysSeen, string service) => + (IP, LastSeen, PercentDaysSeen, Service) = (ip, lastSeen, percentDaysSeen, service); + } +}