Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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 ###
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
<artifactId>jackson-databind</artifactId>
<version>2.18.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/maxmind/geoip2/DatabaseProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,6 +60,27 @@ AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException,
Optional<AnonymousIpResponse> tryAnonymousIp(InetAddress ipAddress) throws IOException,
GeoIp2Exception;

/**
* Look up an IP address in a GeoIP2 Anonymous Plus.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some spots we say GeoIP Anonymous Plus and in other spots we say 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<AnonymousPlusResponse> tryAnonymousPlus(InetAddress ipAddress) throws IOException,
GeoIp2Exception;

/**
* Look up an IP address in a GeoIP2 IP Risk database.
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/maxmind/geoip2/DatabaseReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,6 +82,7 @@ public class DatabaseReader implements DatabaseProvider, Closeable {

private enum DatabaseType {
ANONYMOUS_IP,
ANONYMOUS_PLUS,
ASN,
CITY,
CONNECTION_TYPE,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -427,6 +432,54 @@ private Optional<AnonymousIpResponse> 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<AnonymousPlusResponse> r = getAnonymousPlus(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}

@Override
public Optional<AnonymousPlusResponse> tryAnonymousPlus(InetAddress ipAddress)
throws IOException,
GeoIp2Exception {
return getAnonymousPlus(ipAddress);
}

private Optional<AnonymousPlusResponse> getAnonymousPlus(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult<AnonymousPlusResponse> 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.
*
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/maxmind/geoip2/model/AbstractResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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();
Expand Down
145 changes: 145 additions & 0 deletions src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
25 changes: 25 additions & 0 deletions src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/maxmind-db
Submodule maxmind-db updated 57 files
+7 −6 .github/workflows/codeql-analysis.yml
+6 −2 .github/workflows/go.yml
+7 −3 .github/workflows/golangci-lint.yml
+32 −0 .github/workflows/zizmor.yml
+0 −708 .golangci.toml
+737 −0 .golangci.yml
+0 −11 .perltidyallrc
+0 −5 .tidyallrc
+0 −4 LICENSE
+202 −0 LICENSE-APACHE
+17 −0 LICENSE-MIT
+1 −2 MaxMind-DB-spec.md
+8 −1 README.md
+1 −1 go.mod
+0 −18 perltidyrc
+7 −5 pkg/writer/geoip2.go
+2 −1 pkg/writer/maxmind.go
+4 −1 pkg/writer/writer.go
+175 −0 source-data/GeoIP-Anonymous-Plus-Test.json
+6 −0 source-data/GeoIP2-Anonymous-IP-Test.json
+73 −7 source-data/GeoIP2-City-Test.json
+3 −28 source-data/GeoIP2-Country-Test.json
+27 −8 source-data/GeoIP2-Enterprise-Test.json
+31 −0 source-data/GeoIP2-IP-Risk-Test.json
+585 −11 source-data/GeoIP2-Precision-Enterprise-Test.json
+0 −3 source-data/GeoLite2-City-Test.json
+0 −3 source-data/GeoLite2-Country-Test.json
+ test-data/GeoIP-Anonymous-Plus-Test.mmdb
+ test-data/GeoIP2-Anonymous-IP-Test.mmdb
+ test-data/GeoIP2-City-Test.mmdb
+ test-data/GeoIP2-Connection-Type-Test.mmdb
+ test-data/GeoIP2-Country-Test.mmdb
+ test-data/GeoIP2-DensityIncome-Test.mmdb
+ test-data/GeoIP2-Domain-Test.mmdb
+ test-data/GeoIP2-Enterprise-Test.mmdb
+ test-data/GeoIP2-IP-Risk-Test.mmdb
+ test-data/GeoIP2-ISP-Test.mmdb
+ test-data/GeoIP2-Precision-Enterprise-Test.mmdb
+ test-data/GeoIP2-Static-IP-Score-Test.mmdb
+ test-data/GeoIP2-User-Count-Test.mmdb
+ test-data/GeoLite2-ASN-Test.mmdb
+ test-data/GeoLite2-City-Test.mmdb
+ test-data/GeoLite2-Country-Test.mmdb
+ test-data/MaxMind-DB-no-ipv4-search-tree.mmdb
+ test-data/MaxMind-DB-string-value-entries.mmdb
+ test-data/MaxMind-DB-test-decoder.mmdb
+ test-data/MaxMind-DB-test-ipv4-24.mmdb
+ test-data/MaxMind-DB-test-ipv4-28.mmdb
+ test-data/MaxMind-DB-test-ipv4-32.mmdb
+ test-data/MaxMind-DB-test-ipv6-24.mmdb
+ test-data/MaxMind-DB-test-ipv6-28.mmdb
+ test-data/MaxMind-DB-test-ipv6-32.mmdb
+ test-data/MaxMind-DB-test-metadata-pointers.mmdb
+ test-data/MaxMind-DB-test-mixed-24.mmdb
+ test-data/MaxMind-DB-test-mixed-28.mmdb
+ test-data/MaxMind-DB-test-mixed-32.mmdb
+ test-data/MaxMind-DB-test-nested.mmdb
Loading