Skip to content
Merged
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Run tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.x"

- name: Create Strong Name Keypair
run: echo "${{ secrets.SNK_BASE64 }}" | base64 --decode > sgKeyIPinfoStrongName.snk

- name: Restore dependencies
run: dotnet restore

- name: Test
run: dotnet test
env:
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}

- name: Build
run: dotnet build --configuration Release /p:AssemblyOriginatorKeyFile=sgKeyIPinfoStrongName.snk
153 changes: 153 additions & 0 deletions IPinfo.Tests/IPApiLiteTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using Xunit;

using IPinfo.Models;

namespace IPinfo.Tests
{
public class IPApiLiteTest
{
[Fact]
public void TestGetDetails()
{
string ip = "8.8.8.8";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

var expectations = new List<Tuple<object, object>>()
{
new("8.8.8.8", actual.IP),
new("AS15169", actual.Asn),
new("Google LLC", actual.AsName),
new("google.com", actual.AsDomain),
new("US", actual.CountryCode),
new("United States", actual.Country),
new("United States", actual.CountryName),
new(false, actual.IsEU),
new("🇺🇸", actual.CountryFlag.Emoji),
new("U+1F1FA U+1F1F8", actual.CountryFlag.Unicode),
new("https://cdn.ipinfo.io/static/images/countries-flags/US.svg", actual.CountryFlagURL),
new("USD", actual.CountryCurrency.Code),
new("$", actual.CountryCurrency.Symbol),
new("NA", actual.Continent.Code),
new("North America", actual.Continent.Name),
};
Assert.All(expectations, pair => Assert.Equal(pair.Item1, pair.Item2));
}

[Fact]
public void TestGetDetailsIPV6()
{
string ip = "2001:4860:4860::8888";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

var expectations = new List<Tuple<object, object>>()
{
new("2001:4860:4860::8888", actual.IP),
new("AS15169", actual.Asn),
new("Google LLC", actual.AsName),
new("google.com", actual.AsDomain),
new("US", actual.CountryCode),
new("United States", actual.Country),
new("United States", actual.CountryName),
new(false, actual.IsEU),
new("🇺🇸", actual.CountryFlag.Emoji),
new("U+1F1FA U+1F1F8", actual.CountryFlag.Unicode),
new("https://cdn.ipinfo.io/static/images/countries-flags/US.svg", actual.CountryFlagURL),
new("USD", actual.CountryCurrency.Code),
new("$", actual.CountryCurrency.Symbol),
new("NA", actual.Continent.Code),
new("North America", actual.Continent.Name),
};
Assert.All(expectations, pair => Assert.Equal(pair.Item1, pair.Item2));
}

[Fact]
public void TestGetDetailsCurrentIP()
{
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails();

Assert.NotNull(actual.IP);
Assert.NotNull(actual.Asn);
Assert.NotNull(actual.AsName);
Assert.NotNull(actual.AsDomain);
Assert.NotNull(actual.CountryCode);
Assert.NotNull(actual.Country);
Assert.NotNull(actual.CountryName);
Assert.NotNull(actual.CountryFlag);
Assert.NotNull(actual.CountryFlag.Emoji);
Assert.NotNull(actual.CountryFlag.Unicode);
Assert.NotNull(actual.CountryFlagURL);
Assert.NotNull(actual.CountryCurrency);
Assert.NotNull(actual.Continent);
}

[Fact]
public void TestBogonIPV4()
{
string ip = "127.0.0.1";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

Assert.Equal("127.0.0.1", actual.IP);
Assert.True(actual.Bogon);
}

[Fact]
public void TestBogonIPV6()
{
string ip = "2001:0:c000:200::0:255:1";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

Assert.Equal("2001:0:c000:200::0:255:1", actual.IP);
Assert.True(actual.Bogon);
}

[Fact]
public void TestNonBogonIPV4()
{
string ip = "1.1.1.1";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

Assert.Equal("1.1.1.1", actual.IP);
Assert.False(actual.Bogon);
}

[Fact]
public void TestNonBogonIPV6()
{
string ip = "2a03:2880:f10a:83:face:b00c:0:25de";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponseLite actual = client.IPApi.GetDetails(ip);

Assert.Equal("2a03:2880:f10a:83:face:b00c:0:25de", actual.IP);
Assert.False(actual.Bogon);
}
}
}
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr

The free plan is limited to 50,000 requests per month, and doesn't include some of the data fields such as IP type and company data. To enable all the data fields and additional request volumes see [https://ipinfo.io/pricing](https://ipinfo.io/pricing)

⚠️ Note: This SDK does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request.
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.

### Installation

Expand Down Expand Up @@ -126,6 +126,33 @@ Console.WriteLine($"IPResponse.Continent.Code: {ipResponse.Continent.Code}");
Console.WriteLine($"IPResponse.Continent.Name: {ipResponse.Continent.Name}");
```

### Lite API

The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.

The returned details are slightly different from the Core API.

```csharp
// namespace
using IPinfo;
using IPinfo.Models;

// initializing IPinfo client for Lite API
string token = "MY_TOKEN";
IPinfoClientLite client = new IPinfoClientLite.Builder()
.AccessToken(token)
.Build();

// making API call
string ip = "216.239.36.21";
IPResponseLite ipResponse = await client.IPApi.GetDetailsAsync(ip);

// accessing details from response
Console.WriteLine($"IPResponse.IP: {ipResponse.IP}");
Console.WriteLine($"IPResponse.Country: {ipResponse.Country}");
Console.WriteLine($"IPResponse.CountryName: {ipResponse.CountryName}");
```

### Caching

In-memory caching of data is provided by default. Custom implementation of the cache can also be provided by implementing the `ICache` interface.
Expand Down
2 changes: 1 addition & 1 deletion src/IPinfo/Apis/BaseApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal BaseApi(
/// <summary>
/// Gets base url values.
/// </summary>
internal string BaseUrl => DefaultBaseUrl;
protected string BaseUrl { get; set; } = DefaultBaseUrl;
internal string BaseUrlIPv6 => DefaultBaseUrlIPv6;

/// <summary>
Expand Down
118 changes: 118 additions & 0 deletions src/IPinfo/Apis/IPApiLite.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

using IPinfo.Utilities;
using IPinfo.Http.Client;
using IPinfo.Http.Request;
using IPinfo.Models;
using IPinfo.Http.Response;
using IPinfo.Cache;

namespace IPinfo.Apis
{
/// <summary>
/// IPApiLite.
/// </summary>
public sealed class IPApiLite : BaseApi
{
/// <summary>
/// Initializes a new instance of the <see cref="IPApiLite"/> class.
/// </summary>
/// <param name="httpClient"> httpClient. </param>
/// <param name="token"> token. </param>
internal IPApiLite(IHttpClient httpClient, string token, CacheHandler cacheHandler)
: base(httpClient, token, cacheHandler)
{
this.BaseUrl = "https://api.ipinfo.io/lite/";
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <returns>Returns the Models.IPResponseLite response from the API call.</returns>
public Models.IPResponseLite GetDetails(
IPAddress ipAddress)
{
string ipString = ipAddress?.ToString();
return this.GetDetails(ipString);
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <returns>Returns the Models.IPResponseLite response from the API call.</returns>
public Models.IPResponseLite GetDetails(
string ipAddress = "")
{
Task<Models.IPResponseLite> t = this.GetDetailsAsync(ipAddress);
ApiHelper.RunTaskSynchronously(t);
return t.Result;
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <param name="cancellationToken">Cancellation token if the request is cancelled. </param>
/// <returns>Returns the Models.IPResponseLite response from the API call.</returns>
public Task<Models.IPResponseLite> GetDetailsAsync(
IPAddress ipAddress,
CancellationToken cancellationToken = default)
{
string ipString = ipAddress?.ToString();
return this.GetDetailsAsync(ipString, cancellationToken);
}


/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <param name="cancellationToken">Cancellation token if the request is cancelled. </param>
/// <returns>Returns the Models.IPResponseLite response from the API call.</returns>
public async Task<Models.IPResponseLite> GetDetailsAsync(
string ipAddress = "",
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(ipAddress))
{
ipAddress = "me";
}
// first check the data in the cache if cache is available
IPResponseLite ipResponse = (IPResponseLite)GetFromCache(ipAddress);
if (ipResponse != null)
{
return ipResponse;
}

if (BogonHelper.IsBogon(ipAddress))
{
ipResponse = new IPResponseLite()
{
IP = ipAddress,
Bogon = true
};
return ipResponse;
}

// prepare the API call request to fetch the response.
HttpRequest httpRequest = this.CreateGetRequest(this.BaseUrl + ipAddress);
// 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 = JsonHelper.ParseIPResponseLite(response.Body);

SetInCache(ipAddress, responseModel);
return responseModel;
}
}
}
5 changes: 3 additions & 2 deletions src/IPinfo/IPinfo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@

<ItemGroup>
<PackageReference Include="System.Runtime.Caching" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>IPinfo.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a3e25284f42d5a26fdeb222c8fb9ac5df0b7421bdf350a8e828c18f72f1ec06fec81eff098d3610ebd64ec3c3b9b66f3801fc3d38fde5968d04c46b319896074cdae04c20a5c4d2dc5438e259265617106567f7357b6daf0fbf301fbe43df5019c53c5668b231423e23411e6e4c4cfae2b0b2e0f6e1333e0fe2cd691c26717d4</_Parameter1>
</AssemblyAttribute>

</ItemGroup>

<ItemGroup>
Expand Down
Loading