├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── dockerhub-description.yaml │ ├── smoke-test.yaml │ └── weekly-release.yaml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── examples ├── README.md ├── docker-compose.yaml ├── kubernetes-deployment.yml └── nginx-geoip │ ├── README.md │ ├── docker-compose.yaml │ └── localhost.conf.template ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── observabilitystack │ │ └── geoip │ │ ├── GeoIpApi.java │ │ ├── GeoIpApiMetricsExporter.java │ │ ├── GeoIpEntry.java │ │ ├── GeolocationProvider.java │ │ ├── LookupException.java │ │ ├── MaxmindGeolocationDatabase.java │ │ ├── NativeImageConfiguration.java │ │ └── web │ │ ├── GeoIpEntryHttpHeaders.java │ │ ├── GeoIpEntryLinkHttpHeaders.java │ │ ├── GeoIpRestController.java │ │ ├── InetAdressPropertyEditor.java │ │ ├── InvalidIpAddressException.java │ │ └── TooManyAddressesException.java └── resources │ ├── application.yml │ └── log4j2.xml └── test ├── bats └── smoke-test.sh └── java └── org └── observabilitystack └── geoip ├── GeoIpEntryTest.java ├── MaxmindGeolocationDatabaseTest.java ├── RestApiIT.java ├── SpringBootActuatorIT.java └── web └── GeoIpRestControllerTest.java /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | examples 3 | target 4 | #!target/*.jar -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "docker" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | reviewers: 9 | - "tboeghk" 10 | - package-ecosystem: "maven" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | reviewers: 15 | - "tboeghk" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | reviewers: 21 | - "tboeghk" 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: continuous integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | - feature/** 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Java 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: 17 22 | distribution: temurin 23 | cache: 'maven' 24 | 25 | - name: Build JAR from Maven 26 | run: mvn --batch-mode --no-transfer-progress clean verify -Drevisison=$(date +%Y-%V) 27 | 28 | docker: 29 | needs: build 30 | runs-on: ubuntu-latest 31 | steps: 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - uses: actions/checkout@v4 40 | 41 | - name: Build Docker Image 42 | uses: docker/build-push-action@v6 43 | with: 44 | push: false 45 | build-args: | 46 | MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} 47 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yaml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - README.md 9 | - .github/workflows/dockerhub-description.yml 10 | 11 | jobs: 12 | dockerhub-description: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Update Docker Hub description 18 | uses: peter-evans/dockerhub-description@v4 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | repository: ${{ github.repository }} 23 | short-description: ${{ github.event.repository.description }} 24 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test.yaml: -------------------------------------------------------------------------------- 1 | name: daily smoke test 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 5 * * *' 6 | jobs: 7 | smoke-test: 8 | runs-on: ubuntu-latest 9 | container: 10 | image: bats/bats:1.9.0 11 | 12 | services: 13 | geoip-api: 14 | image: ghcr.io/${{ github.repository }}:latest 15 | ports: 16 | - 8080:8080 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - run: | 21 | apk add curl jq 22 | ./src/test/bats/smoke-test.sh 23 | -------------------------------------------------------------------------------- /.github/workflows/weekly-release.yaml: -------------------------------------------------------------------------------- 1 | name: geoip-api weekly release 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 3 * * WED' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - name: Set up QEMU 12 | uses: docker/setup-qemu-action@v3 13 | 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v3 16 | 17 | - name: Login to DockerHub 18 | uses: docker/login-action@v3 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | 23 | - name: Log in to GitHub Docker Registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - uses: actions/checkout@v4 31 | 32 | - id: determine-tag-and-build-date 33 | name: Determine Docker tag 34 | run: | 35 | echo build_tag=$(date +%Y-%V) >> $GITHUB_ENV 36 | echo build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") >> $GITHUB_ENV 37 | 38 | - name: Build and push to DockerHub registry 39 | uses: docker/build-push-action@v6 40 | with: 41 | platforms: linux/amd64,linux/arm64 42 | push: true 43 | build-args: | 44 | MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} 45 | CREATED_AT=${{ env.build_date }} 46 | VERSION=${{ env.build_tag }} 47 | GIT_REVISION=${{ github.sha }} 48 | tags: | 49 | ${{ github.repository }}:${{ env.build_tag }} 50 | ${{ github.repository }}:latest 51 | ${{ github.repository }}:latest-native 52 | ghcr.io/${{ github.repository }}:${{ env.build_tag }} 53 | ghcr.io/${{ github.repository }}:latest 54 | ghcr.io/${{ github.repository }}:latest-native 55 | 56 | create-release: 57 | runs-on: ubuntu-latest 58 | needs: build 59 | steps: 60 | 61 | - uses: actions/checkout@v4 62 | 63 | - id: determine-tag 64 | name: Determine Docker tag 65 | run: echo "::set-output name=tag::$(date +%Y-%V)" 66 | 67 | - name: Create Release 68 | uses: ncipollo/release-action@v1 69 | with: 70 | tag: ${{ steps.determine-tag.outputs.tag }} 71 | body: | 72 | Weekly release with updated geoip information 73 | as of week ${{ steps.determine-tag.outputs.tag }}. 74 | allowUpdates: true 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .classpath 3 | .DS_Store 4 | .vscode 5 | .factorypath 6 | .settings 7 | .project 8 | .idea 9 | *.iml 10 | 11 | dump.rdb 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG GEOIP-API 2 | =================== 3 | 4 | 2023-01 (2022-12-31) 5 | ------------------- 6 | 7 | * Update to Spring Boot 3.0.1 8 | * Native build is the default build 9 | * Use graalvm-maven-builder builder image 10 | 11 | 2021-46 (2021-11-17) 12 | ------------------- 13 | 14 | * Add geoip database specific metrics you can alert 15 | the databases age with. 16 | * Add the possiblity to query geoip data by HTTP header 17 | * Add a native Docker image with ~100x faster startup time 18 | 19 | 2021-45 (2021-11-10) 20 | ------------------- 21 | * Push Docker images to the Github Container Registry as well. 22 | As Docker 23 | 24 | 2021-43 (2021-10-20) 25 | ------------------- 26 | * Bump Spring Boot to 2.5.6 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # (1) build stage 3 | # --------------------------------------------------------------------- 4 | FROM observabilitystack/graalvm-maven-builder:21.0.1-ol9 AS builder 5 | ARG MAXMIND_LICENSE_KEY 6 | 7 | ADD . /build 8 | WORKDIR /build 9 | 10 | # Build application 11 | RUN mvn -B native:compile -P native --no-transfer-progress -DskipTests=true && \ 12 | chmod +x /build/target/geoip-api 13 | 14 | # Download recent geoip data 15 | RUN curl -sfSL "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=${MAXMIND_LICENSE_KEY}" | tar -xz && \ 16 | curl -sfSL "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&suffix=tar.gz&license_key=${MAXMIND_LICENSE_KEY}" | tar -xz && \ 17 | mv GeoLite2-City_*/GeoLite2-City.mmdb . && \ 18 | mv GeoLite2-ASN_*/GeoLite2-ASN.mmdb . 19 | 20 | # --------------------------------------------------------------------- 21 | # (2) run stage 22 | # --------------------------------------------------------------------- 23 | FROM debian:bookworm-slim 24 | ARG CREATED_AT 25 | ARG VERSION 26 | ARG GIT_REVISION 27 | # 28 | ## Add labels to identify release 29 | LABEL org.opencontainers.image.authors="Torsten B. Köster " \ 30 | org.opencontainers.image.url="https://github.com/observabilitystack/geoip-api" \ 31 | org.opencontainers.image.licenses="Apache-2.0" \ 32 | org.opencontainers.image.title="Geoip-API" \ 33 | org.opencontainers.image.description="A JSON REST API for Maxmind GeoIP databases" \ 34 | org.opencontainers.image.created="${CREATED_AT}" \ 35 | org.opencontainers.image.version="${VERSION}" \ 36 | org.opencontainers.image.revision="${GIT_REVISION}" 37 | 38 | ## install curl for healthcheck 39 | RUN apt-get update && \ 40 | apt-get install -y curl && \ 41 | apt-get clean 42 | 43 | ## place app and data 44 | COPY --from=builder "/build/target/geoip-api" /srv/geoip-api 45 | COPY --from=builder "/build/GeoLite2-City.mmdb" /srv/GeoLite2-City.mmdb 46 | COPY --from=builder "/build/GeoLite2-ASN.mmdb" /srv/GeoLite2-ASN.mmdb 47 | 48 | ENV CITY_DB_FILE /srv/GeoLite2-City.mmdb 49 | ENV ASN_DB_FILE /srv/GeoLite2-ASN.mmdb 50 | HEALTHCHECK --interval=5s --timeout=1s CMD curl -f http://localhost:8080/actuator/health 51 | EXPOSE 8080 52 | CMD exec /srv/geoip-api 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IP Geolocation REST API 2 | 3 | [![geoip-api ci build](https://github.com/observabilitystack/geoip-api/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/observabilitystack/geoip-api/actions/workflows/ci.yaml) 4 | [![docker-pulls](https://img.shields.io/docker/pulls/observabilitystack/geoip-api)](https://hub.docker.com/r/observabilitystack/geoip-api) 5 | ![GitHub Release Date](https://img.shields.io/github/release-date/observabilitystack/geoip-api) 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/observabilitystack/geoip-api) 7 | ![apache license](https://img.shields.io/github/license/observabilitystack/geoip-api) 8 | 9 | > ♻️ This is the official and maintained fork of the original [@shopping24](https://github.com/shopping24) repository maintained by [@tboeghk](https://thiswayup.de). 10 | 11 | This project provides a simple REST web service which returns geolocation information for a given IP address. The service loads location information from [Maxminds GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) or GeoIP2 City (commercial) database. 12 | 13 | You can use this project in a microservice infrastructure if you have multiple services requesting geolocation data. This service can be used together with the [Logstash http filter plugin](https://www.elastic.co/guide/en/logstash/current/plugins-filters-http.html) to enrich log data. 14 | 15 | * [Running the container](#running-the-container) 16 | * [Using the API](#using-the-api) 17 | * [Kubernetes & Docker-Compose Examples](#examples) 18 | 19 | ## 💨 Running the container 20 | 21 | The Docker image available on Docker Hub comes bundled with a recent [GeoLite2 city database](https://dev.maxmind.com/geoip/geoip2/geolite2/). The container is built every week with a recent version of the database. 22 | 23 | ``` 24 | $ docker run -p 8080:8080 ghcr.io/observabilitystack/geoip-api:latest 25 | ``` 26 | 27 | ``` 28 | $ docker run -p 8080:8080 observabilitystack/geoip-api:latest 29 | ``` 30 | 31 | ### Available tags & repositories 32 | 33 | > 💡 Although running containers tagged as _latest_ is 34 | > not recommended in production, for geoip-api we highly 35 | > recommend this to have the most up-to-data geoip 36 | > data. 37 | 38 | * Images are available on both [Docker Hub](https://hub.docker.com/repository/docker/observabilitystack/geoip-api) and [GitHub Container Registry](https://github.com/observabilitystack/geoip-api/pkgs/container/geoip-api). 39 | * The images are tagged in `yyyy-VV` (year & week number) format. 40 | * The most recent container is tagged as `latest`. 41 | * Images are available for `amd64` and `arm64` architectures. 42 | * Updates (data & code) are released weekly. 43 | 44 | ### Using a custom (commercial) database 45 | 46 | > ☝️ When running in production, using a commercial [Maxmind GeoIP2 City database](https://www.maxmind.com/en/geoip2-city) is highly recommeded due to it's increased 47 | precision and general data quality. 48 | 49 | You can mount the database in _mmdb_ format into the container. The location of the database can be customized using the following 50 | variables: 51 | 52 | | Variable | Description | Default value | 53 | | -------- | ----------- | ------------- | 54 | | CITY_DB_FILE | The location of the GeoIP2 City or GeoLite2 database file. | `/srv/GeoLite2-City.mmdb` | 55 | | ASN_DB_FILE | The location of the GeoIP2 ASN database file. | `/srv/GeoLite2-ASN.mmdb` | 56 | | ISP_DB_FILE | The location of the GeoIP2 ISP database file. | (none) | 57 | 58 | ### Examples 59 | 60 | The [`examples`](examples/) folder contains examples how 61 | to run _geoip-api_ in Docker-Compose or Kubernetes. 62 | 63 | ## 🤓 Using the API 64 | 65 | The prefered way to query the API is via simple HTTP GET requests. Supply 66 | the ip address to resolve as path variable. 67 | 68 | ```bash 69 | $ curl -s http://localhost:8080/8.8.8.8 70 | { 71 | "country": "US", 72 | "latitude": "37.751", 73 | "longitude": "-97.822", 74 | "continent": "NA", 75 | "timezone": "America/Chicago", 76 | "accuracyRadius": 1000, 77 | "asn": 15169, 78 | "asnOrganization": "GOOGLE", 79 | "asnNetwork": "8.8.8.0/24" 80 | } 81 | $ curl -s "http://localhost:8080/$(curl -s https://ifconfig.me/ip)" 82 | { 83 | "country": "DE", 84 | "stateprov": "Free and Hanseatic City of Hamburg", 85 | "stateprovCode": "HH", 86 | "city": "Hamburg", 87 | "latitude": "53.5742", 88 | "longitude": "10.0497", 89 | "continent": "EU", 90 | "timezone": "Europe/Berlin", 91 | "asn": 15943, 92 | "asnOrganization": "wilhelm.tel GmbH" 93 | } 94 | ``` 95 | 96 | ### Additional data links 97 | 98 | Geoip-API returns links to extensive absuse and ripe information 99 | in the `Link` header. The `ripe-asn` information can be retrieved 100 | directly. The `abuse` link requires registration and 101 | [an API key for retrieval](https://docs.abuseipdb.com/#introduction). 102 | 103 | ```bash 104 | curl "http://localhost:8080/$(curl -s https://ifconfig.me/ip)" 105 | HTTP/1.1 200 106 | Link: ; rel="abuse", 107 | ; rel="ripe-asn" 108 | 109 | { 110 | "country": "DE", 111 | "stateprov": "Land Berlin", 112 | "stateprovCode": "BE", 113 | "city": "Berlin", 114 | "latitude": "52.5196", 115 | "longitude": "13.4069", 116 | "continent": "EU", 117 | "timezone": "Europe/Berlin", 118 | "accuracyRadius": 200, 119 | "asn": 8881, 120 | "asnOrganization": "1&1 Versatel Deutschland GmbH", 121 | "asnNetwork": "104.151.0.0/17" 122 | } 123 | ``` 124 | 125 | ### Querying by HTTP header 126 | 127 | The `X-Geoip-Address` header is an alternative way to query the api 128 | (as used in the [Nginx-Geoip example](examples/nginx-geoip/)). Here 129 | the address to resolve is supplied as header value. The geoip information 130 | is returned as header values as well. The return code is always `204`. 131 | 132 | 133 | ```bash 134 | $ curl -sI -H "X-Geoip-Address: $(curl -s https://ifconfig.me/ip)" "http://localhost:8080/" 135 | HTTP/1.1 204 136 | X-Geoip-Country: DE 137 | X-Geoip-StateProv: Free and Hanseatic City of Hamburg 138 | X-Geoip-City: Hamburg 139 | X-Geoip-Latitude: 53.6042 140 | X-Geoip-Longitude: 10.0596 141 | X-Geoip-Continent: EU 142 | X-Geoip-Timezone: Europe/Berlin 143 | ``` 144 | 145 | ### Querying multiple IPs via POST 146 | 147 | ```bash 148 | curl --location 'http://localhost:8080/' \ 149 | --header 'Content-Type: application/json' \ 150 | --data '[ 151 | "8.8.8.8", 152 | "8.8.4.4" 153 | ]' 154 | 155 | { 156 | "8.8.4.4": { 157 | "country":"US", 158 | "latitude":"37.751", 159 | "longitude":"-97.822", 160 | "continent":"NA", 161 | "timezone":"America/Chicago", 162 | "accuracyRadius":1000, 163 | "asn":15169, 164 | "asnOrganization":"GOOGLE", 165 | "asnNetwork":"8.8.4.0/24" 166 | }, 167 | "8.8.8.8": { 168 | "country":"US", 169 | "latitude":"37.751", 170 | "longitude":"-97.822", 171 | "continent":"NA", 172 | "timezone":"America/Chicago", 173 | "accuracyRadius":1000, 174 | "asn":15169, 175 | "asnOrganization":"GOOGLE", 176 | "asnNetwork":"8.8.8.0/24" 177 | } 178 | } 179 | ``` 180 | 181 | Takes up to 100 addresses at once. 182 | 183 | Result will be a JSON object indexed by requested IP address. 184 | 185 | If a requested address can not be resolved, the entry will be missing in the response. 186 | 187 | ## 📦 Building the project 188 | 189 | The project is built through multi stage Docker builds. You need 190 | a valid Maxmind lincense key to download the Geoip2 database. 191 | 192 | ```shell 193 | $ export MAXMIND_LICENSE_KEY=... 194 | $ docker build \ 195 | --build-arg MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY} \ 196 | --build-arg VERSION=$(date +%Y-%V) \ 197 | -t geoip-api:latest . 198 | ``` 199 | 200 | If you want to build (or test) a multi-platform build, use 201 | the [Docker Buildx extension](https://docs.docker.com/buildx/working-with-buildx/): 202 | 203 | ```shell 204 | $ docker buildx create --use --name multi-platform 205 | $ docker buildx build \ 206 | --platform linux/amd64,linux/arm64 \ 207 | --build-arg MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY} \ 208 | -t geoip-api:latest-native -f Dockerfile.native . 209 | ``` 210 | 211 | ## 👋 Contributing 212 | 213 | We're looking forward to your comments, issues and pull requests! 214 | 215 | ## ⚖️ License 216 | 217 | This project is licensed under the [Apache License, Version 2](http://www.apache.org/licenses/LICENSE-2.0.html). 218 | 219 | This product includes GeoLite2 data created by MaxMind, available from 220 | https://www.maxmind.com. 221 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Geoip-API deployment examples 2 | 3 | This folder contains examples, how to deploy Geoip-API in different scenarios. 4 | 5 | ## `docker-compose.yaml` 6 | 7 | A very simple [Docker-Compose example](docker-compose.yaml). 8 | 9 | ## `kubernetes-deployment.yaml` 10 | 11 | A [simple Kubernetes deployment](kubernetes-deployment.yaml) that creates a deployment and a service. 12 | Use the Ingress definition below to direct web traffic to your Geoip-API 13 | instance. 14 | 15 | ```yaml 16 | --- 17 | apiVersion: networking.k8s.io/v1 18 | kind: Ingress 19 | metadata: 20 | name: geoip 21 | spec: 22 | rules: 23 | - host: geoip.YOURDOMAIN 24 | http: 25 | paths: 26 | - path: / 27 | pathType: Prefix 28 | backend: 29 | service: 30 | name: geoip 31 | port: 32 | name: web 33 | ``` 34 | 35 | ## [nginx-geoip](nginx-geoip/) 36 | 37 | A more comprehensive example to supply geoip information to Nginx using a centralized 38 | Geoip-API webservice. 39 | -------------------------------------------------------------------------------- /examples/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # This is a very basic Docker Compose 2 | # deployment. 3 | version: '2.4' 4 | 5 | services: 6 | geoip: 7 | image: ghcr.io/observabilitystack/geoip-api:latest 8 | ports: 9 | - 8080:8080 10 | -------------------------------------------------------------------------------- /examples/kubernetes-deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: geoip 6 | spec: 7 | ports: 8 | - name: web 9 | port: 8080 10 | selector: 11 | name: geoip 12 | 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: geoip 18 | spec: 19 | replicas: 1 20 | strategy: 21 | type: Recreate 22 | selector: 23 | matchLabels: 24 | name: geoip 25 | template: 26 | metadata: 27 | labels: 28 | name: geoip 29 | annotations: 30 | "prometheus.io/scrape": "true" 31 | "prometheus.io/path": "/actuator/prometheus" 32 | "prometheus.io/port": "8080" 33 | 34 | spec: 35 | containers: 36 | 37 | - name: geoip 38 | image: ghcr.io/observabilitystack/geoip-api:latest 39 | resources: 40 | requests: 41 | cpu: "100m" 42 | memory: "1Gi" 43 | limits: 44 | cpu: "500m" 45 | memory: "1Gi" 46 | ports: 47 | - containerPort: 8080 48 | name: web 49 | readinessProbe: 50 | httpGet: 51 | path: /actuator/health 52 | port: 8080 53 | scheme: HTTP 54 | initialDelaySeconds: 30 55 | failureThreshold: 20 56 | periodSeconds: 3 57 | --- 58 | apiVersion: networking.k8s.io/v1 59 | kind: Ingress 60 | metadata: 61 | name: geoip 62 | spec: 63 | rules: 64 | - host: geoip.${HOSTNAME} 65 | http: 66 | paths: 67 | - path: / 68 | pathType: Prefix 69 | backend: 70 | service: 71 | name: geoip 72 | port: 73 | name: web 74 | -------------------------------------------------------------------------------- /examples/nginx-geoip/README.md: -------------------------------------------------------------------------------- 1 | # Geoip-API as central geoip lookup for Nginx 2 | 3 | This example is suitable for larger scale environments, where you do not 4 | want to use the native [Nginx Geoip module](http://nginx.org/en/docs/http/ngx_http_geoip_module.html). Using this example, you have to update the 5 | geoip data only at a single location - by pulling a more recent Docker 6 | image weekly. 7 | 8 | ## Launching the example 9 | 10 | The Nginx can be queried on port `8080`. For every request, it will issue 11 | an internal subrequest to the Geoip-API and enrich the request (to the 12 | client or reverse proxy target) with geoip information in headers. 13 | 14 | ``` 15 | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 16 | ┌─────────────┐ │ 17 | │ │ Geoip-API │ 18 | └──▲───────┬──┘ │ 19 | │ │ │ 20 | │ │ │ 21 | http │ │ │ ┌─────────────┐ 22 | (8080) ┌──┴───────▼──┐ │Reverse proxy│ │ 23 | ─────────┼────▶│ Nginx │───────▶│ target │ 24 | └─────────────┘ │ │ │ 25 | │ └─────────────┘ 26 | ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ 27 | ``` 28 | 29 | Launch the Docker Compose example and start querying Geoip-API via Nginx: 30 | 31 | ```bash 32 | $ docker-compose up 33 | ``` 34 | 35 | ## Querying Nginx 36 | 37 | Nginx uses the clients ip address for it's geoip lookup. As you are 38 | querying on your machine, your ip address falls in the 39 | [RFC1918](https://en.wikipedia.org/wiki/Private_network) range and 40 | will not return any results. 41 | 42 | Nginx is configured to recognize the `X-Forwarded-For` sent by reverse 43 | proxies to contain the original client ip. We can use this behaviour 44 | to query the Geoip-API using custom ip addresses, in this case our 45 | public ip address: 46 | 47 | ```bash 48 | $ curl -sI -H "X-Forwarded-For: $(curl -s https://ifconfig.me/ip)" "http://localhost:8080/" 49 | HTTP/1.1 200 OK 50 | Server: nginx/1.21.4 51 | Content-Type: text/html 52 | Connection: keep-alive 53 | X-Geoip-Country: DE 54 | X-Geoip-StateProv: Free and Hanseatic City of Hamburg 55 | X-Geoip-City: Hamburg 56 | X-Geoip-Latitude: 53.6042 57 | X-Geoip-Longitude: 10.0596 58 | X-Geoip-Continent: EU 59 | X-Geoip-Timezone: Europe/Berlin 60 | Accept-Ranges: bytes 61 | ``` 62 | 63 | ## Querying Geoip-API using headers 64 | 65 | Internally, the Geoip-API is queried by Nginx using this request: 66 | 67 | ```bash 68 | $ curl -sI -H "X-Geoip-Address: $(curl -s https://ifconfig.me/ip)" "http://localhost:8081/" 69 | HTTP/1.1 204 70 | X-Geoip-Country: DE 71 | X-Geoip-StateProv: Free and Hanseatic City of Hamburg 72 | X-Geoip-City: Hamburg 73 | X-Geoip-Latitude: 53.6042 74 | X-Geoip-Longitude: 10.0596 75 | X-Geoip-Continent: EU 76 | X-Geoip-Timezone: Europe/Berlin 77 | ``` 78 | -------------------------------------------------------------------------------- /examples/nginx-geoip/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # This is a very basic Docker Compose 2 | # deployment. 3 | version: '2.4' 4 | 5 | services: 6 | geoip: 7 | image: ghcr.io/observabilitystack/geoip-api:latest 8 | ports: 9 | - 8081:8080 10 | 11 | nginx: 12 | image: nginx:latest 13 | ports: 14 | - 8080:8080 15 | volumes: 16 | - .:/etc/nginx/templates 17 | -------------------------------------------------------------------------------- /examples/nginx-geoip/localhost.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080 default_server; 3 | root /usr/share/nginx/html; 4 | 5 | # This uses the value in X-Forwarded-For as remote_addr 6 | # that will be sent to the Geoip instance later 7 | set_real_ip_from 192.168.0.0/16; 8 | set_real_ip_from 172.16.0.0/12; 9 | set_real_ip_from 10.0.0.0/8; 10 | real_ip_header X-Forwarded-For; 11 | 12 | # Make a subrequest to the /geoip handler defined below. 13 | auth_request @geoip; 14 | 15 | # Transfer header values returned from the auth_request into 16 | # Nginx variables 17 | auth_request_set $geoip_country $upstream_http_x_geoip_country; 18 | auth_request_set $geoip_stateprov $upstream_http_x_geoip_stateprov; 19 | auth_request_set $geoip_city $upstream_http_x_geoip_city; 20 | auth_request_set $geoip_latitude $upstream_http_x_geoip_latitude; 21 | auth_request_set $geoip_longitude $upstream_http_x_geoip_longitude; 22 | auth_request_set $geoip_continent $upstream_http_x_geoip_continent; 23 | auth_request_set $geoip_timezone $upstream_http_x_geoip_timezone; 24 | auth_request_set $geoip_asn $upstream_http_x_geoip_asn; 25 | auth_request_set $geoip_asnorganization $upstream_http_x_geoip_asnorganization; 26 | 27 | # Use the variables we defined above to send header values back 28 | # to the client. To send those values further to a downstream 29 | # reverse proxy target, use proxy_set_header directive 30 | add_header X-Geoip-Country $geoip_country always; 31 | add_header X-Geoip-StateProv $geoip_stateprov always; 32 | add_header X-Geoip-City $geoip_city always; 33 | add_header X-Geoip-Latitude $geoip_latitude always; 34 | add_header X-Geoip-Longitude $geoip_longitude always; 35 | add_header X-Geoip-Continent $geoip_continent always; 36 | add_header X-Geoip-Timezone $geoip_timezone always; 37 | add_header X-Geoip-Asn $geoip_asn always; 38 | add_header X-Geoip-AsnOrganization $geoip_asnorganization always; 39 | 40 | # a internal handler for subrequest to the Geoip-Api. Populate 41 | # the X-Geoip-Address with the clients ip address (retrieved from 42 | # the X-Forwarded-For header) 43 | location = @geoip { 44 | internal; 45 | 46 | proxy_pass http://geoip:8080/; 47 | proxy_pass_request_body off; 48 | proxy_set_header X-Geoip-Address $remote_addr; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.observabilitystack 5 | geoip-api 6 | GeoIP-API 7 | jar 8 | ${revision} 9 | A REST API to query GeoIP databases 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 3.2.5 15 | 16 | 17 | 18 | 0.0.1-SNAPSHOT 19 | 17 20 | org.observabilitystack.geoip.GeoIpApi 21 | 22 | 23 | 24 | 25 | com.google.guava 26 | guava 27 | 33.2.1-jre 28 | 29 | 30 | org.projectlombok 31 | lombok 32 | 1.18.34 33 | provided 34 | 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-web 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-actuator 46 | 47 | 48 | io.micrometer 49 | micrometer-registry-prometheus 50 | runtime 51 | 52 | 53 | com.maxmind.geoip2 54 | geoip2 55 | 4.2.0 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.graalvm.buildtools 70 | native-maven-plugin 71 | 72 | 73 | 78 | 82 | 85 | -march=compatibility 86 | 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-maven-plugin 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/GeoIpApi.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Optional; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.BeanInitializationException; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Qualifier; 12 | import org.springframework.boot.SpringApplication; 13 | import org.springframework.boot.autoconfigure.SpringBootApplication; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.ImportRuntimeHints; 16 | 17 | import com.maxmind.db.CHMCache; 18 | import com.maxmind.geoip2.DatabaseReader; 19 | 20 | @SpringBootApplication(proxyBeanMethods = false) 21 | @ImportRuntimeHints(NativeImageConfiguration.class) 22 | public class GeoIpApi { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(GeoIpApi.class); 25 | 26 | public static void main(String[] args) { 27 | SpringApplication.run(GeoIpApi.class, args); 28 | } 29 | 30 | @Bean 31 | public GeolocationProvider cityProvider( 32 | @Autowired(required = false) @Qualifier("cityDatabaseReader") DatabaseReader cityDatabaseReader, 33 | @Autowired(required = false) @Qualifier("asnDatabaseReader") DatabaseReader asnDatabaseReader, 34 | @Autowired(required = false) @Qualifier("ispDatabaseReader") DatabaseReader ispDatabaseReader) { 35 | if (cityDatabaseReader == null && ispDatabaseReader == null && asnDatabaseReader == null) { 36 | throw new BeanInitializationException("Neither CITY_DB_FILE nor ASN_DB_FILE nor ISP_DB_FILE given"); 37 | } 38 | 39 | return new MaxmindGeolocationDatabase(cityDatabaseReader, asnDatabaseReader, ispDatabaseReader); 40 | } 41 | 42 | @Bean(name = "cityDatabaseReader") 43 | //@ConditionalOnProperty("CITY_DB_FILE") 44 | public DatabaseReader cityDatabaseReader() throws IOException { 45 | return buildDatabaseReaderFromEnvironment("CITY_DB_FILE"); 46 | } 47 | 48 | @Bean(name = "asnDatabaseReader") 49 | //@ConditionalOnProperty("ASN_DB_FILE") 50 | public DatabaseReader asnDatabaseReader() throws IOException { 51 | return buildDatabaseReaderFromEnvironment("ASN_DB_FILE"); 52 | } 53 | 54 | @Bean(name = "ispDatabaseReader") 55 | //@ConditionalOnProperty("ISP_DB_FILE") 56 | public DatabaseReader ispDatabaseReader() throws IOException { 57 | return buildDatabaseReaderFromEnvironment("ISP_DB_FILE"); 58 | } 59 | 60 | private DatabaseReader buildDatabaseReaderFromEnvironment(String environment) throws IOException { 61 | Optional filename = Optional.ofNullable(System.getenv(environment)); 62 | 63 | if (filename.isPresent()) { 64 | return buildDatabaseReader(filename.get()); 65 | } 66 | 67 | return null; 68 | } 69 | 70 | private DatabaseReader buildDatabaseReader(String fileName) throws IOException { 71 | File file = new File(fileName); 72 | DatabaseReader bean = new DatabaseReader.Builder(file).withCache(new CHMCache()).build(); 73 | logger.info("Loaded database file {} (build date: {})", file, bean.getMetadata().getBuildDate()); 74 | return bean; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/GeoIpApiMetricsExporter.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import java.util.Collection; 4 | import java.util.Date; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import com.maxmind.geoip2.DatabaseReader; 8 | 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.stereotype.Component; 11 | 12 | import io.micrometer.core.instrument.MeterRegistry; 13 | import io.micrometer.core.instrument.Tags; 14 | 15 | @Component 16 | @Profile("!test") 17 | public class GeoIpApiMetricsExporter { 18 | 19 | public GeoIpApiMetricsExporter(MeterRegistry registry, Collection databases) { 20 | for (DatabaseReader db : databases) { 21 | Tags tags = Tags.of( 22 | "type", db.getMetadata().getDatabaseType().toLowerCase()); 23 | 24 | registry.gauge("geoip.database", tags, 1d); 25 | registry.gauge("geoip.database_age_days", tags, db.getMetadata(), m -> { 26 | long ageInMillis = Math.abs(new Date().getTime() - m.getBuildDate().getTime()); 27 | return TimeUnit.DAYS.convert(ageInMillis, TimeUnit.MILLISECONDS); 28 | }); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/GeoIpEntry.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.google.common.base.MoreObjects; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | 11 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; 12 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; 13 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; 14 | 15 | /** 16 | * A geolocation database entry. 17 | */ 18 | @JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE) 19 | @JsonInclude(NON_EMPTY) 20 | @lombok.Builder(setterPrefix = "set") 21 | @ToString 22 | @Getter 23 | @AllArgsConstructor 24 | public class GeoIpEntry { 25 | 26 | private final String country; 27 | private final String stateprov; 28 | private final String stateprovCode; 29 | private final String mobileCountryCode; 30 | private final String mobileNetworkCode; 31 | private final String city; 32 | private final String latitude; 33 | private final String longitude; 34 | private final String continent; 35 | private final String timezone; 36 | private final Integer usMetroCode; 37 | private final Integer accuracyRadius; 38 | private final Integer populationDensity; 39 | private final String isp; 40 | private final String organization; 41 | private final Long asn; 42 | private final String asnOrganization; 43 | private final String asnNetwork; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/GeolocationProvider.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import java.net.InetAddress; 4 | import java.util.Optional; 5 | 6 | /** 7 | * Provides location information for an IP address. 8 | */ 9 | public interface GeolocationProvider { 10 | 11 | /** 12 | * Returns the location information for the given IP address, or {@code null} if no information was found for the 13 | * given address. 14 | * 15 | * @param addr 16 | * the IP address. 17 | */ 18 | Optional lookup(InetAddress addr); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/LookupException.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | public class LookupException 4 | extends RuntimeException { 5 | 6 | public LookupException(String message, Throwable cause) { 7 | super(message, cause); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/MaxmindGeolocationDatabase.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | 8 | import org.observabilitystack.geoip.GeoIpEntry.GeoIpEntryBuilder; 9 | 10 | import com.maxmind.geoip2.DatabaseReader; 11 | import com.maxmind.geoip2.exception.AddressNotFoundException; 12 | import com.maxmind.geoip2.exception.GeoIp2Exception; 13 | import com.maxmind.geoip2.model.AsnResponse; 14 | import com.maxmind.geoip2.model.CityResponse; 15 | import com.maxmind.geoip2.model.IspResponse; 16 | import com.maxmind.geoip2.record.City; 17 | import com.maxmind.geoip2.record.Continent; 18 | import com.maxmind.geoip2.record.Country; 19 | import com.maxmind.geoip2.record.Subdivision; 20 | 21 | /** 22 | * Implements {@code GeolocationProvider} by using the Maxmind GeoIP database. 23 | */ 24 | public class MaxmindGeolocationDatabase 25 | implements GeolocationProvider { 26 | 27 | private final DatabaseReader cityDatabaseReader; 28 | private final DatabaseReader asnDatabaseReader; 29 | private final DatabaseReader ispDatabaseReader; 30 | 31 | public MaxmindGeolocationDatabase(DatabaseReader cityDatabaseReader, DatabaseReader asnDatabaseReader, 32 | DatabaseReader ispDatabaseReader) { 33 | if (cityDatabaseReader == null && ispDatabaseReader == null && asnDatabaseReader == null) { 34 | throw new IllegalArgumentException( 35 | "At least one of cityDatabaseReader, asnDatabaseReader, ispDatabaseReader must be non-null"); 36 | } 37 | this.cityDatabaseReader = cityDatabaseReader; 38 | this.asnDatabaseReader = asnDatabaseReader; 39 | this.ispDatabaseReader = ispDatabaseReader; 40 | } 41 | 42 | @Override 43 | public Optional lookup(InetAddress addr) { 44 | GeoIpEntryBuilder responseBuilder = GeoIpEntry.builder(); 45 | boolean hasCityData = lookupCityData(addr, responseBuilder); 46 | boolean hasAsnData = lookupAsnData(addr, responseBuilder); 47 | boolean hasIspData = lookupIspData(addr, responseBuilder); 48 | if (hasCityData || hasIspData || hasAsnData) { 49 | return Optional.of(responseBuilder.build()); 50 | } 51 | return Optional.empty(); 52 | } 53 | 54 | private boolean lookupCityData(InetAddress addr, GeoIpEntryBuilder builder) { 55 | if (cityDatabaseReader == null) { 56 | return false; 57 | } 58 | try { 59 | CityResponse response = cityDatabaseReader.city(addr); 60 | 61 | Optional.ofNullable(response.getCountry()) 62 | .map(Country::getIsoCode) 63 | .ifPresent(builder::setCountry); 64 | Optional.ofNullable(response.getMostSpecificSubdivision()) 65 | .map(Subdivision::getName) 66 | .ifPresent(builder::setStateprov); 67 | Optional.ofNullable(response.getMostSpecificSubdivision()) 68 | .map(Subdivision::getIsoCode) 69 | .ifPresent(builder::setStateprovCode); 70 | Optional.ofNullable(response.getCity()) 71 | .map(City::getName) 72 | .ifPresent(builder::setCity); 73 | Optional.ofNullable(response.getContinent()) 74 | .map(Continent::getCode) 75 | .ifPresent(builder::setContinent); 76 | 77 | Optional.ofNullable(response.getLocation()) 78 | .ifPresent( 79 | location -> { 80 | Optional.ofNullable(location.getLatitude()) 81 | .map(Objects::toString) 82 | .ifPresent(builder::setLatitude); 83 | Optional.ofNullable(location.getLongitude()) 84 | .map(Objects::toString) 85 | .ifPresent(builder::setLongitude); 86 | Optional.ofNullable(location.getTimeZone()) 87 | .map(Objects::toString) 88 | .ifPresent(builder::setTimezone); 89 | Optional.ofNullable(location.getAccuracyRadius()) 90 | .ifPresent(builder::setAccuracyRadius); 91 | Optional.ofNullable(location.getPopulationDensity()) 92 | .ifPresent(builder::setPopulationDensity); 93 | Optional.ofNullable(location.getMetroCode()) 94 | .ifPresent(builder::setUsMetroCode); 95 | }); 96 | 97 | return true; 98 | 99 | } catch (AddressNotFoundException e) { 100 | // no city information found, this is not an error 101 | return false; 102 | } catch (IOException | GeoIp2Exception e) { 103 | throw new LookupException("Could not lookup city of address " + addr, e); 104 | } 105 | } 106 | 107 | private boolean lookupIspData(InetAddress addr, GeoIpEntryBuilder builder) { 108 | if (ispDatabaseReader == null) { 109 | return false; 110 | } 111 | try { 112 | IspResponse response = ispDatabaseReader.isp(addr); 113 | 114 | builder.setIsp(response.getIsp()) 115 | .setOrganization(response.getOrganization()) 116 | .setAsn(response.getAutonomousSystemNumber()) 117 | .setAsnOrganization(response.getAutonomousSystemOrganization()) 118 | .setMobileCountryCode(response.getMobileCountryCode()) 119 | .setMobileNetworkCode(response.getMobileNetworkCode()); 120 | 121 | Optional.ofNullable(response.getNetwork()) 122 | .map(Objects::toString) 123 | .ifPresent(builder::setAsnNetwork); 124 | 125 | return true; 126 | 127 | } catch (AddressNotFoundException e) { 128 | // no ISP information found, this is not an error 129 | return false; 130 | } catch (IOException | GeoIp2Exception e) { 131 | throw new LookupException("Could not lookup ISP of address " + addr, e); 132 | } 133 | } 134 | 135 | private boolean lookupAsnData(InetAddress addr, GeoIpEntryBuilder builder) { 136 | if (asnDatabaseReader == null) { 137 | return false; 138 | } 139 | try { 140 | AsnResponse response = asnDatabaseReader.asn(addr); 141 | 142 | builder.setAsn(response.getAutonomousSystemNumber()) 143 | .setAsnOrganization(response.getAutonomousSystemOrganization()); 144 | 145 | Optional.ofNullable(response.getNetwork()) 146 | .map(Objects::toString) 147 | .ifPresent(builder::setAsnNetwork); 148 | 149 | return true; 150 | 151 | } catch (AddressNotFoundException e) { 152 | // no ASN information found, this is not an error 153 | return false; 154 | } catch (IOException | GeoIp2Exception e) { 155 | throw new LookupException("Could not lookup ASN of address " + addr, e); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/NativeImageConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.springframework.aot.hint.MemberCategory; 6 | import org.springframework.aot.hint.RuntimeHints; 7 | import org.springframework.aot.hint.RuntimeHintsRegistrar; 8 | 9 | import com.maxmind.db.Metadata; 10 | import com.maxmind.db.Network; 11 | import com.maxmind.geoip2.NetworkDeserializer; 12 | import com.maxmind.geoip2.model.AsnResponse; 13 | import com.maxmind.geoip2.model.CityResponse; 14 | import com.maxmind.geoip2.model.IspResponse; 15 | import com.maxmind.geoip2.record.City; 16 | import com.maxmind.geoip2.record.Continent; 17 | import com.maxmind.geoip2.record.Country; 18 | import com.maxmind.geoip2.record.Location; 19 | import com.maxmind.geoip2.record.Postal; 20 | import com.maxmind.geoip2.record.RepresentedCountry; 21 | import com.maxmind.geoip2.record.Subdivision; 22 | import com.maxmind.geoip2.record.Traits; 23 | 24 | public class NativeImageConfiguration implements RuntimeHintsRegistrar { 25 | 26 | @Override 27 | public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 28 | // maxmind lib 29 | hints.reflection().registerType(Metadata.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 30 | hints.reflection().registerType(CityResponse.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 31 | hints.reflection().registerType(IspResponse.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 32 | hints.reflection().registerType(AsnResponse.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 33 | hints.reflection().registerType(Continent.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 34 | hints.reflection().registerType(Location.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 35 | hints.reflection().registerType(Postal.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 36 | hints.reflection().registerType(Country.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 37 | hints.reflection().registerType(City.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 38 | hints.reflection().registerType(RepresentedCountry.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 39 | hints.reflection().registerType(Subdivision.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 40 | hints.reflection().registerType(Traits.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 41 | hints.reflection().registerType(NetworkDeserializer.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 42 | hints.reflection().registerType(Network.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 43 | hints.reflection().registerType(GeoIpEntry.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 44 | 45 | // also used by maxmind lib 46 | hints.reflection().registerType(ArrayList.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/GeoIpEntryHttpHeaders.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | import java.util.Objects; 4 | 5 | import org.observabilitystack.geoip.GeoIpEntry; 6 | import org.springframework.http.HttpHeaders; 7 | 8 | public class GeoIpEntryHttpHeaders extends HttpHeaders { 9 | 10 | public static final String X_GEOIP_ = "X-Geoip-"; 11 | public static final String X_GEOIP_ADDRESS = X_GEOIP_ + "Address"; 12 | public static final String X_GEOIP_COUNTRY = X_GEOIP_ + "Country"; 13 | public static final String X_GEOIP_STATEPROV = X_GEOIP_ + "StateProv"; 14 | public static final String X_GEOIP_CITY = X_GEOIP_ + "City"; 15 | public static final String X_GEOIP_LATITUDE = X_GEOIP_ + "Latitude"; 16 | public static final String X_GEOIP_LONGITUDE = X_GEOIP_ + "Longitude"; 17 | public static final String X_GEOIP_CONTINENT = X_GEOIP_ + "Continent"; 18 | public static final String X_GEOIP_TIMEZONE = X_GEOIP_ + "Timezone"; 19 | public static final String X_GEOIP_ISP = X_GEOIP_ + "Isp"; 20 | public static final String X_GEOIP_ORGANIZATION = X_GEOIP_ + "Organization"; 21 | public static final String X_GEOIP_ASN = X_GEOIP_ + "Asn"; 22 | public static final String X_GEOIP_ASN_ORGANIZATION = X_GEOIP_ + "AsnOrganization"; 23 | 24 | public GeoIpEntryHttpHeaders(GeoIpEntry entry) { 25 | super(); 26 | 27 | Objects.requireNonNull(entry); 28 | add(X_GEOIP_COUNTRY, entry.getCountry()); 29 | add(X_GEOIP_STATEPROV, entry.getStateprov()); 30 | add(X_GEOIP_CITY, entry.getCity()); 31 | add(X_GEOIP_LATITUDE, entry.getLatitude()); 32 | add(X_GEOIP_LONGITUDE, entry.getLongitude()); 33 | add(X_GEOIP_CONTINENT, entry.getContinent()); 34 | add(X_GEOIP_TIMEZONE, entry.getTimezone()); 35 | add(X_GEOIP_ISP, entry.getIsp()); 36 | add(X_GEOIP_ORGANIZATION, entry.getOrganization()); 37 | 38 | if (entry.getAsn() != null) { 39 | add(X_GEOIP_ASN, String.valueOf(entry.getAsn())); 40 | add(X_GEOIP_ASN_ORGANIZATION, entry.getAsnOrganization()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/GeoIpEntryLinkHttpHeaders.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | import java.net.InetAddress; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | 8 | import org.observabilitystack.geoip.GeoIpEntry; 9 | import org.springframework.http.HttpHeaders; 10 | 11 | /** 12 | * Adds links to further information sources 13 | */ 14 | public class GeoIpEntryLinkHttpHeaders extends HttpHeaders { 15 | 16 | public GeoIpEntryLinkHttpHeaders(InetAddress address, GeoIpEntry entry) { 17 | super(); 18 | 19 | final List links = new LinkedList<>(); 20 | links.add(String.format("; rel=\"abuse\"", 21 | address.getHostAddress())); 22 | 23 | if (entry.getAsn() != null) { 24 | links.add(String.format("; rel=\"ripe-asn\"", 25 | entry.getAsn())); 26 | } 27 | 28 | add(HttpHeaders.LINK, links.stream().collect(Collectors.joining(", "))); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/GeoIpRestController.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | import java.net.InetAddress; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.TreeMap; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.WebDataBinder; 13 | import org.springframework.web.bind.annotation.CrossOrigin; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.InitBinder; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestBody; 19 | import org.springframework.web.bind.annotation.RequestHeader; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | import org.springframework.web.bind.annotation.ResponseBody; 23 | import org.springframework.web.bind.annotation.ResponseStatus; 24 | import org.springframework.web.bind.annotation.RestController; 25 | import org.observabilitystack.geoip.GeoIpEntry; 26 | import org.observabilitystack.geoip.GeolocationProvider; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import static java.util.Objects.requireNonNull; 31 | 32 | /** 33 | * Provides a Geo Lookup service for IPv4 and IPv6 addresses with the help of 34 | * DB-IP. 35 | * 36 | * @author shopping24 GmbH, Torsten Bøgh Köster (@tboeghk) 37 | */ 38 | @RestController 39 | public class GeoIpRestController { 40 | 41 | private final Logger logger = LoggerFactory.getLogger(getClass()); 42 | 43 | private final GeolocationProvider geolocations; 44 | 45 | /** 46 | * Creates a controller that serves the geolocations from the given provider. 47 | * 48 | * @param geolocations 49 | * the geolocation provider. 50 | */ 51 | @Autowired 52 | public GeoIpRestController(GeolocationProvider geolocations) { 53 | this.geolocations = requireNonNull(geolocations); 54 | } 55 | 56 | @GetMapping({ "/favicon.ico", "/robots.txt" }) 57 | public ResponseEntity handleKnownNotFounds() { 58 | return ResponseEntity.notFound().build(); 59 | } 60 | 61 | @CrossOrigin(methods = { RequestMethod.GET, RequestMethod.HEAD }, allowedHeaders = GeoIpEntryHttpHeaders.X_GEOIP_ADDRESS) 62 | @GetMapping("/") 63 | public ResponseEntity handleHeader( 64 | @RequestHeader(name = GeoIpEntryHttpHeaders.X_GEOIP_ADDRESS, required = false) InetAddress address) { 65 | if (address != null) { 66 | Optional result = geolocations.lookup(address); 67 | 68 | if (result.isPresent()) { 69 | return ResponseEntity.noContent() 70 | .headers(new GeoIpEntryHttpHeaders(result.get())) 71 | .build(); 72 | } else { 73 | return ResponseEntity.noContent().build(); 74 | } 75 | } 76 | 77 | return handleKnownNotFounds(); 78 | } 79 | 80 | /** 81 | * Lookup the geolocation information for an ip address. 82 | */ 83 | @CrossOrigin(methods = RequestMethod.GET) 84 | @GetMapping("/{address:.+}") 85 | public ResponseEntity lookup(@PathVariable("address") InetAddress address) { 86 | final Optional entry = geolocations.lookup(address); 87 | 88 | if (entry.isPresent()) { 89 | return ResponseEntity.ok() 90 | .headers(new GeoIpEntryLinkHttpHeaders(address, entry.get())) 91 | .body(entry.get()); 92 | } else { 93 | return ResponseEntity.of(entry); 94 | } 95 | } 96 | 97 | @RequestMapping(path = "/", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 98 | public ResponseEntity> multiLookup(@RequestBody InetAddress[] ipAddresses) { 99 | if (ipAddresses.length > 100) { 100 | throw new TooManyAddressesException("Only 100 address requests allowed at once"); 101 | } 102 | 103 | Map entries = new TreeMap<>(); 104 | for (InetAddress address : ipAddresses) { 105 | Optional entry = geolocations.lookup(address); 106 | if (entry.isPresent()) { 107 | entries.put(address.getHostAddress(), entry.get()); 108 | } 109 | } 110 | return ResponseEntity.ok(entries); 111 | } 112 | 113 | @ExceptionHandler(TooManyAddressesException.class) 114 | @ResponseBody 115 | @ResponseStatus(HttpStatus.BAD_REQUEST) 116 | public String handleTooManyAddressesException(TooManyAddressesException e) { 117 | return e.getMessage(); 118 | } 119 | 120 | @ExceptionHandler(InvalidIpAddressException.class) 121 | @ResponseBody 122 | @ResponseStatus(HttpStatus.BAD_REQUEST) 123 | public String handleInvalidIpAddress(Exception e) { 124 | return e.getMessage(); 125 | } 126 | 127 | @ExceptionHandler(Exception.class) 128 | @ResponseBody 129 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 130 | public String handleException(Exception e) { 131 | logger.error(e.getMessage(), e); 132 | return "We ran into an error: " + e.getMessage(); 133 | } 134 | 135 | /** 136 | * Initializes data binding. 137 | */ 138 | @InitBinder 139 | public void initBinder(WebDataBinder binder) { 140 | binder.registerCustomEditor(InetAddress.class, new InetAdressPropertyEditor()); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/InetAdressPropertyEditor.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | import java.beans.PropertyEditorSupport; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import com.google.common.net.InetAddresses; 9 | 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | public class InetAdressPropertyEditor extends PropertyEditorSupport { 13 | 14 | private final Logger logger = LoggerFactory.getLogger(getClass()); 15 | 16 | @Override 17 | public void setAsText(String text) { 18 | checkNotNull(text, "Pre-condition violated: text must not be null."); 19 | 20 | try { 21 | setValue(InetAddresses.forString(text)); 22 | } catch (IllegalArgumentException e) { 23 | logger.info("Invalid IP address given: {}", e.getMessage()); 24 | throw new InvalidIpAddressException("Invalid IP address given"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/InvalidIpAddressException.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | /** 4 | * Thrown if the REST API is called with an invalid IP address. 5 | */ 6 | public class InvalidIpAddressException extends RuntimeException { 7 | 8 | public InvalidIpAddressException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/observabilitystack/geoip/web/TooManyAddressesException.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | public class TooManyAddressesException extends RuntimeException { 4 | private static final long serialVersionUID = 742704466635106825L; 5 | 6 | public TooManyAddressesException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode: "off" 2 | management.endpoints.web.exposure.include: health, prometheus 3 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/bats/smoke-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load /usr/lib/bats/bats-support/load.bash 4 | load /usr/lib/bats/bats-assert/load.bash 5 | 6 | @test "[meta] Healthcheck ok" { 7 | curl -fs "http://geoip-api:8080/actuator/health" 8 | } 9 | 10 | @test "[meta] Prometheus metrics ok" { 11 | curl -fs "http://geoip-api:8080/actuator/prometheus" 12 | } 13 | 14 | @test "[meta] Prometheus metrics available" { 15 | curl -fs "http://geoip-api:8080/actuator/prometheus"|grep "geoip_database" 16 | } 17 | 18 | @test "[geoip] 8.8.8.8" { 19 | run curl -fs "http://geoip-api:8080/8.8.8.8" 20 | assert_success 21 | assert_output --partial '"country":"US"' 22 | assert_output --partial '"asnOrganization":"GOOGLE"' 23 | } 24 | 25 | @test "[geoip] 149.233.213.224" { 26 | run curl -fs "http://geoip-api:8080/149.233.213.224" 27 | assert_success 28 | assert_output --partial '"country":"DE"' 29 | assert_output --partial '"city":"Hamburg"' 30 | assert_output --partial '"asnOrganization":"wilhelm.tel GmbH"' 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/org/observabilitystack/geoip/GeoIpEntryTest.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class GeoIpEntryTest { 9 | 10 | @Test 11 | public void testGetters() { 12 | GeoIpEntry entry = GeoIpEntry.builder() 13 | .setCountry("country") 14 | .setStateprov("region") 15 | .setCity("city") 16 | .setContinent("continent") 17 | .setLatitude("latitude") 18 | .setLongitude("longitude") 19 | .setTimezone("timezoneName") 20 | .setIsp("isp") 21 | .setOrganization("organization") 22 | .setAsn(64512l) 23 | .setAsnOrganization("asnOrganization") 24 | .build(); 25 | 26 | assertThat(entry.getCountry()).isEqualTo("country"); 27 | assertThat(entry.getStateprov()).isEqualTo("region"); 28 | assertThat(entry.getCity()).isEqualTo("city"); 29 | assertThat(entry.getContinent()).isEqualTo("continent"); 30 | assertThat(entry.getLatitude()).isEqualTo("latitude"); 31 | assertThat(entry.getLongitude()).isEqualTo("longitude"); 32 | assertThat(entry.getTimezone()).isEqualTo("timezoneName"); 33 | assertThat(entry.getIsp()).isEqualTo("isp"); 34 | assertThat(entry.getOrganization()).isEqualTo("organization"); 35 | assertThat(entry.getAsn()).isEqualTo(64512l); 36 | assertThat(entry.getAsnOrganization()).isEqualTo("asnOrganization"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/org/observabilitystack/geoip/MaxmindGeolocationDatabaseTest.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.net.Inet4Address; 8 | import java.net.InetAddress; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | import com.google.common.collect.ImmutableMap; 15 | import com.google.common.collect.Lists; 16 | import com.google.common.net.InetAddresses; 17 | import com.maxmind.db.Network; 18 | import com.maxmind.geoip2.DatabaseReader; 19 | import com.maxmind.geoip2.model.AsnResponse; 20 | import com.maxmind.geoip2.model.CityResponse; 21 | import com.maxmind.geoip2.model.IspResponse; 22 | import com.maxmind.geoip2.record.City; 23 | import com.maxmind.geoip2.record.Continent; 24 | import com.maxmind.geoip2.record.Country; 25 | import com.maxmind.geoip2.record.Location; 26 | import com.maxmind.geoip2.record.MaxMind; 27 | import com.maxmind.geoip2.record.Postal; 28 | import com.maxmind.geoip2.record.Subdivision; 29 | import com.maxmind.geoip2.record.Traits; 30 | 31 | import org.junit.jupiter.api.BeforeEach; 32 | import org.junit.jupiter.api.Test; 33 | import org.mockito.Mockito; 34 | import org.observabilitystack.geoip.GeoIpEntry; 35 | import org.observabilitystack.geoip.MaxmindGeolocationDatabase; 36 | 37 | public class MaxmindGeolocationDatabaseTest { 38 | 39 | private DatabaseReader cityDatabaseReader = Mockito.mock(DatabaseReader.class); 40 | private DatabaseReader asnDatabaseReader = Mockito.mock(DatabaseReader.class); 41 | private DatabaseReader ispDatabaseReader = Mockito.mock(DatabaseReader.class); 42 | private MaxmindGeolocationDatabase db = new MaxmindGeolocationDatabase(cityDatabaseReader, asnDatabaseReader, ispDatabaseReader); 43 | 44 | @BeforeEach 45 | public void setUp() throws Exception { 46 | List locales = Collections.singletonList("DE"); 47 | Country country = new Country(locales, 0, 0l, true, "DE", Map.of("DE", "Deutschland")); 48 | CityResponse cityResponse = new CityResponse( 49 | new City(locales, 0, 0l, Map.of("DE", "Hamburg")), 50 | new Continent(locales, "EU", 0l, ImmutableMap.of("DE", "Europa")), 51 | country, 52 | new Location(0, 0, 53.5854, 10.0073, 0, 0, "Europe/Berlin"), 53 | new MaxMind(), 54 | new Postal("22301", 0), 55 | null, 56 | null, 57 | Lists.newArrayList(new Subdivision(locales, 0, 0l, "DE", ImmutableMap.of("DE", "Hamburg"))), 58 | new Traits()); 59 | IspResponse ispResponse = new IspResponse(64512l, "private use range", "192.168.1.1", "local isp", "DE", "DE", "organization", new Network(InetAddress.getByName("192.168.1.1"), 24)); 60 | AsnResponse asnResponse = new AsnResponse(64513l, "private use range", "192.168.1.1", new Network(InetAddress.getByName("192.168.1.1"), 24)); 61 | when(cityDatabaseReader.city(any(InetAddress.class))).thenReturn(cityResponse); 62 | when(asnDatabaseReader.asn(any(InetAddress.class))).thenReturn(asnResponse); 63 | when(ispDatabaseReader.isp(any(InetAddress.class))).thenReturn(ispResponse); 64 | } 65 | 66 | @Test 67 | public void testEmptyResponseIsConvertedCorrectly() throws Exception { 68 | CityResponse emptyCityResponse = new CityResponse(null, null, null, null, null, null, null, null, null, null); 69 | IspResponse emtpyIspResponse = new IspResponse(null, null, null, null, null, null, null, new Network(InetAddress.getByName("192.168.1.1"), 24)); 70 | when(cityDatabaseReader.city(any(InetAddress.class))).thenReturn(emptyCityResponse); 71 | when(ispDatabaseReader.isp(any(InetAddress.class))).thenReturn(emtpyIspResponse); 72 | 73 | Optional result = db.lookup(InetAddresses.forString("192.168.1.1")); 74 | 75 | assertThat(result).isPresent(); 76 | GeoIpEntry geoIpEntry = result.get(); 77 | assertThat(geoIpEntry.getCountry()).isNull(); 78 | assertThat(geoIpEntry.getStateprov()).isNull(); 79 | assertThat(geoIpEntry.getStateprovCode()).isNull(); 80 | assertThat(geoIpEntry.getCity()).isNull(); 81 | assertThat(geoIpEntry.getContinent()).isNull(); 82 | assertThat(geoIpEntry.getLatitude()).isNull(); 83 | assertThat(geoIpEntry.getLongitude()).isNull(); 84 | assertThat(geoIpEntry.getTimezone()).isNull(); 85 | assertThat(geoIpEntry.getIsp()).isNull(); 86 | assertThat(geoIpEntry.getOrganization()).isNull(); 87 | assertThat(geoIpEntry.getAsn()).isNull(); 88 | assertThat(geoIpEntry.getAsnOrganization()).isNull(); 89 | } 90 | 91 | @Test 92 | public void testResponseWithDataIsConvertedCorrectly() { 93 | Optional result = db.lookup(InetAddresses.forString("192.168.1.1")); 94 | assertThat(result).isPresent(); 95 | GeoIpEntry geoIpEntry = result.get(); 96 | assertThat(geoIpEntry.getCountry()).isEqualTo("DE"); 97 | assertThat(geoIpEntry.getStateprov()).isEqualTo("Hamburg"); 98 | assertThat(geoIpEntry.getStateprovCode()).isEqualTo("DE"); 99 | assertThat(geoIpEntry.getCity()).isEqualTo("Hamburg"); 100 | assertThat(geoIpEntry.getContinent()).isEqualTo("EU"); 101 | assertThat(geoIpEntry.getLatitude()).isEqualTo("53.5854"); 102 | assertThat(geoIpEntry.getLongitude()).isEqualTo("10.0073"); 103 | assertThat(geoIpEntry.getTimezone()).isEqualTo("Europe/Berlin"); 104 | assertThat(geoIpEntry.getIsp()).isEqualTo("local isp"); 105 | assertThat(geoIpEntry.getOrganization()).isEqualTo("organization"); 106 | assertThat(geoIpEntry.getAsn()).isEqualTo(64512l); 107 | assertThat(geoIpEntry.getAsnOrganization()).isEqualTo("private use range"); 108 | } 109 | 110 | @Test 111 | public void testDatabaseWithOnlyCityDatabase() { 112 | db = new MaxmindGeolocationDatabase(cityDatabaseReader, null, null); 113 | Optional result= db.lookup(InetAddresses.forString("192.168.1.1")); 114 | assertThat(result).isPresent(); 115 | GeoIpEntry geoIpEntry = result.get(); 116 | assertThat(geoIpEntry.getCountry()).isEqualTo("DE"); 117 | assertThat(geoIpEntry.getStateprov()).isEqualTo("Hamburg"); 118 | assertThat(geoIpEntry.getStateprovCode()).isEqualTo("DE"); 119 | assertThat(geoIpEntry.getCity()).isEqualTo("Hamburg"); 120 | assertThat(geoIpEntry.getContinent()).isEqualTo("EU"); 121 | assertThat(geoIpEntry.getLatitude()).isEqualTo("53.5854"); 122 | assertThat(geoIpEntry.getLongitude()).isEqualTo("10.0073"); 123 | assertThat(geoIpEntry.getTimezone()).isEqualTo("Europe/Berlin"); 124 | assertThat(geoIpEntry.getIsp()).isNull(); 125 | assertThat(geoIpEntry.getOrganization()).isNull(); 126 | assertThat(geoIpEntry.getAsn()).isNull(); 127 | assertThat(geoIpEntry.getAsnOrganization()).isNull(); 128 | } 129 | 130 | @Test 131 | public void testDatabaseWithOnlyIspDatabase() { 132 | db = new MaxmindGeolocationDatabase(null, null, ispDatabaseReader); 133 | Optional result = db.lookup(InetAddresses.forString("192.168.1.1")); 134 | assertThat(result).isPresent(); 135 | GeoIpEntry geoIpEntry = result.get(); 136 | assertThat(geoIpEntry.getCountry()).isNull(); 137 | assertThat(geoIpEntry.getStateprov()).isNull(); 138 | assertThat(geoIpEntry.getCity()).isNull(); 139 | assertThat(geoIpEntry.getContinent()).isNull(); 140 | assertThat(geoIpEntry.getLatitude()).isNull(); 141 | assertThat(geoIpEntry.getLongitude()).isNull(); 142 | assertThat(geoIpEntry.getTimezone()).isNull(); 143 | assertThat(geoIpEntry.getIsp()).isEqualTo("local isp"); 144 | assertThat(geoIpEntry.getOrganization()).isEqualTo("organization"); 145 | assertThat(geoIpEntry.getAsn()).isEqualTo(64512l); 146 | assertThat(geoIpEntry.getAsnOrganization()).isEqualTo("private use range"); 147 | } 148 | 149 | @Test 150 | public void testDatabaseWithOnlyAsnDatabase() { 151 | db = new MaxmindGeolocationDatabase(null, asnDatabaseReader, null); 152 | Optional result = db.lookup(InetAddresses.forString("192.168.1.1")); 153 | assertThat(result).isPresent(); 154 | GeoIpEntry geoIpEntry = result.get(); 155 | assertThat(geoIpEntry.getCountry()).isNull(); 156 | assertThat(geoIpEntry.getStateprov()).isNull(); 157 | assertThat(geoIpEntry.getCity()).isNull(); 158 | assertThat(geoIpEntry.getContinent()).isNull(); 159 | assertThat(geoIpEntry.getLatitude()).isNull(); 160 | assertThat(geoIpEntry.getLongitude()).isNull(); 161 | assertThat(geoIpEntry.getTimezone()).isNull(); 162 | assertThat(geoIpEntry.getIsp()).isNull(); 163 | assertThat(geoIpEntry.getOrganization()).isNull(); 164 | assertThat(geoIpEntry.getAsn()).isEqualTo(64513l); 165 | assertThat(geoIpEntry.getAsnOrganization()).isEqualTo("private use range"); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/java/org/observabilitystack/geoip/RestApiIT.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.AdditionalMatchers.not; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.eq; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.net.InetAddress; 10 | import java.util.Map; 11 | 12 | import com.google.common.net.InetAddresses; 13 | import com.maxmind.geoip2.DatabaseReader; 14 | import com.maxmind.geoip2.exception.AddressNotFoundException; 15 | import com.maxmind.geoip2.model.CityResponse; 16 | import com.maxmind.geoip2.record.Country; 17 | 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.observabilitystack.geoip.GeoIpApi; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.boot.test.context.SpringBootTest; 24 | import org.springframework.boot.test.mock.mockito.MockBean; 25 | import org.springframework.boot.test.web.client.TestRestTemplate; 26 | import org.springframework.http.HttpStatus; 27 | import org.springframework.http.MediaType; 28 | import org.springframework.http.ResponseEntity; 29 | import org.springframework.test.context.ActiveProfiles; 30 | import org.springframework.test.context.junit.jupiter.SpringExtension; 31 | 32 | @ExtendWith(SpringExtension.class) 33 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = GeoIpApi.class) 34 | @ActiveProfiles("test") 35 | public class RestApiIT { 36 | 37 | private static final String REST_URL = "/{address}"; 38 | 39 | @Autowired 40 | private TestRestTemplate restTemplate; 41 | 42 | @MockBean(name = "cityDatabaseReader") 43 | private DatabaseReader cityDatabaseReader; 44 | 45 | @MockBean(name = "ispDatabaseReader") 46 | private DatabaseReader ispDatabaseReader; 47 | 48 | @BeforeEach 49 | public void setUp() throws Exception { 50 | when(cityDatabaseReader.city(eq(InetAddresses.forString("192.0.2.1")))).thenReturn( 51 | new CityResponse(null, null, 52 | new Country(), 53 | null, null, null, null, null, null, null)); 54 | when(cityDatabaseReader.city(not(eq(InetAddresses.forString("192.0.2.1"))))) 55 | .thenThrow(new AddressNotFoundException("test")); 56 | when(ispDatabaseReader.isp(any(InetAddress.class))).thenThrow(new AddressNotFoundException("test")); 57 | } 58 | 59 | @Test 60 | public void testRestApiRunningInContainer() { 61 | ResponseEntity response = restTemplate.getForEntity(REST_URL, Map.class, "192.0.2.1"); 62 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 63 | assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); 64 | assertThat(response.getBody().get("country")).isNull(); 65 | } 66 | 67 | @Test 68 | public void test404ResponseForIpAddressWithoutEntry() { 69 | ResponseEntity response = restTemplate.getForEntity(REST_URL, Map.class, "192.0.2.2"); 70 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 71 | } 72 | 73 | @Test 74 | public void test400ResponseForInvalidInput() { 75 | ResponseEntity response = restTemplate.getForEntity(REST_URL, String.class, "invalid"); 76 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/org/observabilitystack/geoip/SpringBootActuatorIT.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.net.InetAddress; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.boot.test.mock.mockito.MockBean; 16 | import org.springframework.boot.test.web.client.TestRestTemplate; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.test.context.ActiveProfiles; 20 | import org.springframework.test.context.junit.jupiter.SpringExtension; 21 | 22 | import com.maxmind.geoip2.DatabaseReader; 23 | import com.maxmind.geoip2.exception.AddressNotFoundException; 24 | 25 | @AutoConfigureObservability 26 | @ExtendWith(SpringExtension.class) 27 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = GeoIpApi.class) 28 | @ActiveProfiles("test") 29 | public class SpringBootActuatorIT { 30 | 31 | @Autowired 32 | private TestRestTemplate restTemplate; 33 | 34 | @MockBean(name = "ispDatabaseReader") 35 | private DatabaseReader ispDatabaseReader; 36 | 37 | @BeforeEach 38 | public void setUp() throws Exception { 39 | when(ispDatabaseReader.isp(any(InetAddress.class))).thenThrow(new AddressNotFoundException("test")); 40 | } 41 | 42 | @Test 43 | public void testActuator() { 44 | ResponseEntity response = restTemplate.getForEntity("/actuator", String.class, "invalid"); 45 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 46 | } 47 | 48 | @Test 49 | public void testActuatorHealth() { 50 | ResponseEntity response = restTemplate.getForEntity("/actuator/health", String.class, "invalid"); 51 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 52 | } 53 | 54 | @Test 55 | public void testActuatorPrometheus() { 56 | ResponseEntity response = restTemplate.getForEntity("/actuator/prometheus", String.class, "invalid"); 57 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/observabilitystack/geoip/web/GeoIpRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.observabilitystack.geoip.web; 2 | 3 | import static org.mockito.ArgumentMatchers.eq; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 11 | 12 | import java.net.InetAddress; 13 | import java.util.Optional; 14 | 15 | import com.google.common.net.InetAddresses; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.observabilitystack.geoip.GeoIpEntry; 20 | import org.observabilitystack.geoip.GeolocationProvider; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 24 | 25 | public class GeoIpRestControllerTest { 26 | 27 | private static final InetAddress IPV4_ADDR = InetAddresses.forString("192.168.1.1"); 28 | private static final InetAddress IPV4_ADDR2 = InetAddresses.forString("172.16.0.1"); 29 | private static final InetAddress IPV4_ADDR3 = InetAddresses.forString("10.0.0.1"); 30 | private static final InetAddress IPV6_ADDR = InetAddresses.forString("2001:db8:1::1"); 31 | 32 | private MockMvc mockMvc; 33 | private GeolocationProvider provider; 34 | private GeoIpRestController restController; 35 | 36 | @BeforeEach 37 | public void setUp() { 38 | provider = mock(GeolocationProvider.class); 39 | when(provider.lookup(eq(IPV4_ADDR))).thenReturn(Optional.of(GeoIpEntry.builder().setCountry("ZZ").build())); 40 | when(provider.lookup(eq(IPV4_ADDR2))).thenReturn(Optional.of(GeoIpEntry.builder().setCountry("ZZ").build())); 41 | when(provider.lookup(eq(IPV4_ADDR3))).thenReturn(Optional.of(GeoIpEntry.builder().setCountry("ZZ").build())); 42 | when(provider.lookup(eq(IPV6_ADDR))).thenReturn(Optional.of(GeoIpEntry.builder().setCountry("ZZ").build())); 43 | restController = new GeoIpRestController(provider); 44 | mockMvc = MockMvcBuilders.standaloneSetup(restController).build(); 45 | } 46 | 47 | @Test 48 | public void testIpAddressNotFound() throws Exception { 49 | mockMvc.perform(get("/192.168.42.1").accept(MediaType.APPLICATION_JSON)) 50 | .andExpect(status().isNotFound()); 51 | } 52 | 53 | @Test 54 | public void testKnownNotFounds() throws Exception { 55 | mockMvc.perform(get("/")).andExpect(status().isNotFound()); 56 | mockMvc.perform(get("/robots.txt")).andExpect(status().isNotFound()); 57 | mockMvc.perform(get("/favicon.ico")).andExpect(status().isNotFound()); 58 | } 59 | 60 | @Test 61 | public void testIpv4Address() throws Exception { 62 | mockMvc.perform(get("/192.168.1.1").accept(MediaType.APPLICATION_JSON)) 63 | .andExpect(status().isOk()) 64 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) 65 | .andExpect(content().json("{\"country\":\"ZZ\"}")); 66 | } 67 | 68 | @Test 69 | public void testMultiIpv4Addresses() throws Exception { 70 | mockMvc.perform(post("/").contentType(MediaType.APPLICATION_JSON).content( 71 | "[\"" + 72 | IPV4_ADDR.getHostAddress() + 73 | "\", \"" + 74 | IPV4_ADDR2.getHostAddress() + 75 | "\", \"" + 76 | IPV4_ADDR3.getHostAddress() + 77 | "\"]" 78 | )) 79 | .andExpect(status().isOk()) 80 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 81 | .andExpect(jsonPath("$[\"" + IPV4_ADDR.getHostAddress() + "\"].country").value("ZZ")) 82 | .andExpect(jsonPath("$[\"" + IPV4_ADDR2.getHostAddress() + "\"].country").value("ZZ")) 83 | .andExpect(jsonPath("$[\"" + IPV4_ADDR3.getHostAddress() + "\"].country").value("ZZ")); 84 | } 85 | 86 | @Test 87 | public void testMultiIpv4AddressesExceedingLimit() throws Exception { 88 | InetAddress[] ipAddresses = new InetAddress[101]; 89 | for (int i = 0; i < 101; i++) { 90 | ipAddresses[i] = InetAddress.getByName("192.168.1." + i); 91 | } 92 | 93 | String jsonContent = "["; 94 | for (InetAddress address : ipAddresses) { 95 | jsonContent += "\"" + address.getHostAddress() + "\","; 96 | } 97 | jsonContent = jsonContent.substring(0, jsonContent.length() - 1) + "]"; 98 | 99 | mockMvc.perform(post("/") 100 | .contentType(MediaType.APPLICATION_JSON) 101 | .content(jsonContent)) 102 | .andExpect(status().isBadRequest()) 103 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 104 | .andExpect(content().string("Only 100 address requests allowed at once")); 105 | } 106 | 107 | @Test 108 | public void testIpv6Address() throws Exception { 109 | mockMvc.perform(get("/2001:db8:1::1").accept(MediaType.APPLICATION_JSON)) 110 | .andExpect(status().isOk()) 111 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) 112 | .andExpect(content().json("{\"country\":\"ZZ\"}")); 113 | } 114 | 115 | @Test 116 | public void testInvalidIpAddresses() throws Exception { 117 | mockMvc.perform(get("/1.2.3.4.5")).andExpect(status().isBadRequest()); 118 | mockMvc.perform(get("/example.com")).andExpect(status().isBadRequest()); 119 | mockMvc.perform(get("/something")).andExpect(status().isBadRequest()); 120 | } 121 | } 122 | --------------------------------------------------------------------------------