diff --git a/src/main/java/io/ipinfo/api/IPinfoCore.java b/src/main/java/io/ipinfo/api/IPinfoCore.java new file mode 100644 index 0000000..19b434b --- /dev/null +++ b/src/main/java/io/ipinfo/api/IPinfoCore.java @@ -0,0 +1,91 @@ +package io.ipinfo.api; + +import io.ipinfo.api.cache.Cache; +import io.ipinfo.api.cache.SimpleCache; +import io.ipinfo.api.context.Context; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.IPResponseCore; +import io.ipinfo.api.request.IPRequestCore; +import java.time.Duration; +import okhttp3.OkHttpClient; + +public class IPinfoCore { + + private final OkHttpClient client; + private final Context context; + private final String token; + private final Cache cache; + + IPinfoCore( + OkHttpClient client, + Context context, + String token, + Cache cache + ) { + this.client = client; + this.context = context; + this.token = token; + this.cache = cache; + } + + public static void main(String[] args) throws RateLimitedException { + System.out.println("Running IPinfo Core client"); + } + + /** + * Lookup IP information using the IP. This is a blocking call. + * + * @param ip IP address to query information for. + * @return Response containing IP information. + * @throws RateLimitedException if the user has exceeded the rate limit. + */ + public IPResponseCore lookupIP(String ip) throws RateLimitedException { + IPRequestCore request = new IPRequestCore(client, token, ip); + IPResponseCore response = request.handle(); + + if (response != null) { + response.setContext(context); + if (!response.getBogon()) { + cache.set(cacheKey(ip), response); + } + } + + return response; + } + + public static String cacheKey(String k) { + return "core_" + k; + } + + public static class Builder { + + private OkHttpClient client; + private String token; + private Cache cache; + + public Builder setClient(OkHttpClient client) { + this.client = client; + return this; + } + + public Builder setToken(String token) { + this.token = token; + return this; + } + + public Builder setCache(Cache cache) { + this.cache = cache; + return this; + } + + public IPinfoCore build() { + if (client == null) { + client = new OkHttpClient(); + } + if (cache == null) { + cache = new SimpleCache(Duration.ofDays(1)); + } + return new IPinfoCore(client, new Context(), token, cache); + } + } +} diff --git a/src/main/java/io/ipinfo/api/model/Geo.java b/src/main/java/io/ipinfo/api/model/Geo.java new file mode 100644 index 0000000..9f44db3 --- /dev/null +++ b/src/main/java/io/ipinfo/api/model/Geo.java @@ -0,0 +1,102 @@ +package io.ipinfo.api.model; + +public class Geo { + private final String city; + private final String region; + private final String region_code; + private final String country; + private final String country_code; + private final String continent; + private final String continent_code; + private final Double latitude; + private final Double longitude; + private final String timezone; + private final String postal_code; + + public Geo( + String city, + String region, + String region_code, + String country, + String country_code, + String continent, + String continent_code, + Double latitude, + Double longitude, + String timezone, + String postal_code + ) { + this.city = city; + this.region = region; + this.region_code = region_code; + this.country = country; + this.country_code = country_code; + this.continent = continent; + this.continent_code = continent_code; + this.latitude = latitude; + this.longitude = longitude; + this.timezone = timezone; + this.postal_code = postal_code; + } + + public String getCity() { + return city; + } + + public String getRegion() { + return region; + } + + public String getRegionCode() { + return region_code; + } + + public String getCountry() { + return country; + } + + public String getCountryCode() { + return country_code; + } + + public String getContinent() { + return continent; + } + + public String getContinentCode() { + return continent_code; + } + + public Double getLatitude() { + return latitude; + } + + public Double getLongitude() { + return longitude; + } + + public String getTimezone() { + return timezone; + } + + public String getPostalCode() { + return postal_code; + } + + @Override + public String toString() { + return "Geo{" + + "city='" + city + '\'' + + ", region='" + region + '\'' + + ", region_code='" + region_code + '\'' + + ", country='" + country + '\'' + + ", country_code='" + country_code + '\'' + + ", continent='" + continent + '\'' + + ", continent_code='" + continent_code + '\'' + + ", latitude=" + latitude + + ", longitude=" + longitude + + ", timezone='" + timezone + '\'' + + ", postal_code='" + postal_code + '\'' + + '}'; + } +} diff --git a/src/main/java/io/ipinfo/api/model/IPResponseCore.java b/src/main/java/io/ipinfo/api/model/IPResponseCore.java new file mode 100644 index 0000000..92f1589 --- /dev/null +++ b/src/main/java/io/ipinfo/api/model/IPResponseCore.java @@ -0,0 +1,140 @@ +package io.ipinfo.api.model; + +import com.google.gson.annotations.SerializedName; +import io.ipinfo.api.context.Context; + +public class IPResponseCore { + private final String ip; + private final Geo geo; + @SerializedName("as") + private final ASN asn; + private final Boolean is_anonymous; + private final Boolean is_anycast; + private final Boolean is_hosting; + private final Boolean is_mobile; + private final Boolean is_satellite; + private final boolean bogon; + private transient Context context; + + public IPResponseCore( + String ip, + Geo geo, + ASN asn, + Boolean is_anonymous, + Boolean is_anycast, + Boolean is_hosting, + Boolean is_mobile, + Boolean is_satellite + ) { + this.ip = ip; + this.geo = geo; + this.asn = asn; + this.is_anonymous = is_anonymous; + this.is_anycast = is_anycast; + this.is_hosting = is_hosting; + this.is_mobile = is_mobile; + this.is_satellite = is_satellite; + this.bogon = false; + } + + public IPResponseCore(String ip, boolean bogon) { + this.ip = ip; + this.bogon = bogon; + this.geo = null; + this.asn = null; + this.is_anonymous = null; + this.is_anycast = null; + this.is_hosting = null; + this.is_mobile = null; + this.is_satellite = null; + } + + /** + * Set by the library for extra utility functions + * + * @param context for country information + */ + public void setContext(Context context) { + this.context = context; + } + + public String getIp() { + return ip; + } + + public Geo getGeo() { + return geo; + } + + public ASN getAsn() { + return asn; + } + + public Boolean getIsAnonymous() { + return is_anonymous; + } + + public Boolean getIsAnycast() { + return is_anycast; + } + + public Boolean getIsHosting() { + return is_hosting; + } + + public Boolean getIsMobile() { + return is_mobile; + } + + public Boolean getIsSatellite() { + return is_satellite; + } + + public boolean getBogon() { + return bogon; + } + + public String getCountryName() { + return context != null && geo != null ? context.getCountryName(geo.getCountryCode()) : (geo != null ? geo.getCountry() : null); + } + + public Boolean isEU() { + return context != null && geo != null ? context.isEU(geo.getCountryCode()) : null; + } + + public CountryFlag getCountryFlag() { + return context != null && geo != null ? context.getCountryFlag(geo.getCountryCode()) : null; + } + + public String getCountryFlagURL() { + return context != null && geo != null ? context.getCountryFlagURL(geo.getCountryCode()) : null; + } + + public CountryCurrency getCountryCurrency() { + return context != null && geo != null ? context.getCountryCurrency(geo.getCountryCode()) : null; + } + + public Continent getContinentInfo() { + return context != null && geo != null ? context.getContinent(geo.getCountryCode()) : null; + } + + @Override + public String toString() { + if (bogon) { + return "IPResponseCore{" + + "ip='" + ip + '\'' + + ", bogon=" + bogon + + '}'; + } + return "IPResponseCore{" + + "ip='" + ip + '\'' + + ", geo=" + geo + + ", asn=" + asn + + ", is_anonymous=" + is_anonymous + + ", is_anycast=" + is_anycast + + ", is_hosting=" + is_hosting + + ", is_mobile=" + is_mobile + + ", is_satellite=" + is_satellite + + '}'; + } +} diff --git a/src/main/java/io/ipinfo/api/request/IPRequestCore.java b/src/main/java/io/ipinfo/api/request/IPRequestCore.java new file mode 100644 index 0000000..f63022a --- /dev/null +++ b/src/main/java/io/ipinfo/api/request/IPRequestCore.java @@ -0,0 +1,44 @@ +package io.ipinfo.api.request; + +import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.IPResponseCore; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class IPRequestCore extends BaseRequest { + private final static String URL_FORMAT = "https://api.ipinfo.io/lookup/%s"; + private final String ip; + + public IPRequestCore(OkHttpClient client, String token, String ip) { + super(client, token); + this.ip = ip; + } + + @Override + public IPResponseCore handle() throws RateLimitedException { + if (IPRequest.isBogon(ip)) { + try { + return new IPResponseCore(ip, true); + } catch (Exception ex) { + throw new ErrorResponseException(ex); + } + } + + String url = String.format(URL_FORMAT, ip); + Request.Builder request = new Request.Builder().url(url).get(); + + try (Response response = handleRequest(request)) { + if (response == null || response.body() == null) { + return null; + } + + try { + return gson.fromJson(response.body().string(), IPResponseCore.class); + } catch (Exception ex) { + throw new ErrorResponseException(ex); + } + } + } +} diff --git a/src/test/java/io/ipinfo/IPinfoCoreTest.java b/src/test/java/io/ipinfo/IPinfoCoreTest.java new file mode 100644 index 0000000..7db15b7 --- /dev/null +++ b/src/test/java/io/ipinfo/IPinfoCoreTest.java @@ -0,0 +1,124 @@ +package io.ipinfo; + +import static org.junit.jupiter.api.Assertions.*; + +import io.ipinfo.api.IPinfoCore; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.IPResponseCore; +import org.junit.jupiter.api.Test; + +public class IPinfoCoreTest { + + @Test + public void testAccessToken() { + String token = "test_token"; + IPinfoCore client = new IPinfoCore.Builder() + .setToken(token) + .build(); + assertNotNull(client); + } + + @Test + public void testGoogleDNS() { + IPinfoCore client = new IPinfoCore.Builder() + .setToken(System.getenv("IPINFO_TOKEN")) + .build(); + + try { + IPResponseCore response = client.lookupIP("8.8.8.8"); + assertAll( + "8.8.8.8", + () -> assertEquals("8.8.8.8", response.getIp(), "IP mismatch"), + () -> assertNotNull(response.getGeo(), "geo should be set"), + () -> assertEquals("Mountain View", response.getGeo().getCity(), "city mismatch"), + () -> assertEquals("California", response.getGeo().getRegion(), "region mismatch"), + () -> assertEquals("CA", response.getGeo().getRegionCode(), "region code mismatch"), + () -> assertEquals("United States", response.getGeo().getCountry(), "country mismatch"), + () -> assertEquals("US", response.getGeo().getCountryCode(), "country code mismatch"), + () -> assertEquals("North America", response.getGeo().getContinent(), "continent mismatch"), + () -> assertEquals("NA", response.getGeo().getContinentCode(), "continent code mismatch"), + () -> assertNotNull(response.getGeo().getLatitude(), "latitude should be set"), + () -> assertNotNull(response.getGeo().getLongitude(), "longitude should be set"), + () -> assertEquals("America/Los_Angeles", response.getGeo().getTimezone(), "timezone mismatch"), + () -> assertEquals("94043", response.getGeo().getPostalCode(), "postal code mismatch"), + // Enriched fields + () -> assertEquals("United States", response.getCountryName(), "country name mismatch"), + () -> assertFalse(response.isEU(), "isEU mismatch"), + () -> assertEquals("🇺🇸", response.getCountryFlag().getEmoji(), "emoji mismatch"), + () -> assertEquals("U+1F1FA U+1F1F8", response.getCountryFlag().getUnicode(), "unicode mismatch"), + () -> assertEquals("https://cdn.ipinfo.io/static/images/countries-flags/US.svg", response.getCountryFlagURL(), "flag URL mismatch"), + () -> assertEquals("USD", response.getCountryCurrency().getCode(), "currency code mismatch"), + () -> assertEquals("$", response.getCountryCurrency().getSymbol(), "currency symbol mismatch"), + () -> assertEquals("NA", response.getContinentInfo().getCode(), "continent info code mismatch"), + () -> assertEquals("North America", response.getContinentInfo().getName(), "continent info name mismatch"), + // AS fields + () -> assertNotNull(response.getAsn(), "asn should be set"), + () -> assertEquals("AS15169", response.getAsn().getAsn(), "ASN mismatch"), + () -> assertEquals("Google LLC", response.getAsn().getName(), "AS name mismatch"), + () -> assertEquals("google.com", response.getAsn().getDomain(), "AS domain mismatch"), + () -> assertEquals("hosting", response.getAsn().getType(), "AS type mismatch"), + // Network flags + () -> assertFalse(response.getIsAnonymous(), "is_anonymous mismatch"), + () -> assertTrue(response.getIsAnycast(), "is_anycast mismatch"), + () -> assertTrue(response.getIsHosting(), "is_hosting mismatch"), + () -> assertFalse(response.getIsMobile(), "is_mobile mismatch"), + () -> assertFalse(response.getIsSatellite(), "is_satellite mismatch"), + () -> assertFalse(response.getBogon(), "bogon mismatch") + ); + } catch (RateLimitedException e) { + fail(e); + } + } + + @Test + public void testBogon() { + IPinfoCore client = new IPinfoCore.Builder() + .setToken(System.getenv("IPINFO_TOKEN")) + .build(); + + try { + IPResponseCore response = client.lookupIP("127.0.0.1"); + assertAll( + "127.0.0.1", + () -> assertEquals("127.0.0.1", response.getIp(), "IP mismatch"), + () -> assertTrue(response.getBogon(), "bogon mismatch") + ); + } catch (RateLimitedException e) { + fail(e); + } + } + + @Test + public void testCloudFlareDNS() { + IPinfoCore client = new IPinfoCore.Builder() + .setToken(System.getenv("IPINFO_TOKEN")) + .build(); + + try { + IPResponseCore response = client.lookupIP("1.1.1.1"); + assertAll( + "1.1.1.1", + () -> assertEquals("1.1.1.1", response.getIp(), "IP mismatch"), + () -> assertEquals("AS13335", response.getAsn().getAsn(), "ASN mismatch"), + () -> assertEquals("Cloudflare, Inc.", response.getAsn().getName(), "AS name mismatch"), + () -> assertEquals("cloudflare.com", response.getAsn().getDomain(), "AS domain mismatch"), + () -> assertNotNull(response.getGeo().getCountryCode(), "country code should be set"), + () -> assertNotNull(response.getGeo().getCountry(), "country should be set"), + () -> assertNotNull(response.getCountryName(), "country name should be set"), + () -> assertNotNull(response.getGeo().getContinentCode(), "continent code should be set"), + () -> assertNotNull(response.getGeo().getContinent(), "continent should be set"), + () -> assertNotNull(response.isEU(), "isEU should be set"), + () -> assertNotNull(response.getCountryFlag().getEmoji(), "emoji should be set"), + () -> assertNotNull(response.getCountryFlag().getUnicode(), "unicode should be set"), + () -> assertNotNull(response.getCountryFlagURL(), "flag URL should be set"), + () -> assertNotNull(response.getCountryCurrency().getCode(), "currency code should be set"), + () -> assertNotNull(response.getCountryCurrency().getSymbol(), "currency symbol should be set"), + () -> assertNotNull(response.getContinentInfo().getCode(), "continent info code should be set"), + () -> assertNotNull(response.getContinentInfo().getName(), "continent info name should be set"), + () -> assertFalse(response.getBogon(), "bogon mismatch") + ); + } catch (RateLimitedException e) { + fail(e); + } + } +}