diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd8e7ee..b00f10c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ CHANGELOG 4.3.0 ------------------ +* Support for the GeoIP Anonymous Plus database has been added. To do a + lookup in this database, use the `anonymousPlus` method on + `DatabaseReader`. * `getMetroCode` in the `Location` model has been deprecated. The code values are no longer being maintained. diff --git a/README.md b/README.md index 9529be16..2ed94358 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ To use the web service API, you must create a new `WebServiceClient` using the `WebServiceClient.Builder`. You must provide the `Builder` constructor your MaxMind `accountId` and `licenseKey`. To use the GeoLite2 web services instead of GeoIP2, set the `host` method on the builder to `geolite.info`. To use -the Sandbox GeoIP2 web services intead of the production GeoIP2 web +the Sandbox GeoIP2 web services instead of the production GeoIP2 web services, set the `host` method on the builder to `sandbox.maxmind.com`. You may also set a `timeout` or set the `locales` fallback order using the methods on the `Builder`. After you have created the `WebServiceClient`, @@ -304,7 +304,31 @@ try (DatabaseReader reader = new DatabaseReader.Builder(database).build()) { System.out.println(response.isResidentialProxy()); // false System.out.println(response.isTorExitNode()); //true } +``` + +### Anonymous Plus ### + +```java +// A File object pointing to your GeoIP2 Anonymous Plus database +File database = new File("/path/to/GeoIP-Anonymous-Plus.mmdb"); + +// This creates the DatabaseReader object. To improve performance, reuse +// the object across lookups. The object is thread-safe. +try (DatabaseReader reader = new DatabaseReader.Builder(database).build()) { + InetAddress ipAddress = InetAddress.getByName("85.25.43.84"); + + AnonymousIpResponse response = reader.anonymousPlus(ipAddress); + System.out.println(response.getAnonymizerConfidence()); // 30 + System.out.println(response.isAnonymous()); // true + System.out.println(response.isAnonymousVpn()); // false + System.out.println(response.isHostingProvider()); // false + System.out.println(response.isPublicProxy()); // false + System.out.println(response.isResidentialProxy()); // false + System.out.println(response.isTorExitNode()); // true + System.out.println(response.getNetworkLastSeen()); // "2025-04-14" + System.out.println(response.getProviderName()); // "FooBar VPN" +} ``` ### ASN ### diff --git a/pom.xml b/pom.xml index 16076e0e..02fd1701 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,11 @@ jackson-databind 2.18.3 + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.18.3 + com.fasterxml.jackson.core jackson-core diff --git a/src/main/java/com/maxmind/geoip2/DatabaseProvider.java b/src/main/java/com/maxmind/geoip2/DatabaseProvider.java index 997229c7..c7c28d57 100644 --- a/src/main/java/com/maxmind/geoip2/DatabaseProvider.java +++ b/src/main/java/com/maxmind/geoip2/DatabaseProvider.java @@ -2,6 +2,7 @@ import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.AnonymousIpResponse; +import com.maxmind.geoip2.model.AnonymousPlusResponse; import com.maxmind.geoip2.model.AsnResponse; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.ConnectionTypeResponse; @@ -59,6 +60,27 @@ AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException, Optional tryAnonymousIp(InetAddress ipAddress) throws IOException, GeoIp2Exception; + /** + * Look up an IP address in a GeoIP2 Anonymous Plus. + * + * @param ipAddress IPv4 or IPv6 address to lookup. + * @return a AnonymousPlusResponse for the requested IP address. + * @throws com.maxmind.geoip2.exception.GeoIp2Exception if there is an error looking up the IP + * @throws java.io.IOException if there is an IO error + */ + AnonymousPlusResponse anonymousPlus(InetAddress ipAddress) throws IOException, + GeoIp2Exception; + + /** + * Look up an IP address in a GeoIP2 Anonymous Plus. + * + * @param ipAddress IPv4 or IPv6 address to lookup. + * @return a AnonymousPlusResponse for the requested IP address or empty if it is not in the DB. + * @throws com.maxmind.geoip2.exception.GeoIp2Exception if there is an error looking up the IP + * @throws java.io.IOException if there is an IO error + */ + Optional tryAnonymousPlus(InetAddress ipAddress) throws IOException, + GeoIp2Exception; /** * Look up an IP address in a GeoIP2 IP Risk database. diff --git a/src/main/java/com/maxmind/geoip2/DatabaseReader.java b/src/main/java/com/maxmind/geoip2/DatabaseReader.java index e63549e4..0109e45a 100644 --- a/src/main/java/com/maxmind/geoip2/DatabaseReader.java +++ b/src/main/java/com/maxmind/geoip2/DatabaseReader.java @@ -10,6 +10,7 @@ import com.maxmind.geoip2.exception.AddressNotFoundException; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.AnonymousIpResponse; +import com.maxmind.geoip2.model.AnonymousPlusResponse; import com.maxmind.geoip2.model.AsnResponse; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.ConnectionTypeResponse; @@ -81,6 +82,7 @@ public class DatabaseReader implements DatabaseProvider, Closeable { private enum DatabaseType { ANONYMOUS_IP, + ANONYMOUS_PLUS, ASN, CITY, CONNECTION_TYPE, @@ -119,6 +121,9 @@ private int getDatabaseType() { if (databaseType.contains("GeoIP2-Anonymous-IP")) { type |= DatabaseType.ANONYMOUS_IP.type; } + if (databaseType.contains("GeoIP-Anonymous-Plus")) { + type |= DatabaseType.ANONYMOUS_PLUS.type; + } if (databaseType.contains("GeoIP2-IP-Risk")) { type |= DatabaseType.IP_RISK.type; } @@ -427,6 +432,54 @@ private Optional getAnonymousIp( ); } + /** + * Look up an IP address in a GeoIP2 Anonymous Plus. + * + * @param ipAddress IPv4 or IPv6 address to lookup. + * @return a AnonymousPlusResponse for the requested IP address. + * @throws GeoIp2Exception if there is an error looking up the IP + * @throws IOException if there is an IO error + */ + @Override + public AnonymousPlusResponse anonymousPlus(InetAddress ipAddress) throws IOException, + GeoIp2Exception { + Optional r = getAnonymousPlus(ipAddress); + if (r.isEmpty()) { + throw new AddressNotFoundException("The address " + + ipAddress.getHostAddress() + " is not in the database."); + } + return r.get(); + } + + @Override + public Optional tryAnonymousPlus(InetAddress ipAddress) + throws IOException, + GeoIp2Exception { + return getAnonymousPlus(ipAddress); + } + + private Optional getAnonymousPlus( + InetAddress ipAddress + ) throws IOException, GeoIp2Exception { + LookupResult result = this.get( + ipAddress, + AnonymousPlusResponse.class, + DatabaseType.ANONYMOUS_PLUS + ); + AnonymousPlusResponse response = result.getModel(); + if (response == null) { + return Optional.empty(); + } + return Optional.of( + new AnonymousPlusResponse( + response, + result.getIpAddress(), + result.getNetwork() + ) + ); + } + + /** * Look up an IP address in a GeoIP2 IP Risk database. * diff --git a/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java b/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java index 6c8f1178..6c742f1b 100644 --- a/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java +++ b/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; /** @@ -18,6 +19,7 @@ public abstract class AbstractResponse { public String toJson() throws IOException { JsonMapper mapper = JsonMapper.builder() .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) + .addModule(new JavaTimeModule()) .serializationInclusion(Include.NON_NULL) .serializationInclusion(Include.NON_EMPTY) .build(); diff --git a/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java b/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java new file mode 100644 index 00000000..7397c36a --- /dev/null +++ b/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java @@ -0,0 +1,145 @@ +package com.maxmind.geoip2.model; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.maxmind.db.MaxMindDbConstructor; +import com.maxmind.db.MaxMindDbParameter; +import com.maxmind.db.Network; +import com.maxmind.geoip2.NetworkDeserializer; +import java.time.LocalDate; + +/** + * This class provides the GeoIP Anonymous Plus model. + */ +public class AnonymousPlusResponse extends AnonymousIpResponse { + private final Integer anonymizerConfidence; + private final LocalDate networkLastSeen; + private final String providerName; + + /** + * Constructs an instance of {@code AnonymousPlusResponse} with the specified values. + * + * @param anonymizerConfidence confidence that the network is a VPN. + * @param ipAddress the IP address being checked + * @param isAnonymous whether the IP address belongs to any sort of anonymous network + * @param isAnonymousVpn whether the IP address belongs to an anonymous VPN system + * @param isHostingProvider whether the IP address belongs to a hosting provider + * @param isPublicProxy whether the IP address belongs to a public proxy system + * @param isResidentialProxy whether the IP address belongs to a residential proxy system + * @param isTorExitNode whether the IP address is a Tor exit node + * @param network the network associated with the record + * @param networkLastSeen the last sighting of the network. + * @param providerName the name of the VPN provider. + */ + public AnonymousPlusResponse( + @JsonProperty("anonymizer_confidence") Integer anonymizerConfidence, + @JacksonInject("ip_address") @JsonProperty("ip_address") String ipAddress, + @JsonProperty("is_anonymous") Boolean isAnonymous, + @JsonProperty("is_anonymous_vpn") Boolean isAnonymousVpn, + @JsonProperty("is_hosting_provider") Boolean isHostingProvider, + @JsonProperty("is_public_proxy") Boolean isPublicProxy, + @JsonProperty("is_residential_proxy") Boolean isResidentialProxy, + @JsonProperty("is_tor_exit_node") Boolean isTorExitNode, + @JacksonInject("network") @JsonDeserialize(using = NetworkDeserializer.class) + @JsonProperty("network") Network network, + @JsonProperty("network_last_seen") LocalDate networkLastSeen, + @JsonProperty("provider_name") String providerName + ) { + super(ipAddress, isAnonymous, isAnonymousVpn, isHostingProvider, isPublicProxy, + isResidentialProxy, isTorExitNode, network); + + this.anonymizerConfidence = anonymizerConfidence; + this.networkLastSeen = networkLastSeen; + this.providerName = providerName; + } + + /** + * Constructs an instance of {@code AnonymousPlusResponse} with the specified values. + * + * @param anonymizerConfidence confidence that the network is a VPN. + * @param ipAddress the IP address being checked + * @param isAnonymous whether the IP address belongs to any sort of anonymous network + * @param isAnonymousVpn whether the IP address belongs to an anonymous VPN system + * @param isHostingProvider whether the IP address belongs to a hosting provider + * @param isPublicProxy whether the IP address belongs to a public proxy system + * @param isResidentialProxy whether the IP address belongs to a residential proxy system + * @param isTorExitNode whether the IP address is a Tor exit node + * @param network the network associated with the record + * @param networkLastSeen the last sighting of the network. + * @param providerName the name of the VPN provider. + */ + @MaxMindDbConstructor + public AnonymousPlusResponse( + @MaxMindDbParameter(name = "anonymizer_confidence") Integer anonymizerConfidence, + @MaxMindDbParameter(name = "ip_address") String ipAddress, + @MaxMindDbParameter(name = "is_anonymous") Boolean isAnonymous, + @MaxMindDbParameter(name = "is_anonymous_vpn") Boolean isAnonymousVpn, + @MaxMindDbParameter(name = "is_hosting_provider") Boolean isHostingProvider, + @MaxMindDbParameter(name = "is_public_proxy") Boolean isPublicProxy, + @MaxMindDbParameter(name = "is_residential_proxy") Boolean isResidentialProxy, + @MaxMindDbParameter(name = "is_tor_exit_node") Boolean isTorExitNode, + @MaxMindDbParameter(name = "network") Network network, + @MaxMindDbParameter(name = "network_last_seen") String networkLastSeen, + @MaxMindDbParameter(name = "provider_name") String providerName + ) { + this(anonymizerConfidence, ipAddress, isAnonymous, isAnonymousVpn, + isHostingProvider, isPublicProxy, isResidentialProxy, isTorExitNode, network, + networkLastSeen != null ? LocalDate.parse(networkLastSeen) : null, + providerName); + } + + /** + * Constructs an instance of {@code AnonymousPlusResponse} from the values in the + * response and the specified IP address and network. + * + * @param response the response to copy + * @param ipAddress the IP address being checked + * @param network the network associated with the record + */ + public AnonymousPlusResponse( + AnonymousPlusResponse response, + String ipAddress, + Network network + ) { + this( + response.getAnonymizerConfidence(), + ipAddress, + response.isAnonymous(), + response.isAnonymousVpn(), + response.isHostingProvider(), + response.isPublicProxy(), + response.isResidentialProxy(), + response.isTorExitNode(), + network, + response.getNetworkLastSeen(), + response.getProviderName() + ); + } + + /** + * @return A score ranging from 1 to 99 that is our percent confidence that the network is + * currently part of an actively used VPN service. + */ + @JsonProperty + public Integer getAnonymizerConfidence() { + return anonymizerConfidence; + } + + /** + * @return The last day that the network was sighted in our analysis of anonymized networks. + */ + @JsonProperty + public LocalDate getNetworkLastSeen() { + return networkLastSeen; + } + + /** + * @return The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated with the + * network. + */ + @JsonProperty + public String getProviderName() { + return providerName; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 7020345e..fea2e6f6 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,6 +2,7 @@ module com.maxmind.geoip2 { requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; requires transitive com.maxmind.db; requires java.net.http; diff --git a/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java b/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java index b38e1826..e70b3e05 100644 --- a/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java +++ b/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java @@ -11,6 +11,7 @@ import com.maxmind.geoip2.exception.AddressNotFoundException; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.AnonymousIpResponse; +import com.maxmind.geoip2.model.AnonymousPlusResponse; import com.maxmind.geoip2.model.AsnResponse; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.ConnectionTypeResponse; @@ -227,6 +228,30 @@ public void testAnonymousIp() throws Exception { } } + @Test + public void testAnonymousPlus() throws Exception { + try (DatabaseReader reader = new DatabaseReader.Builder( + this.getFile("GeoIP-Anonymous-Plus-Test.mmdb")).build() + ) { + InetAddress ipAddress = InetAddress.getByName("1.2.0.1"); + AnonymousPlusResponse response = reader.anonymousPlus(ipAddress); + assertEquals(30, response.getAnonymizerConfidence()); + assertTrue(response.isAnonymous()); + assertTrue(response.isAnonymousVpn()); + assertFalse(response.isHostingProvider()); + assertFalse(response.isPublicProxy()); + assertFalse(response.isResidentialProxy()); + assertFalse(response.isTorExitNode()); + assertEquals(ipAddress.getHostAddress(), response.getIpAddress()); + assertEquals("1.2.0.1/32", response.getNetwork().toString()); + assertEquals("2025-04-14", response.getNetworkLastSeen().toString()); + assertEquals("foo", response.getProviderName()); + + AnonymousPlusResponse tryResponse = reader.tryAnonymousPlus(ipAddress).get(); + assertEquals(response.toJson(), tryResponse.toJson()); + } + } + @Test public void testAnonymousIpIsResidentialProxy() throws Exception { try (DatabaseReader reader = new DatabaseReader.Builder( diff --git a/src/test/resources/maxmind-db b/src/test/resources/maxmind-db index 1271107c..f387805e 160000 --- a/src/test/resources/maxmind-db +++ b/src/test/resources/maxmind-db @@ -1 +1 @@ -Subproject commit 1271107ccad72c320bc7dc8aefd767cba550101a +Subproject commit f387805e4af30036ec4245c9069ac1f2fdae4953