diff --git a/Makefile b/Makefile index 0e670e35a16310ae04b2dcdfa3decaec36cd0383..6d7e25c58a65e438c800195a1213dda4b3349e90 100644 --- a/Makefile +++ b/Makefile @@ -12,18 +12,16 @@ clean:; rm -rf ./maxmind mvn clean -generate-spec: validate-spec; +compile-test-resources: install-yarn; yarn run generate-json-schema install-yarn:; yarn install --frozen-lockfile -validate-spec: install-yarn; - -compile-java: generate-spec; +compile-java: compile-test-resources; mvn compile package -Declipse.maxmind.root=${PWD}/maxmind -compile-java-quick: generate-spec; +compile-java-quick: compile-test-resources; mvn compile package -Dmaven.test.skip=true -Declipse.maxmind.root=${PWD}/maxmind compile: clean compile-java; diff --git a/package.json b/package.json index 940b1f377989862cb5b536e22a739200b49a1aeb..fb08ab5b0e53bb95220230b3a4dc3f8f9937a8f5 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,13 @@ "name": "eclipsefdn-geoip-rest-api-support", "version": "1.0.0", "dependencies": { - "@redocly/openapi-cli": "^1.0.0-beta.54", - "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1", - "@stoplight/json-ref-resolver": "^3.1.2", - "decamelize": "^5.0.0", - "js-yaml": "^4.1.0", - "yargs": "^17.0.1" + "eclipsefdn-api-support": "1.0.0" }, "private": true, "scripts": { "start": "yarn run generate-json-schema && npx @redocly/openapi-cli preview-docs spec/openapi.yaml -p 8093", "test": "yarn run generate-json-schema && npx @redocly/openapi-cli lint spec/openapi.yaml", - "generate-json-schema": "yarn run clean && node src/main/js/openapi2schema.js -s spec/openapi.yaml -t src/test/resources", + "generate-json-schema": "yarn run clean && node node_modules/eclipsefdn-api-support/src/openapi2schema.js -s spec/openapi.yaml -t src/test/resources", "clean": "rm -rf src/test/resources/schemas/" } } diff --git a/pom.xml b/pom.xml index c7b74a6c1537628c4d2c0fc658928d786836a6b6..a7018efdbf35a720ae0a99c2f55943b1df4eadd2 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,8 @@ <maven.compiler.parameters>true</maven.compiler.parameters> <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id> <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id> - <quarkus.platform.version>2.6.3.Final</quarkus.platform.version> - <eclipse-api-version>0.6.6</eclipse-api-version> + <quarkus.platform.version>2.16.7.Final</quarkus.platform.version> + <eclipse-api-version>0.7.7</eclipse-api-version> <surefire-plugin.version>3.0.0-M5</surefire-plugin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> diff --git a/src/main/java/org/eclipsefoundation/geoip/client/config/EclipseMaxmindSubnetConfig.java b/src/main/java/org/eclipsefoundation/geoip/client/config/EclipseMaxmindSubnetConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6e14336c84718ed1e8d66bf4bb19a51c6adefed0 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/geoip/client/config/EclipseMaxmindSubnetConfig.java @@ -0,0 +1,32 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.geoip.client.config; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * Config mapper for eclipse maxmind file locations. Contains filepaths for + * ipv4, ipv6, and countries files. + */ +@ConfigMapping(prefix = "eclipse.maxmind.subnet") +public interface EclipseMaxmindSubnetConfig { + + @WithDefault(value = "/tmp/maxmind/db/GeoLite2-Country-Blocks-IPv4.csv") + String ipv4FilePath(); + + @WithDefault(value = "/tmp/maxmind/db/GeoLite2-Country-Blocks-IPv6.csv") + String ipv6FilePath(); + + @WithDefault(value = "/tmp/maxmind/db/GeoLite2-Country-Locations-en.csv") + String countriesFilePath(); +} diff --git a/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindDatabaseConfig.java b/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindDatabaseConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4e3a9ea4bca06ab2fefcea586e19b41f9e202dd1 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindDatabaseConfig.java @@ -0,0 +1,32 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.geoip.client.config; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithName; + +/** + * Config mapper for maxmind database configs. Contains the db root folder, as + * well as city and country file names. + */ +@ConfigMapping(prefix = "maxmind.database") +public interface MaxmindDatabaseConfig { + + @WithName("root") + String dbRoot(); + + @WithName("city.file") + String cityFileName(); + + @WithName("country.file") + String countryFileName(); +} diff --git a/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindReflectionRegistrationConfig.java b/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindReflectionRegistrationConfig.java index 3fc7575580a29e4172d4edce80efffe9d8c4eeff..3168f070ec997a031088420e20d0de3196ee7dd2 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindReflectionRegistrationConfig.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/config/MaxmindReflectionRegistrationConfig.java @@ -16,6 +16,6 @@ import com.maxmind.db.Metadata; import io.quarkus.runtime.annotations.RegisterForReflection; -@RegisterForReflection(targets = {Metadata.class, MaxMindDbConstructor.class}) +@RegisterForReflection(targets = { Metadata.class, MaxMindDbConstructor.class }) public class MaxmindReflectionRegistrationConfig { } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/helper/InetAddressHelper.java b/src/main/java/org/eclipsefoundation/geoip/client/helper/InetAddressHelper.java deleted file mode 100644 index 18dc35d590563e9d1d87826a95725550b72eb1ad..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipsefoundation/geoip/client/helper/InetAddressHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -/********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Martin Lowe <martin.lowe@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.geoip.client.helper; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.google.common.net.InetAddresses; - -/** - * Helper class centralizing checks of IP addresses. - * - * @author Martin Lowe - */ -public class InetAddressHelper { - - public static final List<String> INVALID_ADDRESSES_IPv4 = Collections - .unmodifiableList(Arrays.asList("127.0.0.1", "0.0.0.0")); - public static final List<String> INVALID_ADDRESSES_IPv6 = Collections.unmodifiableList(Arrays.asList("::1", "::")); - - /** - * Centralized IP address check that blacklists loopback and unspecified - * - * @param address the IP address to check - * @return true if address is a valid IP address, false otherwise - */ - public static boolean validInetAddress(String address) { - // return immediately if null - if (address == null) { - return false; - } - - // check lightweight options first for invalid addresses - if (INVALID_ADDRESSES_IPv4.contains(address) || INVALID_ADDRESSES_IPv6.contains(address)) { - return false; - } - // Use Google Guava as larger more intensive check of string values - return InetAddresses.isInetAddress(address); - } - - // hide constructor - private InetAddressHelper() { - } -} diff --git a/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java b/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java index 219fcf293916cb0f8d8639ca7b0c634bca98c75f..de4c405571450a41c9228e5108ba9c29137346fc 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java @@ -23,14 +23,12 @@ import io.quarkus.runtime.annotations.RegisterForReflection; */ @RegisterForReflection public class Country { + @CsvBindByName(column = "geoname_id") private String id; @CsvBindByName(column = "country_iso_code") private String countryIsoCode; - public Country() { - } - /** * @return the id */ @@ -58,5 +56,4 @@ public class Country { public void setCountryIsoCode(String countryIsoCode) { this.countryIsoCode = countryIsoCode; } - } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/model/SubnetRange.java b/src/main/java/org/eclipsefoundation/geoip/client/model/SubnetRange.java index c282dbd1131164fc2ed211d3f63b3eef4508094d..346dc570368bc99bcc07c34701fee1441028de93 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/model/SubnetRange.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/model/SubnetRange.java @@ -32,12 +32,6 @@ public class SubnetRange { @CsvBindByName private String network; - /** - * - */ - public SubnetRange() { - } - /** * @return the geoname */ @@ -79,5 +73,4 @@ public class SubnetRange { public void setNetwork(String network) { this.network = network; } - } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/resources/AbstractLocationResource.java b/src/main/java/org/eclipsefoundation/geoip/client/resources/AbstractLocationResource.java new file mode 100644 index 0000000000000000000000000000000000000000..a5990e791824389c6a34939c8d41e45a7df4149d --- /dev/null +++ b/src/main/java/org/eclipsefoundation/geoip/client/resources/AbstractLocationResource.java @@ -0,0 +1,84 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.geoip.client.resources; + +import java.io.IOException; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.core.exception.ApplicationException; +import org.eclipsefoundation.geoip.client.service.GeoIPService; + +import com.google.common.net.InetAddresses; +import com.maxmind.geoip2.record.AbstractNamedRecord; + +/** + * Superclass of {@link CountryResource} and {@link CityResource}. Contains a + * method for converting a City/Country to Json, as well as validation for + * incoming IP addresses. + */ +public abstract class AbstractLocationResource { + + private static final List<String> INVALID_ADDRESSES_IPv4 = List.of("127.0.0.1", "0.0.0.0"); + private static final List<String> INVALID_ADDRESSES_IPv6 = List.of("::1", "::"); + + @Inject + GeoIPService geoIp; + + /** + * Returns a 200 OK Response containing the JSON converted entity. Converts to + * JSON using the built-in {@link AbstractNamedRecord#toJson()} Throws a 500 + * Internal Server Error if there was an issue converting the body to JSON. + * + * @param record The entity to convert and send to the client + * @return A 200 OK Response containing the JSON converted entity. A 500 + * Internal Server Error if there was an issue converting the body to + * JSON. + */ + protected Response returnAsJson(AbstractNamedRecord result) { + // Return converted country/city + try { + return Response.ok(result.toJson()).build(); + } catch (IOException e) { + throw new ApplicationException("Error while converting country record to JSON", e); + } + } + + /** + * Validates the incoming IP address. IPs are invalid if they are null, + * contained in the list of invalid Ipv4/Ipv6 addresses, or considered invalid + * by {@link InetAddresses#isInetAddress(String)}. Throws a 400 + * BadRequestException if the Guava check throws an error validating the IP. + * + * @param address The given IP address to validate. + * @return True if the IP is valid. False if not. + */ + protected boolean isValidInetAddress(String address) { + try { + // Check null and lightweight options first for invalid addresses + if (StringUtils.isBlank(address) + || INVALID_ADDRESSES_IPv4.contains(address) + || INVALID_ADDRESSES_IPv6.contains(address)) { + return false; + } + + // Use Google Guava as larger more intensive check of string values + return InetAddresses.isInetAddress(address); + } catch (Exception e) { + throw new BadRequestException("Passed IP address was not a valid address"); + } + } +} diff --git a/src/main/java/org/eclipsefoundation/geoip/client/resources/CityResource.java b/src/main/java/org/eclipsefoundation/geoip/client/resources/CityResource.java index 209244a4a5b95c27a8360205c6f83b8788cf4750..cdcc214ef2593300330e79b48c4f5f3e3fef35d4 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/resources/CityResource.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/resources/CityResource.java @@ -1,63 +1,53 @@ /********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. +* Copyright (c) 2019, 2023 Eclipse Foundation. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> +* Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ package org.eclipsefoundation.geoip.client.resources; -import java.io.IOException; - -import javax.inject.Inject; +import javax.ws.rs.BadRequestException; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; - -import com.maxmind.geoip2.record.City; - -import org.eclipsefoundation.core.model.Error; -import org.eclipsefoundation.geoip.client.helper.InetAddressHelper; -import org.eclipsefoundation.geoip.client.service.GeoIPService; /** - * @author Martin Lowe - * + * Main resource class for all endpoints under '/cities'. */ @Path("/cities") @Produces(MediaType.APPLICATION_JSON) -public class CityResource { - - @Inject - GeoIPService geoIp; +public class CityResource extends AbstractLocationResource { + /** + * Returns a 200 OK Response containing the city location of a given IP address. + * Returns a 400 Bad Request if the IP is invalid. Returns a 500 Internal Server + * Error if there is an issue retrieving the IP location, or if the outgoing + * JSON conversion fails. + * + * @param ipAddr The IP address to search. + * @return A 200 OK Response containing the city location of a given IP address. + * A 400 Bad Request if the IP is invalid. A 500 Internal Server + * Error if there is an issue retrieving the IP location, or if the + * outgoing JSON conversion fails. + */ @GET @Path("/{ipAddr}") public Response get(@PathParam("ipAddr") String ipAddr) { // validate IP - if (ipAddr == null || !InetAddressHelper.validInetAddress(ipAddr)) { - return new Error(Status.BAD_REQUEST, "Valid IP address must be passed to retrieve location data") - .asResponse(); - } - // retrieve cached city data - City c = geoIp.getCity(ipAddr); - if (c == null) { - return new Error(Status.INTERNAL_SERVER_ERROR, "Error while retrieving location for IP " + ipAddr) - .asResponse(); - } - // return city data - try { - return Response.ok(c.toJson()).build(); - } catch (IOException e) { - throw new RuntimeException("Error while converting city record to JSON", e); + if (!isValidInetAddress(ipAddr)) { + throw new BadRequestException("Valid IP address must be passed to retrieve location data"); } + + // Retrieve cached city data and return the JSON object + return returnAsJson(geoIp.getCity(ipAddr)); } } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java b/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java index 3f89ecbd51f371737dc7e3ffc25a2fee22b82a74..f13315fff6f302520d2945def84494e5d6943b7e 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java @@ -1,63 +1,53 @@ /********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. +* Copyright (c) 2019, 2023 Eclipse Foundation. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> +* Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ package org.eclipsefoundation.geoip.client.resources; -import java.io.IOException; - -import javax.inject.Inject; +import javax.ws.rs.BadRequestException; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; - -import com.maxmind.geoip2.record.Country; - -import org.eclipsefoundation.core.model.Error; -import org.eclipsefoundation.geoip.client.helper.InetAddressHelper; -import org.eclipsefoundation.geoip.client.service.GeoIPService; /** - * @author Martin Lowe - * + * Main resource class for all endpoints under '/countries'. */ @Path("/countries") @Produces(MediaType.APPLICATION_JSON) -public class CountryResource { - - @Inject - GeoIPService geoIp; +public class CountryResource extends AbstractLocationResource { + /** + * Returns a 200 OK Response containing the country location of a given IP + * address. Returns a 400 Bad Request if the IP is invalid. Returns a 500 + * Internal Server Error if there is an issue retrieving the IP location, or if + * the outgoing JSON conversion fails. + * + * @param ipAddr The IP address to search. + * @return A 200 OK Response containing the country location of a given IP + * address. A 400 Bad Request if the IP is invalid. A 500 Internal + * Server Error if there is an issue retrieving the IP location, or if + * the outgoing JSON conversion fails. + */ @GET @Path("/{ipAddr}") public Response get(@PathParam("ipAddr") String ipAddr) { // validate IP - if (!InetAddressHelper.validInetAddress(ipAddr)) { - return new Error(Status.BAD_REQUEST, "Valid IP address must be passed to retrieve location data") - .asResponse(); - } - // retrieve cached country data - Country c = geoIp.getCountry(ipAddr); - if (c == null) { - return new Error(Status.INTERNAL_SERVER_ERROR, "Error while retrieving location for IP " + ipAddr) - .asResponse(); - } - // return country data - try { - return Response.ok(c.toJson()).build(); - } catch (IOException e) { - throw new RuntimeException("Error while converting country record to JSON", e); + if (!isValidInetAddress(ipAddr)) { + throw new BadRequestException("Valid IP address must be passed to retrieve location data"); } + + // Retrieve cached country data and return the JSON object + return returnAsJson(geoIp.getCountry(ipAddr)); } } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/resources/mapper/RuntimeMapper.java b/src/main/java/org/eclipsefoundation/geoip/client/resources/mapper/RuntimeMapper.java deleted file mode 100644 index df8497efb5781122ddce5645d5f1a2cfe5a2f26c..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipsefoundation/geoip/client/resources/mapper/RuntimeMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -/********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Martin Lowe <martin.lowe@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.geoip.client.resources.mapper; - -import java.net.UnknownHostException; -import java.sql.SQLException; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.maxmind.geoip2.exception.GeoIp2Exception; - -/** - * Captures exceptions and generates responses based on their causes. - * - * @author Martin Lowe - */ -@Provider -public class RuntimeMapper implements ExceptionMapper<RuntimeException> { - private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeMapper.class); - - @Override - public Response toResponse(RuntimeException exception) { - LOGGER.error(exception.getMessage(), exception); - - // respond to error - Throwable cause = exception.getCause(); - Response out; - if (cause instanceof UnknownHostException) { - out = Response.status(Status.BAD_REQUEST).entity("Passed IP address was not a valid address").build(); - } else if (cause instanceof GeoIp2Exception) { - out = Response.status(Status.INTERNAL_SERVER_ERROR) - .entity("An error occured while interacting with the geo IP databases: " + cause.getMessage()) - .build(); - } else if (cause instanceof SQLException) { - out = Response.status(Status.INTERNAL_SERVER_ERROR) - .entity("An error occured while interacting with the subnet dataset: " + cause.getMessage()) - .build(); - } else { - out = Response.status(Status.INTERNAL_SERVER_ERROR) - .entity("Unknown server error occured while processing request").build(); - } - return out; - } - -} \ No newline at end of file diff --git a/src/main/java/org/eclipsefoundation/geoip/client/service/GeoIPService.java b/src/main/java/org/eclipsefoundation/geoip/client/service/GeoIPService.java index a3bcae67f5c78d18972dd9784473ad17e80596fa..ef236eb46d1a4a47458a7562caa198c7e6545c5e 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/service/GeoIPService.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/service/GeoIPService.java @@ -1,11 +1,12 @@ /********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. +* Copyright (c) 2019, 2023 Eclipse Foundation. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> +* Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ @@ -15,12 +16,24 @@ import com.maxmind.geoip2.record.City; import com.maxmind.geoip2.record.Country; /** - * @author Martin Lowe - * + * Defines the GeoIPService used to fetch city and country location data via IP + * address. */ public interface GeoIPService { + /** + * Returns the city where the given IP address is located. + * + * @param ipAddr The given IP address + * @return A City entity corresponding to the IP address' location. + */ City getCity(String ipAddr); - + + /** + * Returns the country where the given IP address is located. + * + * @param ipAddr The given IP address + * @return A Country entity corresponding to the IP address' location. + */ Country getCountry(String ipAddr); } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java index 0ef3a15296a0cb498255938d36e1c7fccbc51a2d..d6fd722e3efc7cc8724b4efb3c0d96deb1fe941d 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java @@ -23,15 +23,17 @@ import java.util.Objects; import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; -import com.opencsv.bean.CsvToBeanBuilder; - -import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipsefoundation.core.exception.ApplicationException; +import org.eclipsefoundation.geoip.client.config.EclipseMaxmindSubnetConfig; import org.eclipsefoundation.geoip.client.model.Country; import org.eclipsefoundation.geoip.client.model.IPVersion; import org.eclipsefoundation.geoip.client.model.SubnetRange; import org.eclipsefoundation.geoip.client.service.NetworkService; +import com.opencsv.bean.CsvToBeanBuilder; + import io.quarkus.runtime.Startup; /** @@ -44,12 +46,8 @@ import io.quarkus.runtime.Startup; @ApplicationScoped public class CSVNetworkService implements NetworkService { - @ConfigProperty(name = "eclipse.subnet.ipv4.path") - String ipv4FilePath; - @ConfigProperty(name = "eclipse.subnet.ipv6.path") - String ipv6FilePath; - @ConfigProperty(name = "eclipse.subnet.countries.path") - String countriesFilePath; + @Inject + EclipseMaxmindSubnetConfig config; private Map<String, String> countryIdToIso; private Map<String, List<String>> ipv4Subnets; @@ -60,10 +58,9 @@ public class CSVNetworkService implements NetworkService { this.ipv4Subnets = new HashMap<>(); this.ipv6Subnets = new HashMap<>(); this.countryIdToIso = new HashMap<>(); - loadCountries(countriesFilePath, countryIdToIso); - loadMap(ipv4FilePath, ipv4Subnets); - // Disabled as it is a 2million item list and crashes the app - // loadMap(ipv6FilePath, ipv6Subnets); + loadCountries(config.countriesFilePath(), countryIdToIso); + loadMap(config.ipv4FilePath(), ipv4Subnets); + // Pre-loading ipv6 entries is a 2million item list and crashes the app } @Override @@ -79,6 +76,14 @@ public class CSVNetworkService implements NetworkService { return Collections.emptyList(); } + /** + * Reads all Country entities from a desired CSV file and loads them into the + * desired container. + * + * @param filePath The location of the CSV file to read. + * @param container Reference to the Map to store all entries read from the + * file. + */ private void loadCountries(String filePath, Map<String, String> container) { try (FileReader reader = new FileReader(filePath)) { // read in all of the country lines as country objects @@ -88,12 +93,20 @@ public class CSVNetworkService implements NetworkService { container.put(c.getId(), c.getCountryIsoCode().toLowerCase()); } } catch (FileNotFoundException e) { - throw new RuntimeException("Could not find country CSV file at path " + filePath, e); + throw new ApplicationException("Could not find country CSV file at path " + filePath, e); } catch (IOException e) { - throw new RuntimeException("Error while reading in CSV file at path " + filePath, e); + throw new ApplicationException("Error while reading in CSV file at path " + filePath, e); } } + /** + * Reads all Subnet entities from a desired CSV file and loads them into the + * desired container. + * + * @param filePath The location of the CSV file to read. + * @param container Reference to the Map to store all entries read from the + * file. + */ private void loadMap(String filePath, Map<String, List<String>> container) { try (FileReader reader = new FileReader(filePath)) { // read in all of the country lines as country objects @@ -110,10 +123,9 @@ public class CSVNetworkService implements NetworkService { } } } catch (FileNotFoundException e) { - throw new RuntimeException("Could not find country CSV file at path " + filePath, e); + throw new ApplicationException("Could not find country CSV file at path " + filePath, e); } catch (IOException e) { - throw new RuntimeException("Error while reading in CSV file at path " + filePath, e); + throw new ApplicationException("Error while reading in CSV file at path " + filePath, e); } } - } diff --git a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/MaxMindGeoIPService.java b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/MaxMindGeoIPService.java index 0742fc1de0d96592d730ea2dff5a3fc6f4340c40..efe329fd0caee0e1af5240a5eb7c2237ef5a96e8 100644 --- a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/MaxMindGeoIPService.java +++ b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/MaxMindGeoIPService.java @@ -1,11 +1,12 @@ /********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. +* Copyright (c) 2019, 2023 Eclipse Foundation. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> +* Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ @@ -18,21 +19,22 @@ import java.net.InetAddress; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipsefoundation.core.exception.ApplicationException; +import org.eclipsefoundation.geoip.client.config.MaxmindDatabaseConfig; import org.eclipsefoundation.geoip.client.service.GeoIPService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.quarkus.runtime.Startup; - import com.maxmind.geoip2.DatabaseReader; -import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.CountryResponse; import com.maxmind.geoip2.record.City; import com.maxmind.geoip2.record.Country; +import io.quarkus.runtime.Startup; + /** * Implementation of the Geo-IP service using MaxMind. Built in caching is * ignored as its a very light implementation that has no eviction policy and @@ -45,12 +47,8 @@ import com.maxmind.geoip2.record.Country; public class MaxMindGeoIPService implements GeoIPService { private static final Logger LOGGER = LoggerFactory.getLogger(MaxMindGeoIPService.class); - @ConfigProperty(name = "maxmind.database.root") - String dbRoot; - @ConfigProperty(name = "maxmind.database.city.file") - String cityFileName; - @ConfigProperty(name = "maxmind.database.country.file") - String countryFileName; + @Inject + MaxmindDatabaseConfig config; private DatabaseReader countryReader; private DatabaseReader cityReader; @@ -60,15 +58,15 @@ public class MaxMindGeoIPService implements GeoIPService { */ @PostConstruct public void init() { - File countryDb = new File(dbRoot + File.separator + countryFileName); - File cityDb = new File(dbRoot + File.separator + cityFileName); + File countryDb = new File(config.dbRoot() + File.separator + config.countryFileName()); + File cityDb = new File(config.dbRoot() + File.separator + config.cityFileName()); // attempt to load db's, throwing up if there is an issue initializing the // readers try { this.countryReader = new DatabaseReader.Builder(countryDb).build(); this.cityReader = new DatabaseReader.Builder(cityDb).build(); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException("Error pre-loading Maxmind DatabaseReaders", e); } } @@ -88,8 +86,8 @@ public class MaxMindGeoIPService implements GeoIPService { InetAddress addr = InetAddress.getByName(ipAddr); CityResponse resp = cityReader.city(addr); return resp.getCity(); - } catch (IOException | GeoIp2Exception e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new ApplicationException("Error interacting with GeoIP databases", e); } } @@ -99,9 +97,8 @@ public class MaxMindGeoIPService implements GeoIPService { InetAddress addr = InetAddress.getByName(ipAddr); CountryResponse resp = countryReader.country(addr); return resp.getCountry(); - } catch (IOException | GeoIp2Exception e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new ApplicationException("Error interacting with GeoIP databases", e); } } - } diff --git a/src/main/js/openapi2schema.js b/src/main/js/openapi2schema.js deleted file mode 100644 index 2680ae038916273e5f7f6ab942ac47bff04fc3e5..0000000000000000000000000000000000000000 --- a/src/main/js/openapi2schema.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2021 Eclipse - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * Author: Martin Lowe <martin.lowe@eclipsefoundation.org> - * - * SPDX-License-Identifier: EPL-2.0 - */ -const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema'); -const Resolver = require('@stoplight/json-ref-resolver'); -const yaml = require('js-yaml'); -const fs = require('fs'); -const decamelize = require('decamelize'); -const args = require('yargs') - .option('s', { - alias: 'src', - desc: 'The fully qualified path to the YAML spec.', - }) - .option('t', { - alias: 'target', - desc: 'The fully qualified path to write the JSON schema to', - }).argv; -if (!args.s || !args.t) { - process.exit(1); -} - -run(); - -/** - * Generates JSON schema files for consumption of the Java tests. - */ -async function run() { - try { - // load in the openapi yaml spec as an object - const doc = yaml.load(fs.readFileSync(args.s, 'utf8')); - // resolve $refs in openapi spec - let resolvedInp = await new Resolver.Resolver().resolve(doc); - const out = toJsonSchema(resolvedInp.result); - // if folder doesn't exist, create it - if (!fs.existsSync(`${args.t}/schemas`)) { - fs.mkdirSync(`${args.t}/schemas`); - } - // for each of the schemas, generate a JSON schema file - for (let schemaName in out.components.schemas) { - fs.writeFileSync(`${args.t}/schemas/${decamelize(schemaName, { separator: '-' })}-schema.json`, JSON.stringify(out.components.schemas[schemaName])); - } - } catch (e) { - console.log(e); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6b42e4872fbc2f6cb93890a300984709adab80ed..d18f35329ac87592cf09c599d8d737153a182657 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,22 +1,22 @@ -# Configuration file -quarkus.http.port=8080 +## Base application configs quarkus.http.root-path=/geoip +quarkus.http.port=8080 +%dev.quarkus.http.port=8090 -eclipse.pagination.enabled=false +quarkus.oidc.enabled=false +quarkus.keycloak.devservices.enabled=false +# Pagination configs +eclipse.pagination.filter.enabled=false + +## Eclipse and Maxmind configs eclipse.maxmind.root=/tmp/maxmind -eclipse.subnet.ipv4.path=${eclipse.maxmind.root}/db/GeoLite2-Country-Blocks-IPv4.csv -eclipse.subnet.ipv6.path=${eclipse.maxmind.root}/db/GeoLite2-Country-Blocks-IPv6.csv -eclipse.subnet.countries.path=${eclipse.maxmind.root}/db/GeoLite2-Country-Locations-en.csv +%dev.eclipse.maxmind.root=${PWD}/maxmind + +eclipse.maxmind.subnet.ipv4-file-path=${eclipse.maxmind.root}/db/GeoLite2-Country-Blocks-IPv4.csv +eclipse.maxmind.subnet.ipv6-file-path=${eclipse.maxmind.root}/db/GeoLite2-Country-Blocks-IPv6.csv +eclipse.maxmind.subnet.countries-file-path=${eclipse.maxmind.root}/db/GeoLite2-Country-Locations-en.csv maxmind.database.root=${eclipse.maxmind.root}/bin maxmind.database.country.file=GeoLite2-Country.mmdb maxmind.database.city.file=GeoLite2-City.mmdb - -%dev.quarkus.http.port=8090 - -%dev.eclipse.maxmind.root=${PWD}/maxmind - -#eclipse.cve.provider=stubbed -quarkus.oidc.enabled=false -quarkus.keycloak.devservices.enabled=false \ No newline at end of file diff --git a/src/test/java/org/eclipsefoundation/geoip/client/resources/CityResourceTest.java b/src/test/java/org/eclipsefoundation/geoip/client/resources/CityResourceTest.java index 3335fddb43f1145b3f6cbe9900d07fd075b0045f..b3fc977bb4998f7523b4ccbb45c66824d485ea3f 100644 --- a/src/test/java/org/eclipsefoundation/geoip/client/resources/CityResourceTest.java +++ b/src/test/java/org/eclipsefoundation/geoip/client/resources/CityResourceTest.java @@ -73,7 +73,7 @@ class CityResourceTest { } // seems to be an issue with Google Guava code, only gets detected by MaxMind - testCase = TestCaseHelper.prepareSuccessCase(CITIES_ENDPOINT_URL, new String[] { "0.1.1.1" }, "") + testCase = TestCaseHelper.prepareTestCase(CITIES_ENDPOINT_URL, new String[] { "0.1.1.1" }, "") .setStatusCode(500).build(); RestAssuredTemplates.testGet(testCase); diff --git a/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java b/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java index 8db047f9971f95b44edb96555ed275f98a04c1df..bf2522e4351b7221e87ef359ca981fc847ac4bc4 100644 --- a/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java +++ b/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java @@ -26,7 +26,7 @@ import io.quarkus.test.junit.QuarkusTest; * @author Martin Lowe */ @QuarkusTest -public class CountryResourceTest { +class CountryResourceTest { public static final String COUNTRY_ENDPOINT_URL = "/countries/{ipAddr}"; @@ -42,7 +42,7 @@ public class CountryResourceTest { new String[] { VALID_IPV6_ADDRESS }, SchemaNamespaceHelper.COUNTRY_SCHEMA_PATH); @Test - public void testCountries_success() { + void testCountries_success() { RestAssuredTemplates.testGet(IPV4_SUCCESS); RestAssuredTemplates.testGet(IPV6_SUCCESS); } @@ -60,7 +60,7 @@ public class CountryResourceTest { } @Test - public void testCountries_badIPs() { + void testCountries_badIPs() { EndpointTestCase testCase; @@ -74,7 +74,7 @@ public class CountryResourceTest { } // seems to be an issue with Google Guava code, only gets detected by MaxMind - testCase = TestCaseHelper.prepareSuccessCase(COUNTRY_ENDPOINT_URL, new String[] { "0.1.1.1" }, "") + testCase = TestCaseHelper.prepareTestCase(COUNTRY_ENDPOINT_URL, new String[] { "0.1.1.1" }, "") .setStatusCode(500).build(); RestAssuredTemplates.testGet(testCase); diff --git a/src/test/java/org/eclipsefoundation/geoip/client/resources/SubnetResourceTest.java b/src/test/java/org/eclipsefoundation/geoip/client/resources/SubnetResourceTest.java index 7f71d32b2d4348086fa020ce05fd7b585c6644de..1e51d33bd8ad79f5db8a2c5fea0018c92eeaa564 100644 --- a/src/test/java/org/eclipsefoundation/geoip/client/resources/SubnetResourceTest.java +++ b/src/test/java/org/eclipsefoundation/geoip/client/resources/SubnetResourceTest.java @@ -27,7 +27,7 @@ import io.quarkus.test.junit.QuarkusTest; * */ @QuarkusTest -public class SubnetResourceTest { +class SubnetResourceTest { public static final String SUBNETS_ENDPOINT_URL = "/subnets/{subnet}/{locale}"; public static final EndpointTestCase IPV4_SUCCESS = TestCaseHelper.buildSuccessCase(SUBNETS_ENDPOINT_URL, @@ -61,13 +61,13 @@ public class SubnetResourceTest { // bad ipv4 calls for (String locale : badLocales) { - testCase = TestCaseHelper.buildBadRequestCase(SUBNETS_ENDPOINT_URL, new String[] { "ipv4", locale }, ""); + testCase = TestCaseHelper.buildNotFoundCase(SUBNETS_ENDPOINT_URL, new String[] { "ipv4", locale }, ""); RestAssuredTemplates.testGet(testCase); } // bad ipv6 calls for (String locale : badLocales) { - testCase = TestCaseHelper.buildBadRequestCase(SUBNETS_ENDPOINT_URL, new String[] { "ipv6", locale }, ""); + testCase = TestCaseHelper.buildNotFoundCase(SUBNETS_ENDPOINT_URL, new String[] { "ipv6", locale }, ""); RestAssuredTemplates.testGet(testCase); } } @@ -79,7 +79,7 @@ public class SubnetResourceTest { // check other permutations (regex endpoint) for (String subnet : badSubnets) { - testCase = TestCaseHelper.buildBadRequestCase(SUBNETS_ENDPOINT_URL, new String[] { subnet, "ca" }, ""); + testCase = TestCaseHelper.buildNotFoundCase(SUBNETS_ENDPOINT_URL, new String[] { subnet, "ca" }, ""); RestAssuredTemplates.testGet(testCase); } } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index a0ce969b87b37753457c8a8583014cdf09a0ba19..d26daa68f42eeca4a3022c46163de27cbdb5a705 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1 +1,4 @@ + +eclipse.maxmind.root=./tmp/maxmind + quarkus.jacoco.includes=**/geoip/**/* \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 12b0cec2f0e736a1170ef2f8867e70323f7c7348..6ff50998e3e73e0d3bbe6f477ffc1f88eaa59436 100644 --- a/yarn.lock +++ b/yarn.lock @@ -266,6 +266,18 @@ dependency-graph@~0.11.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== +eclipsefdn-api-support@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eclipsefdn-api-support/-/eclipsefdn-api-support-1.0.0.tgz#c8937b5ca0d78a8b40af893853a0f47dd6e9b5fd" + integrity sha512-SjhoMWW1JADNJzFd5cQhiYnFGctSNGvhc+U4AH+1/eHA/Qbcs7SatpHLn9pBNSHFq0JqYjLa+9yMznc3FrSJFA== + dependencies: + "@openapi-contrib/openapi-schema-to-json-schema" "^3.1.1" + "@redocly/openapi-cli" "^1.0.0-beta.54" + "@stoplight/json-ref-resolver" "^3.1.2" + decamelize "^5.0.0" + js-yaml "^4.1.0" + yargs "^17.0.1" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"