diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3290d2a --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/IPinfo.Tests/IPApiLiteTest.cs b/IPinfo.Tests/IPApiLiteTest.cs new file mode 100644 index 0000000..c58c5ee --- /dev/null +++ b/IPinfo.Tests/IPApiLiteTest.cs @@ -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>() + { + 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>() + { + 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); + } + } +} diff --git a/README.md b/README.md index 5e44f01..f8c7d4c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/src/IPinfo/Apis/BaseApi.cs b/src/IPinfo/Apis/BaseApi.cs index aee89a6..6830a4f 100644 --- a/src/IPinfo/Apis/BaseApi.cs +++ b/src/IPinfo/Apis/BaseApi.cs @@ -60,7 +60,7 @@ internal BaseApi( /// /// Gets base url values. /// - internal string BaseUrl => DefaultBaseUrl; + protected string BaseUrl { get; set; } = DefaultBaseUrl; internal string BaseUrlIPv6 => DefaultBaseUrlIPv6; /// diff --git a/src/IPinfo/Apis/IPApiLite.cs b/src/IPinfo/Apis/IPApiLite.cs new file mode 100644 index 0000000..3e6c994 --- /dev/null +++ b/src/IPinfo/Apis/IPApiLite.cs @@ -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 +{ + /// + /// IPApiLite. + /// + public sealed class IPApiLite : BaseApi + { + /// + /// Initializes a new instance of the class. + /// + /// httpClient. + /// token. + internal IPApiLite(IHttpClient httpClient, string token, CacheHandler cacheHandler) + : base(httpClient, token, cacheHandler) + { + this.BaseUrl = "https://api.ipinfo.io/lite/"; + } + + /// + /// Retrieves details of an IP address. + /// + /// The IP address of the user to retrieve details for. + /// Returns the Models.IPResponseLite response from the API call. + public Models.IPResponseLite GetDetails( + IPAddress ipAddress) + { + string ipString = ipAddress?.ToString(); + return this.GetDetails(ipString); + } + + /// + /// Retrieves details of an IP address. + /// + /// The IP address of the user to retrieve details for. + /// Returns the Models.IPResponseLite response from the API call. + public Models.IPResponseLite GetDetails( + string ipAddress = "") + { + Task t = this.GetDetailsAsync(ipAddress); + ApiHelper.RunTaskSynchronously(t); + return t.Result; + } + + /// + /// Retrieves details of an IP address. + /// + /// The IP address of the user to retrieve details for. + /// Cancellation token if the request is cancelled. + /// Returns the Models.IPResponseLite response from the API call. + public Task GetDetailsAsync( + IPAddress ipAddress, + CancellationToken cancellationToken = default) + { + string ipString = ipAddress?.ToString(); + return this.GetDetailsAsync(ipString, cancellationToken); + } + + + /// + /// Retrieves details of an IP address. + /// + /// The IP address of the user to retrieve details for. + /// Cancellation token if the request is cancelled. + /// Returns the Models.IPResponseLite response from the API call. + public async Task 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; + } + } +} diff --git a/src/IPinfo/IPinfo.csproj b/src/IPinfo/IPinfo.csproj index 2ce4d7a..4e6dbb1 100644 --- a/src/IPinfo/IPinfo.csproj +++ b/src/IPinfo/IPinfo.csproj @@ -35,13 +35,14 @@ - + - + <_Parameter1>IPinfo.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a3e25284f42d5a26fdeb222c8fb9ac5df0b7421bdf350a8e828c18f72f1ec06fec81eff098d3610ebd64ec3c3b9b66f3801fc3d38fde5968d04c46b319896074cdae04c20a5c4d2dc5438e259265617106567f7357b6daf0fbf301fbe43df5019c53c5668b231423e23411e6e4c4cfae2b0b2e0f6e1333e0fe2cd691c26717d4 + diff --git a/src/IPinfo/IPinfoClientLite.cs b/src/IPinfo/IPinfoClientLite.cs new file mode 100644 index 0000000..d39e620 --- /dev/null +++ b/src/IPinfo/IPinfoClientLite.cs @@ -0,0 +1,120 @@ +using System; + +using IPinfo.Http.Client; +using IPinfo.Apis; +using IPinfo.Cache; +using IPinfo.Utilities; + +namespace IPinfo +{ + /// + /// The gateway for IPinfo SDK. This class holds the configuration of the SDK. + /// + public sealed class IPinfoClientLite + { + private readonly IHttpClient _httpClient; + private readonly CacheHandler _cacheHandler; + private readonly Lazy _ipApi; + + private IPinfoClientLite( + string accessToken, + IHttpClient httpClient, + CacheHandler cacheHandler, + IHttpClientConfiguration httpClientConfiguration) + { + this._httpClient = httpClient; + this._cacheHandler = cacheHandler; + this.HttpClientConfiguration = httpClientConfiguration; + + this._ipApi = new Lazy( + () => new IPApiLite(this._httpClient, accessToken, cacheHandler)); + } + + /// + /// Gets IPApiLite. + /// + public IPApiLite IPApi => this._ipApi.Value; + + /// + /// Gets the configuration of the Http Client associated with this client. + /// + public IHttpClientConfiguration HttpClientConfiguration { get; } + + /// + /// Gets the configuration of the Http Client associated with this client. + /// + public ICache Cache { get => _cacheHandler?.Cache; } + + /// + /// Builder class. + /// + public class Builder + { + private string _accessToken = ""; + private HttpClientConfiguration.Builder _httpClientConfig = new HttpClientConfiguration.Builder(); + private IHttpClient _httpClient; + private CacheHandler _cacheHandler = new CacheHandler(); + + /// + /// Sets credentials for BearerAuth. + /// + /// AccessToken. + /// Builder. + public Builder AccessToken(string accessToken) + { + this._accessToken = accessToken; + return this; + } + + /// + /// Sets HttpClientConfig. + /// + /// Action. + /// Builder. + public Builder HttpClientConfig(Action action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + action(this._httpClientConfig); + return this; + } + + /// + /// Sets the ICache implementation for the Builder. + /// + /// ICache implementation. Pass null to disable the cache. + /// Builder. + public Builder Cache(ICache cache) + { + // Null is allowed here, which is being used to indicate that user do not want the cache. + if(cache == null) + { + this._cacheHandler = null; + } + else + { + this._cacheHandler = new CacheHandler(cache); + } + return this; + } + + /// + /// Creates an object of the IPinfoClientLite using the values provided for the builder. + /// + /// IPinfoClientLite. + public IPinfoClientLite Build() + { + this._httpClient = new HttpClientWrapper(this._httpClientConfig.Build()); + + return new IPinfoClientLite( + this._accessToken, + this._httpClient, + this._cacheHandler, + this._httpClientConfig.Build()); + } + } + } +} diff --git a/src/IPinfo/Models/IPResponseLite.cs b/src/IPinfo/Models/IPResponseLite.cs new file mode 100644 index 0000000..4c83275 --- /dev/null +++ b/src/IPinfo/Models/IPResponseLite.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; + +namespace IPinfo.Models +{ + public class IPResponseLite + { + [JsonInclude] + public bool Anycast { get; private set; } + + [JsonInclude] + public bool Bogon { get; internal set; } + + [JsonInclude] + public string Country { get; private set; } + + [JsonPropertyName("country_code")] + [JsonInclude] + public string CountryCode { get; private set; } + + public string CountryName { get; internal set; } + + public bool IsEU { get; internal set; } + + public CountryFlag CountryFlag { get; internal set; } + + public string CountryFlagURL { get; internal set; } + + public CountryCurrency CountryCurrency { get; internal set; } + + public Continent Continent { get; internal set; } + + [JsonInclude] + public string IP { get; internal set; } + + [JsonInclude] + public string Asn { get; private set; } + + [JsonPropertyName("as_name")] + [JsonInclude] + public string AsName {get; private set; } + + [JsonPropertyName("as_domain")] + [JsonInclude] + public string AsDomain {get; private set; } + } +} diff --git a/src/IPinfo/Utilities/JsonHelper.cs b/src/IPinfo/Utilities/JsonHelper.cs index fff5ac6..201d126 100644 --- a/src/IPinfo/Utilities/JsonHelper.cs +++ b/src/IPinfo/Utilities/JsonHelper.cs @@ -25,18 +25,18 @@ internal static T Deserialize(string json, JsonSerializerOptions options = nu { return default; } - + if(options is null) { options = new JsonSerializerOptions { PropertyNameCaseInsensitive = DefaultCaseInsensitive - }; + }; } - return JsonSerializer.Deserialize(json, options); + return JsonSerializer.Deserialize(json, options); } - + /// /// JSON Deserialization of a given json string. /// @@ -93,7 +93,23 @@ internal static IPResponse ParseIPResponse(string response){ responseModel.CountryCurrency = CountryHelper.GetCountryCurrency(responseModel.Country); responseModel.Continent = CountryHelper.GetContinent(responseModel.Country); responseModel.CountryFlagURL = CountryFlagURL + responseModel.Country + ".svg"; - + + return responseModel; + } + + /// + /// IPResponseLite object with extra manual parsing. + /// + /// The json string to be parsed. + /// The deserialized IPResponseLite object with extra parsing for country being done. + internal static IPResponseLite ParseIPResponseLite(string response) { + IPResponseLite responseModel = JsonHelper.Deserialize(response); + responseModel.CountryName = CountryHelper.GetCountry(responseModel.CountryCode); + responseModel.IsEU = CountryHelper.IsEU(responseModel.CountryCode); + responseModel.CountryFlag = CountryHelper.GetCountryFlag(responseModel.CountryCode); + responseModel.CountryCurrency = CountryHelper.GetCountryCurrency(responseModel.CountryCode); + responseModel.Continent = CountryHelper.GetContinent(responseModel.CountryCode); + responseModel.CountryFlagURL = CountryFlagURL + responseModel.CountryCode + ".svg"; return responseModel; } }