pax_global_header00006660000000000000000000000064152110311600014500gustar00rootroot0000000000000052 comment=08468441546b97b04d73d5351c3ad931ba62ada6 golang-github-oschwald-geoip2-golang-v2-2.2.0/000077500000000000000000000000001521103116000210075ustar00rootroot00000000000000golang-github-oschwald-geoip2-golang-v2-2.2.0/.githooks/000077500000000000000000000000001521103116000227145ustar00rootroot00000000000000golang-github-oschwald-geoip2-golang-v2-2.2.0/.githooks/pre-commit000077500000000000000000000006761521103116000247270ustar00rootroot00000000000000#!/bin/bash # # Git pre-commit hook that runs precious lint on staged files # set -e echo "Running precious lint on staged files..." # Run precious lint on staged files if ! precious lint --staged; then echo "" echo "❌ Linting failed. Please fix the issues above before committing." echo "" echo "To fix formatting issues automatically, run:" echo " precious tidy --staged" exit 1 fi echo "✅ All linters passed!" golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/000077500000000000000000000000001521103116000223475ustar00rootroot00000000000000golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/dependabot.yml000066400000000000000000000003741521103116000252030ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/workflows/000077500000000000000000000000001521103116000244045ustar00rootroot00000000000000golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/workflows/codeql-analysis.yml000066400000000000000000000032111521103116000302140ustar00rootroot00000000000000name: "Code scanning - action" on: push: branches-ignore: - "dependabot/**" pull_request: schedule: - cron: "0 12 * * 5" jobs: CodeQL-Build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/workflows/go.yml000066400000000000000000000012561521103116000255400ustar00rootroot00000000000000name: Go on: [push, pull_request] jobs: build: name: Build strategy: matrix: go-version: [1.26.x, 1.25.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Set up Go 1.x uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v6 with: submodules: true - name: Get dependencies run: go get -v -t -d ./... - name: Build run: go build -v . - name: Test run: go test -race -v ./... golang-github-oschwald-geoip2-golang-v2-2.2.0/.github/workflows/golangci-lint.yml000066400000000000000000000004131521103116000276540ustar00rootroot00000000000000name: golangci-lint on: [push, pull_request] jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: version: latest golang-github-oschwald-geoip2-golang-v2-2.2.0/.gitignore000066400000000000000000000000721521103116000227760ustar00rootroot00000000000000.vscode *.out *.test *.mmdb *.prof *.pprof *.svg *.tar.gz golang-github-oschwald-geoip2-golang-v2-2.2.0/.gitmodules000066400000000000000000000001331521103116000231610ustar00rootroot00000000000000[submodule "test-data"] path = test-data url = https://github.com/maxmind/MaxMind-DB.git golang-github-oschwald-geoip2-golang-v2-2.2.0/.golangci.yml000066400000000000000000000100731521103116000233740ustar00rootroot00000000000000version: "2" run: go: "1.25" tests: true allow-parallel-runners: true linters: default: all disable: - cyclop - depguard - err113 - exhaustive - exhaustruct - forcetypeassert - funlen - gochecknoglobals - gocognit - godox - gosmopolitan - inamedparam - interfacebloat # Seems unstable. It will sometimes fire and other times not. - ireturn - lll - mnd - nlreturn - noinlineerr - nonamedreturns - paralleltest - testpackage - thelper - varnamelen - wrapcheck - wsl - wsl_v5 settings: errcheck: exclude-functions: - (*github.com/oschwald/geoip2-golang/v2.Reader).Close errorlint: errorf: true asserts: true comparison: true exhaustive: default-signifies-exhaustive: true goconst: ignore-tests: true forbidigo: forbid: - pattern: Geoip msg: you should use `GeoIP` - pattern: geoIP msg: you should use `geoip` - pattern: Maxmind msg: you should use `MaxMind` - pattern: ^maxMind msg: you should use `maxmind` - pattern: Minfraud msg: you should use `MinFraud` - pattern: ^minFraud msg: you should use `minfraud` - pattern: ^math.Max$ msg: you should use the max built-in instead. - pattern: ^math.Min$ msg: you should use the min built-in instead. - pattern: ^os.IsNotExist msg: As per their docs, new code should use errors.Is(err, fs.ErrNotExist). - pattern: ^os.IsExist msg: As per their docs, new code should use errors.Is(err, fs.ErrExist) gosec: excludes: - G115 # Potential file inclusion via variable - we only open files asked by # the user of the API. - G304 govet: disable: - shadow enable-all: true lll: line-length: 120 tab-width: 4 misspell: locale: US extra-words: - typo: marshall correction: marshal - typo: marshalling correction: marshaling - typo: marshalls correction: marshals - typo: unmarshall correction: unmarshal - typo: unmarshalling correction: unmarshaling - typo: unmarshalls correction: unmarshals nolintlint: require-explanation: true require-specific: true allow-no-explanation: - lll - misspell allow-unused: false revive: severity: warning enable-all-rules: true rules: - name: add-constant disabled: true - name: cognitive-complexity disabled: true - name: confusing-naming disabled: true - name: confusing-results disabled: true - name: cyclomatic disabled: true - name: deep-exit disabled: true - name: flag-parameter disabled: true - name: function-length disabled: true - name: function-result-limit disabled: true - name: line-length-limit disabled: true - name: max-public-structs disabled: true - name: nested-structs disabled: true - name: unchecked-type-assertion disabled: true - name: unhandled-error disabled: true - name: package-directory-mismatch arguments: - ignore-directories: - geoip2-golang tagliatelle: case: rules: avro: snake bson: snake env: upperSnake envconfig: upperSnake json: snake mapstructure: snake xml: snake yaml: snake unparam: check-exported: true exclusions: warn-unused: true formatters: enable: - gci - gofmt - gofumpt - goimports - golines settings: gci: sections: - standard - default - prefix(github.com/oschwald/maxminddb-golang) gofumpt: extra-rules: true golang-github-oschwald-geoip2-golang-v2-2.2.0/.precious.toml000066400000000000000000000014671521103116000236230ustar00rootroot00000000000000exclude = [ "test-data/**/*", "vendor/**/*", "GeoLite2-ASN.mmdb", "GeoLite2-City.mmdb", "*.prof", "*.pprof", "*.out", "*.svg", "*.tar.gz", "*.test", ] [commands.golangci-lint-fmt] type = "both" include = "**/*.go" invoke = "once" path-args = "none" cmd = ["golangci-lint", "fmt"] lint-flags = "--diff" ok-exit-codes = [0] lint-failure-exit-codes = [1] [commands.golangci-lint] type = "both" include = "**/*.go" invoke = "once" path-args = "none" cmd = ["golangci-lint", "run"] tidy-flags = "--fix" ok-exit-codes = [0] lint-failure-exit-codes = [1] [commands.prettier] type = "both" include = ["**/*.md", "**/*.yml", "**/*.yaml"] invoke = "once" path-args = "file" cmd = ["prettier"] lint-flags = "--check" tidy-flags = "--write" ok-exit-codes = [0] lint-failure-exit-codes = [1] expect-stderr = true golang-github-oschwald-geoip2-golang-v2-2.2.0/CHANGELOG.md000066400000000000000000000252501521103116000226240ustar00rootroot00000000000000# Changes ## 2.2.0 - 2026-05-17 - This module now targets Go 1.25+. - Updated dependencies to latest version. In particular `github.com/oschwald/maxminddb-golang/v2` was updated to `v2.3.0`, which includes several performance improvements. ## 2.1.0 - 2025-12-22 - Added support for the GeoIP Anonymous Plus database. This database provides VPN detection with confidence scoring, provider identification, and temporal tracking via the new `AnonymousPlus()` method. - Deprecated `IsLegitimateProxy` on `EnterpriseTraits`. MaxMind has deprecated this field and it will be removed in the next major release. - Deprecated `StaticIPScore` on `EnterpriseTraits`. This field was added in error and has never been populated. It will be removed in the next major release. ## 2.0.1 - 2025-11-26 - Upgraded `github.com/oschwald/geoip2-golang/v2` to 2.1.1, which fixes an issue that prevented a unclosed memory-mapped file from being unmapped when the reader was garbage collected. ## 2.0.0 - 2025-10-19 - **BREAKING CHANGE**: Lookup methods now require `netip.Addr`, return typed `Names`, and provide `HasData()` helpers while always populating `Network`/`IPAddress` fields so network topology remains accessible. - **BREAKING CHANGE**: Struct field casing now matches MaxMind responses (for example `IsoCode` → `ISOCode`), location coordinates use pointers, and JSON tags rely on Go 1.24 `omitzero` support—upgrade your toolchain before adopting v2. - Added `MIGRATION.md` with detailed guidance for upgrading from v1. - Updated dependency on `github.com/oschwald/maxminddb-golang/v2` to `v2.0.0`. - Added configurable `Option` helpers so `Open` and `OpenBytes` can accept future options without forcing a v3 release. - **BREAKING CHANGE**: Removed deprecated `FromBytes` method. Use `OpenBytes` instead. ## 2.0.0-beta.4 - 2025-08-23 - Updated maxminddb dependency to v2.0.0-beta.9. - Added `OpenBytes` method to match the API changes in maxminddb v2.0.0-beta.9. - Deprecated `FromBytes` method. Use `OpenBytes` instead. `FromBytes` will be removed in a future version. ## 2.0.0-beta.3 - 2025-07-07 - Added support for `GeoIP-City-Redacted-US` and `GeoIP-Enterprise-Redacted-US`. Requested by Tom Anderson. GitHub #134. - Upgrade `github.com/oschwald/maxminddb-golang/v2` to `v2.0.0-beta.7`. ## 2.0.0-beta.2 - 2025-06-28 - **BREAKING CHANGE**: Replaced `IsZero()` methods with `HasData()` methods on all result structs (including Names). The new methods provide clearer semantics: `HasData()` returns `true` when GeoIP data is found and `false` when no data is available. Unlike `IsZero()`, `HasData()` excludes Network and IPAddress fields from validation, allowing users to access network topology information even when no GeoIP data is found. The Network and IPAddress fields are now always populated for all lookups, regardless of whether GeoIP data is available. - **BREAKING CHANGE**: Replaced all anonymous nested structs with named types to improve struct initialization ergonomics. All result structs (Enterprise, City, Country) now use named types like `EnterpriseCityRecord`, `CityTraits`, `CountryRecord`, etc. This makes it much easier to initialize structs in user code while maintaining the same JSON serialization behavior. - **BREAKING CHANGE**: Changed `Location.Latitude` and `Location.Longitude` from `float64` to `*float64` to properly distinguish between missing coordinates and the valid location (0, 0). Missing coordinates are now represented as `nil` and are omitted from JSON output, while valid zero coordinates are preserved. This fixes the ambiguity where (0, 0) was incorrectly treated as "no data". Added `Location.HasCoordinates()` method for safe coordinate access. Reported by Nick Bruun. GitHub #5. ## 2.0.0-beta.1 - 2025-06-22 - **BREAKING CHANGE**: Updated to use `maxminddb-golang/v2` which provides significant performance improvements and a more modern API. - **BREAKING CHANGE**: All lookup methods now accept `netip.Addr` instead of `net.IP`. This provides better performance and aligns with modern Go networking practices. - **BREAKING CHANGE**: Renamed `IsoCode` fields to `ISOCode` in all structs to follow proper capitalization for the ISO acronym. Closes GitHub issue #4. - **BREAKING CHANGE**: Replaced `map[string]string` Names fields with structured `Names` type for significant performance improvements. This eliminates map allocation overhead, reducing memory usage by 34% and allocations by 56%. - **BREAKING CHANGE**: Added JSON tags to all struct fields. JSON tags match the corresponding `maxminddb` tags where they exist. Custom fields (`IPAddress` and `Network`) use snake_case (`ip_address` and `network`). - **BREAKING CHANGE**: Removed `IsAnonymousProxy` and `IsSatelliteProvider` fields from all Traits structs. These fields have been removed from MaxMind databases. Use the dedicated Anonymous IP database for anonymity detection instead. - **BREAKING CHANGE**: Go 1.24 or greater is now required. This enables the use of `omitzero` in JSON tags to match MaxMind database behavior where empty values are not included. - Added `IsZero()` method to all result structs (City, Country, Enterprise, ASN, etc.) to easily check whether any data was found for the queried IP address. Requested by Salim Alami. GitHub [#32](https://github.com/oschwald/geoip2-golang/issues/32). - Added `Network` and `IPAddress` fields to all result structs. The `Network` field exposes the network prefix from the MaxMind database lookup, and the `IPAddress` field contains the IP address used during the lookup. These fields are only populated when data is found for the IP address. For flat record types (ASN, ConnectionType, Domain, ISP, AnonymousIP), the fields are named `Network` and `IPAddress`. For complex types (City, Country, Enterprise), the fields are located at `.Traits.Network` and `.Traits.IPAddress`. Requested by Aaron Bishop. GitHub [#128](https://github.com/oschwald/geoip2-golang/issues/128). - Updated module path to `github.com/oschwald/geoip2-golang/v2` to follow Go's semantic versioning guidelines for breaking changes. - Updated examples and documentation to demonstrate proper error handling with `netip.ParseAddr()`. - Updated linting rules to support both v1 and v2 import paths during the transition period. ### Migration Guide To migrate from v1 to v2: 1. Update your import path: ```go // Old import "github.com/oschwald/geoip2-golang" // New import "github.com/oschwald/geoip2-golang/v2" ``` 2. Replace `net.IP` with `netip.Addr`: ```go // Old ip := net.ParseIP("81.2.69.142") record, err := db.City(ip) // New ip, err := netip.ParseAddr("81.2.69.142") if err != nil { // handle error } record, err := db.City(ip) ``` 3. Update field names from `IsoCode` to `ISOCode`: ```go // Old countryCode := record.Country.IsoCode subdivisionCode := record.Subdivisions[0].IsoCode // New countryCode := record.Country.ISOCode subdivisionCode := record.Subdivisions[0].ISOCode ``` 4. Replace map-based Names access with struct fields: ```go // Old cityName := record.City.Names["en"] countryName := record.Country.Names["pt-BR"] continentName := record.Continent.Names["zh-CN"] // New cityName := record.City.Names.English countryName := record.Country.Names.BrazilianPortuguese continentName := record.Continent.Names.SimplifiedChinese ``` Available Names struct fields: - `English` (en) - `German` (de) - `Spanish` (es) - `French` (fr) - `Japanese` (ja) - `BrazilianPortuguese` (pt-BR) - `Russian` (ru) - `SimplifiedChinese` (zh-CN) 5. Check if data was found using the new `IsZero()` method: ```go record, err := db.City(ip) if err != nil { // handle error } if record.IsZero() { fmt.Println("No data found for this IP") } else { fmt.Printf("City: %s\n", record.City.Names.English) } ``` ## 1.11.0 - 2024-06-03 - Go 1.21 or greater is now required. - The new `is_anycast` output is now supported on the GeoIP2 Country, City, and Enterprise databases. [#119](https://github.com/oschwald/geoip2-golang/issues/119). Note: 1.10.0 was accidentally skipped. ## 1.9.0 - 2023-06-18 - Rearrange fields in structs to reduce memory usage. Although this does reduce readability, these structs are often created at very rates, making the trade-off worth it. ## 1.8.0 - 2022-08-07 - Set Go version to 1.18 in go.mod. ## 1.7.0 - 2022-03-26 - Set the minimum Go version in the go.mod file to 1.17. - Updated dependencies. ## 1.6.1 - 2022-01-28 - This is a re-release with the changes that were supposed to be in 1.6.0. ## 1.6.0 - 2022-01-28 - Add support for new `mobile_country_code` and `mobile_network_code` outputs on GeoIP2 ISP and GeoIP2 Enterprise. ## 1.5.0 - 2021-02-20 - Add `StaticIPScore` field to Enterprise. Pull request by Pierre Bonzel. GitHub [#54](https://github.com/oschwald/geoip2-golang/issues/54). - Add `IsResidentialProxy` field to `AnonymousIP`. Pull request by Brendan Boyle. GitHub [#72](https://github.com/oschwald/geoip2-golang/issues/72). - Support DBIP-ASN-Lite database. Requested by Muhammad Hussein Fattahizadeh. GitHub [#69](https://github.com/oschwald/geoip2-golang/issues/69). ## 1.4.0 - 2019-12-25 - This module now uses Go modules. Requested by Axel Etcheverry. GitHub [#52](https://github.com/oschwald/geoip2-golang/issues/52). - DBIP databases are now supported. Requested by jaw0. GitHub [#45](https://github.com/oschwald/geoip2-golang/issues/45). - Allow using the ASN method with the GeoIP2 ISP database. Pull request by lspgn. GitHub [#47](https://github.com/oschwald/geoip2-golang/issues/47). - The example in the `README.md` now checks the length of the subdivision slice before using it. GitHub [#51](https://github.com/oschwald/geoip2-golang/issues/51). ## 1.3.0 - 2019-08-28 - Added support for the GeoIP2 Enterprise database. ## 1.2.1 - 2018-02-25 - HTTPS is now used for the test data submodule rather than the Git protocol ## 1.2.0 - 2018-02-19 - The country structs for `geoip2.City` and `geoip2.Country` now have an `IsInEuropeanUnion` boolean field. This is true when the associated country is a member state of the European Union. This requires a database built on or after February 13, 2018. - Switch from Go Check to Testify. Closes [#27](https://github.com/oschwald/geoip2-golang/issues/27) ## 1.1.0 - 2017-04-23 - Add support for the GeoLite2 ASN database. - Add support for the GeoIP2 City by Continent databases. GitHub [#26](https://github.com/oschwald/geoip2-golang/issues/26). ## 1.0.0 - 2016-11-09 New release for those using tagged releases. Closes [#21](https://github.com/oschwald/geoip2-golang/issues/21). golang-github-oschwald-geoip2-golang-v2-2.2.0/LICENSE000066400000000000000000000014041521103116000220130ustar00rootroot00000000000000ISC License Copyright (c) 2015, Gregory J. Oschwald Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. golang-github-oschwald-geoip2-golang-v2-2.2.0/MIGRATION.md000066400000000000000000000053541521103116000226710ustar00rootroot00000000000000# Migrating to geoip2-golang v2 Version 2.0 modernizes the public API, improves performance, and tightens parity with current MaxMind databases. This guide highlights the most important changes and shows how to update existing v1 code. ## Upgrade Checklist 1. **Update imports** ```go // v1 import "github.com/oschwald/geoip2-golang" // v2 import "github.com/oschwald/geoip2-golang/v2" ``` 2. **Switch to `netip.Addr`** Lookup methods now take `netip.Addr` instead of `net.IP`. Use `netip.ParseAddr` (errors on invalid input) or `netip.MustParseAddr` for trusted literals. This avoids ambiguity around IPv4-in-IPv6 addresses and removes heap allocations for most lookups. ```go // v1 ip := net.ParseIP("81.2.69.142") record, err := db.City(ip) // v2 ip, err := netip.ParseAddr("81.2.69.142") if err != nil { return err } record, err := db.City(ip) ``` 3. **Adopt the new `HasData()` helpers** All result structs have a `HasData()` method that ignores the always-filled `Network` and `IPAddress` fields. Use it instead of comparing against zero values. ```go record, err := db.City(ip) if err != nil { return err } if !record.HasData() { // handle missing GeoIP data } ``` 4. **Use structured `Names` fields** Localized names are now strongly typed instead of map lookups. Access the language-specific struct field directly. ```go // v1 cityEn := record.City.Names["en"] cityPtBr := record.City.Names["pt-BR"] // v2 cityEn := record.City.Names.English cityPtBr := record.City.Names.BrazilianPortuguese ``` Supported fields include `English`, `German`, `Spanish`, `French`, `Japanese`, `BrazilianPortuguese`, `Russian`, and `SimplifiedChinese`. 5. **Expect `Network` and `IPAddress` on every result** Each lookup populates the queried IP address and the enclosing network on every result struct, even if `HasData()` returns false. Existing code that depended on empty `Network` for “no data” should switch to `HasData()`. 6. **Adjust renamed fields** `IsoCode` is now `ISOCode` across all structs. Update code accordingly. ```go // v1 code := record.Country.IsoCode // v2 code := record.Country.ISOCode ``` ## Additional Notes - `Open` and `OpenBytes` accept optional `Option` values, allowing future extensions without new breaking releases. - The legacy `FromBytes` helper is removed. Use `OpenBytes` instead. - JSON output mirrors MaxMind database naming thanks to the new `omitzero` tags and typed structs. Following the checklist above should cover most migrations. For edge cases, review `models.go` for field definitions and `example_test.go` for idiomatic usage with the v2 API. golang-github-oschwald-geoip2-golang-v2-2.2.0/README.md000066400000000000000000000410531521103116000222710ustar00rootroot00000000000000# GeoIP2 Reader for Go [![PkgGoDev](https://pkg.go.dev/badge/github.com/oschwald/geoip2-golang/v2)](https://pkg.go.dev/github.com/oschwald/geoip2-golang/v2) This library reads MaxMind [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) and [GeoIP2](https://www.maxmind.com/en/geolocation_landing) databases. This library is built using [the Go maxminddb reader](https://github.com/oschwald/maxminddb-golang). All data for the database record is decoded using this library. Version 2.0 provides significant performance improvements with 56% fewer allocations and 34% less memory usage compared to v1. Version 2.0 also adds `Network` and `IPAddress` fields to all result structs, and includes a `HasData()` method to easily check if data was found. If you only need several fields, you may get superior performance by using maxminddb's `Lookup` directly with a result struct that only contains the required fields. (See [example_test.go](https://github.com/oschwald/maxminddb-golang/blob/v2/example_test.go) in the maxminddb repository for an example of this.) ## Installation ``` go get github.com/oschwald/geoip2-golang/v2 ``` ## New in v2 Version 2.0 includes several major improvements: - **Performance**: 56% fewer allocations and 34% less memory usage - **Modern API**: Uses `netip.Addr` instead of `net.IP` for better performance - **Network Information**: All result structs now include `Network` and `IPAddress` fields - **Data Validation**: New `HasData()` method to easily check if data was found - **Structured Names**: Replaced `map[string]string` with typed `Names` struct for better performance ## Migration See [MIGRATION.md](MIGRATION.md) for step-by-step guidance on upgrading from v1. ## Usage [See GoDoc](https://pkg.go.dev/github.com/oschwald/geoip2-golang/v2) for documentation and examples. ## Example ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-City.mmdb") if err != nil { log.Fatal(err) } defer db.Close() // If you are using strings that may be invalid, use netip.ParseAddr and check for errors ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Fatal(err) } record, err := db.City(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Portuguese (BR) city name: %v\n", record.City.Names.BrazilianPortuguese) if len(record.Subdivisions) > 0 { fmt.Printf("English subdivision name: %v\n", record.Subdivisions[0].Names.English) } fmt.Printf("Russian country name: %v\n", record.Country.Names.Russian) fmt.Printf("ISO country code: %v\n", record.Country.ISOCode) fmt.Printf("Time zone: %v\n", record.Location.TimeZone) if record.Location.HasCoordinates() { fmt.Printf("Coordinates: %v, %v\n", *record.Location.Latitude, *record.Location.Longitude) } // Output: // Portuguese (BR) city name: Londres // English subdivision name: England // Russian country name: Великобритания // ISO country code: GB // Time zone: Europe/London // Coordinates: 51.5142, -0.0931 } ``` ## Requirements - Go 1.25 or later - MaxMind GeoIP2 or GeoLite2 database files (.mmdb format) ## Getting Database Files ### GeoLite2 (Free) Download free GeoLite2 databases from [MaxMind's website](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). Registration required. ### GeoIP2 (Commercial) Purchase GeoIP2 databases from [MaxMind](https://www.maxmind.com/en/geoip2-databases) for enhanced accuracy and additional features. ## Database Examples This library supports all MaxMind GeoIP2 and GeoLite2 database types. Below are examples for each database type: ### City Database The City database provides the most comprehensive geolocation data, including city, subdivision, country, and precise location information. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-City.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("128.101.101.101") if err != nil { log.Fatal(err) } record, err := db.City(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("City: %v\n", record.City.Names.English) fmt.Printf("Subdivision: %v\n", record.Subdivisions[0].Names.English) fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) fmt.Printf("Continent: %v (%v)\n", record.Continent.Names.English, record.Continent.Code) fmt.Printf("Postal Code: %v\n", record.Postal.Code) if record.Location.HasCoordinates() { fmt.Printf("Location: %v, %v\n", *record.Location.Latitude, *record.Location.Longitude) } fmt.Printf("Time Zone: %v\n", record.Location.TimeZone) fmt.Printf("Network: %v\n", record.Traits.Network) fmt.Printf("IP Address: %v\n", record.Traits.IPAddress) } ``` ### Country Database The Country database provides country-level geolocation data. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-Country.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Fatal(err) } record, err := db.Country(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) fmt.Printf("Continent: %v (%v)\n", record.Continent.Names.English, record.Continent.Code) fmt.Printf("Is in EU: %v\n", record.Country.IsInEuropeanUnion) fmt.Printf("Network: %v\n", record.Traits.Network) fmt.Printf("IP Address: %v\n", record.Traits.IPAddress) if record.RegisteredCountry.Names.English != "" { fmt.Printf("Registered Country: %v (%v)\n", record.RegisteredCountry.Names.English, record.RegisteredCountry.ISOCode) } } ``` ### ASN Database The ASN database provides Autonomous System Number and organization information. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoLite2-ASN.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("1.128.0.0") if err != nil { log.Fatal(err) } record, err := db.ASN(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("ASN: %v\n", record.AutonomousSystemNumber) fmt.Printf("Organization: %v\n", record.AutonomousSystemOrganization) fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Anonymous IP Database The Anonymous IP database identifies various types of anonymous and proxy networks. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-Anonymous-IP.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Fatal(err) } record, err := db.AnonymousIP(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Is Anonymous: %v\n", record.IsAnonymous) fmt.Printf("Is Anonymous VPN: %v\n", record.IsAnonymousVPN) fmt.Printf("Is Hosting Provider: %v\n", record.IsHostingProvider) fmt.Printf("Is Public Proxy: %v\n", record.IsPublicProxy) fmt.Printf("Is Residential Proxy: %v\n", record.IsResidentialProxy) fmt.Printf("Is Tor Exit Node: %v\n", record.IsTorExitNode) fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Anonymous Plus Database The Anonymous Plus database extends the Anonymous IP database with additional fields for confidence scoring, provider identification, and temporal tracking. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP-Anonymous-Plus.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("1.2.0.1") if err != nil { log.Fatal(err) } record, err := db.AnonymousPlus(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } // Standard anonymous IP flags fmt.Printf("Is Anonymous: %v\n", record.IsAnonymous) fmt.Printf("Is Anonymous VPN: %v\n", record.IsAnonymousVPN) fmt.Printf("Is Hosting Provider: %v\n", record.IsHostingProvider) fmt.Printf("Is Public Proxy: %v\n", record.IsPublicProxy) fmt.Printf("Is Residential Proxy: %v\n", record.IsResidentialProxy) fmt.Printf("Is Tor Exit Node: %v\n", record.IsTorExitNode) // Anonymous Plus specific fields fmt.Printf("Anonymizer Confidence: %v\n", record.AnonymizerConfidence) fmt.Printf("Provider Name: %v\n", record.ProviderName) if !record.NetworkLastSeen.IsZero() { fmt.Printf("Network Last Seen: %v\n", record.NetworkLastSeen.Format("2006-01-02")) } fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Enterprise Database The Enterprise database provides the most comprehensive data, including all City database fields plus additional enterprise features. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-Enterprise.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("128.101.101.101") if err != nil { log.Fatal(err) } record, err := db.Enterprise(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } // Basic location information fmt.Printf("City: %v\n", record.City.Names.English) fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) if record.Location.HasCoordinates() { fmt.Printf("Location: %v, %v\n", *record.Location.Latitude, *record.Location.Longitude) } // Enterprise-specific fields fmt.Printf("ISP: %v\n", record.Traits.ISP) fmt.Printf("Organization: %v\n", record.Traits.Organization) fmt.Printf("ASN: %v (%v)\n", record.Traits.AutonomousSystemNumber, record.Traits.AutonomousSystemOrganization) fmt.Printf("Connection Type: %v\n", record.Traits.ConnectionType) fmt.Printf("Domain: %v\n", record.Traits.Domain) fmt.Printf("User Type: %v\n", record.Traits.UserType) fmt.Printf("Is Anycast: %v\n", record.Traits.IsAnycast) // Mobile carrier information (if available) if record.Traits.MobileCountryCode != "" { fmt.Printf("Mobile Country Code: %v\n", record.Traits.MobileCountryCode) fmt.Printf("Mobile Network Code: %v\n", record.Traits.MobileNetworkCode) } fmt.Printf("Network: %v\n", record.Traits.Network) fmt.Printf("IP Address: %v\n", record.Traits.IPAddress) } ``` ### ISP Database The ISP database provides ISP, organization, and ASN information. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-ISP.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("1.128.0.0") if err != nil { log.Fatal(err) } record, err := db.ISP(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("ISP: %v\n", record.ISP) fmt.Printf("Organization: %v\n", record.Organization) fmt.Printf("ASN: %v (%v)\n", record.AutonomousSystemNumber, record.AutonomousSystemOrganization) // Mobile carrier information (if available) if record.MobileCountryCode != "" { fmt.Printf("Mobile Country Code: %v\n", record.MobileCountryCode) fmt.Printf("Mobile Network Code: %v\n", record.MobileNetworkCode) } fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Domain Database The Domain database provides the second-level domain associated with an IP address. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-Domain.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("1.2.0.0") if err != nil { log.Fatal(err) } record, err := db.Domain(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Domain: %v\n", record.Domain) fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Connection Type Database The Connection Type database identifies the connection type of an IP address. ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-Connection-Type.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("1.0.128.0") if err != nil { log.Fatal(err) } record, err := db.ConnectionType(ip) if err != nil { log.Fatal(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Connection Type: %v\n", record.ConnectionType) fmt.Printf("Network: %v\n", record.Network) fmt.Printf("IP Address: %v\n", record.IPAddress) } ``` ### Error Handling All database lookups can return errors and should be handled appropriately: ```go package main import ( "fmt" "log" "net/netip" "github.com/oschwald/geoip2-golang/v2" ) func main() { db, err := geoip2.Open("GeoIP2-City.mmdb") if err != nil { log.Fatal(err) } defer db.Close() ip, err := netip.ParseAddr("10.0.0.1") // Private IP if err != nil { log.Fatal(err) } record, err := db.City(ip) if err != nil { log.Fatal(err) } // Always check if data was found if !record.HasData() { fmt.Println("No data found for this IP address") return } // Check individual fields before using them if record.City.Names.English != "" { fmt.Printf("City: %v\n", record.City.Names.English) } else { fmt.Println("City name not available") } // Check array bounds for subdivisions if len(record.Subdivisions) > 0 { fmt.Printf("Subdivision: %v\n", record.Subdivisions[0].Names.English) } else { fmt.Println("No subdivision data available") } fmt.Printf("Country: %v\n", record.Country.Names.English) } ``` ## Performance Tips ### Database Reuse Always reuse database instances across requests rather than opening/closing repeatedly: ```go // Good: Create once, use many times db, err := geoip2.Open("GeoIP2-City.mmdb") if err != nil { log.Fatal(err) } defer db.Close() // Use db for multiple lookups... ``` ### Memory Usage For applications needing only specific fields, consider using the lower-level maxminddb library with custom result structs to reduce memory allocation. ### Concurrent Usage The Reader is safe for concurrent use by multiple goroutines. ### JSON Serialization All result structs include JSON tags and support marshaling to JSON: ```go record, err := db.City(ip) if err != nil { log.Fatal(err) } jsonData, err := json.Marshal(record) if err != nil { log.Fatal(err) } fmt.Println(string(jsonData)) ``` ## Migration from v1 ### Breaking Changes - **Import Path**: Change from `github.com/oschwald/geoip2-golang` to `github.com/oschwald/geoip2-golang/v2` - **IP Type**: Use `netip.Addr` instead of `net.IP` - **Field Names**: `IsoCode` → `ISOCode` - **Names Access**: Use struct fields instead of map access - **Data Validation**: Use `HasData()` method to check for data availability ### Migration Example ```go // v1 import "github.com/oschwald/geoip2-golang" ip := net.ParseIP("81.2.69.142") record, err := db.City(ip) cityName := record.City.Names["en"] // v2 import "github.com/oschwald/geoip2-golang/v2" ip, err := netip.ParseAddr("81.2.69.142") if err != nil { // handle error } record, err := db.City(ip) if !record.HasData() { // handle no data found } cityName := record.City.Names.English ``` ## Troubleshooting ### Common Issues **Database not found**: Ensure the .mmdb file path is correct and readable. **No data returned**: Check if `HasData()` returns false - the IP may not be in the database or may be a private/reserved IP. **Performance issues**: Ensure you're reusing the database instance rather than opening it for each lookup. ## Testing Make sure you checked out test data submodule: ``` git submodule init git submodule update ``` Execute test suite: ``` go test ``` ## Contributing Contributions welcome! Please fork the repository and open a pull request with your changes. ## License This is free software, licensed under the ISC license. golang-github-oschwald-geoip2-golang-v2-2.2.0/example_test.go000066400000000000000000000170511521103116000240340ustar00rootroot00000000000000package geoip2 import ( "fmt" "log" "net/netip" ) // Example provides a basic example of using the API. Use of the Country // method is analogous to that of the City method. func Example() { db, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() // If you are using strings that may be invalid, use netip.ParseAddr and check for errors ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Panic(err) } record, err := db.City(ip) if err != nil { log.Panic(err) } fmt.Printf("Portuguese (BR) city name: %v\n", record.City.Names.BrazilianPortuguese) fmt.Printf("English subdivision name: %v\n", record.Subdivisions[0].Names.English) fmt.Printf("Russian country name: %v\n", record.Country.Names.Russian) fmt.Printf("ISO country code: %v\n", record.Country.ISOCode) fmt.Printf("Time zone: %v\n", record.Location.TimeZone) if record.Location.HasCoordinates() { fmt.Printf("Coordinates: %v, %v\n", *record.Location.Latitude, *record.Location.Longitude) } else { fmt.Println("Coordinates: unavailable") } // Output: // Portuguese (BR) city name: Londres // English subdivision name: England // Russian country name: Великобритания // ISO country code: GB // Time zone: Europe/London // Coordinates: 51.5142, -0.0931 } // ExampleReader_City demonstrates how to use the City database. func ExampleReader_City() { db, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Panic(err) } record, err := db.City(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("City: %v\n", record.City.Names.English) fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) fmt.Printf("Time zone: %v\n", record.Location.TimeZone) // Output: // City: London // Country: United Kingdom (GB) // Time zone: Europe/London } // ExampleReader_Country demonstrates how to use the Country database. func ExampleReader_Country() { db, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("81.2.69.142") if err != nil { log.Panic(err) } record, err := db.Country(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) fmt.Printf("Continent: %v (%v)\n", record.Continent.Names.English, record.Continent.Code) // Output: // Country: United Kingdom (GB) // Continent: Europe (EU) } // ExampleReader_ASN demonstrates how to use the ASN database. func ExampleReader_ASN() { db, err := Open("test-data/test-data/GeoLite2-ASN-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.128.0.0") if err != nil { log.Panic(err) } record, err := db.ASN(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("ASN: %v\n", record.AutonomousSystemNumber) fmt.Printf("Organization: %v\n", record.AutonomousSystemOrganization) // Output: // ASN: 1221 // Organization: Telstra Pty Ltd } // ExampleReader_AnonymousIP demonstrates how to use the Anonymous IP database. func ExampleReader_AnonymousIP() { db, err := Open("test-data/test-data/GeoIP2-Anonymous-IP-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.2.0.0") if err != nil { log.Panic(err) } record, err := db.AnonymousIP(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Is Anonymous: %v\n", record.IsAnonymous) fmt.Printf("Is Anonymous VPN: %v\n", record.IsAnonymousVPN) fmt.Printf("Is Public Proxy: %v\n", record.IsPublicProxy) // Output: // Is Anonymous: true // Is Anonymous VPN: true // Is Public Proxy: false } // ExampleReader_AnonymousPlus demonstrates how to use the Anonymous Plus database. func ExampleReader_AnonymousPlus() { db, err := Open("test-data/test-data/GeoIP-Anonymous-Plus-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.2.0.1") if err != nil { log.Panic(err) } record, err := db.AnonymousPlus(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Is Anonymous: %v\n", record.IsAnonymous) fmt.Printf("Is Anonymous VPN: %v\n", record.IsAnonymousVPN) fmt.Printf("Anonymizer Confidence: %v\n", record.AnonymizerConfidence) fmt.Printf("Provider Name: %v\n", record.ProviderName) fmt.Printf("Network Last Seen: %v\n", record.NetworkLastSeen.Format("2006-01-02")) // Output: // Is Anonymous: true // Is Anonymous VPN: true // Anonymizer Confidence: 30 // Provider Name: foo // Network Last Seen: 2025-04-14 } // ExampleReader_Enterprise demonstrates how to use the Enterprise database. func ExampleReader_Enterprise() { db, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("74.209.24.0") if err != nil { log.Panic(err) } record, err := db.Enterprise(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("City: %v\n", record.City.Names.English) fmt.Printf("Country: %v (%v)\n", record.Country.Names.English, record.Country.ISOCode) fmt.Printf("ISP: %v\n", record.Traits.ISP) fmt.Printf("Organization: %v\n", record.Traits.Organization) // Output: // City: Chatham // Country: United States (US) // ISP: Fairpoint Communications // Organization: Fairpoint Communications } // ExampleReader_ISP demonstrates how to use the ISP database. func ExampleReader_ISP() { db, err := Open("test-data/test-data/GeoIP2-ISP-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.128.0.0") if err != nil { log.Panic(err) } record, err := db.ISP(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("ISP: %v\n", record.ISP) fmt.Printf("Organization: %v\n", record.Organization) fmt.Printf("ASN: %v\n", record.AutonomousSystemNumber) // Output: // ISP: Telstra Internet // Organization: Telstra Internet // ASN: 1221 } // ExampleReader_Domain demonstrates how to use the Domain database. func ExampleReader_Domain() { db, err := Open("test-data/test-data/GeoIP2-Domain-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.2.0.0") if err != nil { log.Panic(err) } record, err := db.Domain(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Domain: %v\n", record.Domain) // Output: // Domain: maxmind.com } // ExampleReader_ConnectionType demonstrates how to use the Connection Type database. func ExampleReader_ConnectionType() { db, err := Open("test-data/test-data/GeoIP2-Connection-Type-Test.mmdb") if err != nil { log.Panic(err) } defer db.Close() ip, err := netip.ParseAddr("1.0.128.0") if err != nil { log.Panic(err) } record, err := db.ConnectionType(ip) if err != nil { log.Panic(err) } if !record.HasData() { fmt.Println("No data found for this IP") return } fmt.Printf("Connection Type: %v\n", record.ConnectionType) // Output: // Connection Type: Cable/DSL } golang-github-oschwald-geoip2-golang-v2-2.2.0/go.mod000066400000000000000000000005221521103116000221140ustar00rootroot00000000000000module github.com/oschwald/geoip2-golang/v2 go 1.25.0 require ( github.com/oschwald/maxminddb-golang/v2 v2.3.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.44.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-oschwald-geoip2-golang-v2-2.2.0/go.sum000066400000000000000000000023211521103116000221400ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/oschwald/maxminddb-golang/v2 v2.3.0 h1:PnXjMGjkSQlwOBSyZ7hk6Fd75t7erkAhJNJgEhA3MQU= github.com/oschwald/maxminddb-golang/v2 v2.3.0/go.mod h1:NSQvgFwPxODpBTJI5+5Ns1AAucnx7ggW9PSRRifAT1s= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-oschwald-geoip2-golang-v2-2.2.0/models.go000066400000000000000000000774371521103116000226430ustar00rootroot00000000000000package geoip2 import ( "fmt" "net/netip" "time" "github.com/oschwald/maxminddb-golang/v2/mmdbdata" ) // Date represents a date value that is stored as an ISO 8601 string // in the database but exposed as a time.Time. type Date struct { time.Time } // UnmarshalMaxMindDB implements the mmdbdata.Unmarshaler interface. func (d *Date) UnmarshalMaxMindDB(decoder *mmdbdata.Decoder) error { s, err := decoder.ReadString() if err != nil { return err } if s == "" { return nil } t, err := time.Parse(time.DateOnly, s) if err != nil { return fmt.Errorf("parsing date %q: %w", s, err) } d.Time = t return nil } // MarshalJSON implements json.Marshaler to serialize as ISO date string. func (d Date) MarshalJSON() ([]byte, error) { //nolint:unparam // error required by json.Marshaler if d.IsZero() { return []byte("null"), nil } return []byte(`"` + d.Format(time.DateOnly) + `"`), nil } // Names contains localized names for geographic entities. type Names struct { // German localized name German string `json:"de,omitzero" maxminddb:"de"` // English localized name English string `json:"en,omitzero" maxminddb:"en"` // Spanish localized name Spanish string `json:"es,omitzero" maxminddb:"es"` // French localized name French string `json:"fr,omitzero" maxminddb:"fr"` // Japanese localized name Japanese string `json:"ja,omitzero" maxminddb:"ja"` // BrazilianPortuguese localized name (pt-BR) BrazilianPortuguese string `json:"pt-BR,omitzero" maxminddb:"pt-BR"` //nolint:tagliatelle,lll // pt-BR matches MMDB format // Russian localized name Russian string `json:"ru,omitzero" maxminddb:"ru"` // SimplifiedChinese localized name (zh-CN) SimplifiedChinese string `json:"zh-CN,omitzero" maxminddb:"zh-CN"` //nolint:tagliatelle // zh-CN matches MMDB format } var ( zeroNames Names zeroContinent Continent zeroLocation Location zeroRepresentedCountry RepresentedCountry zeroCityRecord CityRecord zeroCityPostal CityPostal zeroCitySubdivision CitySubdivision zeroCountryRecord CountryRecord zeroEnterpriseCityRecord EnterpriseCityRecord zeroEnterprisePostal EnterprisePostal zeroEnterpriseSubdivision EnterpriseSubdivision zeroEnterpriseCountryRecord EnterpriseCountryRecord zeroEnterpriseTraits EnterpriseTraits zeroCityTraits CityTraits zeroCountryTraits CountryTraits ) // HasData returns true if the Names struct has any localized names. func (n Names) HasData() bool { return n != zeroNames } // Common types used across multiple database records // Continent contains data for the continent record associated with an IP address. type Continent struct { // Names contains localized names for the continent Names Names `json:"names,omitzero" maxminddb:"names"` // Code is a two character continent code like "NA" (North America) or // "OC" (Oceania) Code string `json:"code,omitzero" maxminddb:"code"` // GeoNameID for the continent GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` } // HasData returns true if the Continent has any data. func (c Continent) HasData() bool { return c != zeroContinent } // Location contains data for the location record associated with an IP address. type Location struct { // Latitude is the approximate latitude of the location associated with // the IP address. This value is not precise and should not be used to // identify a particular address or household. Will be nil if not present // in the database. Latitude *float64 `json:"latitude,omitzero" maxminddb:"latitude"` // Longitude is the approximate longitude of the location associated with // the IP address. This value is not precise and should not be used to // identify a particular address or household. Will be nil if not present // in the database. Longitude *float64 `json:"longitude,omitzero" maxminddb:"longitude"` // TimeZone is the time zone associated with location, as specified by // the IANA Time Zone Database (e.g., "America/New_York") TimeZone string `json:"time_zone,omitzero" maxminddb:"time_zone"` // MetroCode is a metro code for targeting advertisements. // // Deprecated: Metro codes are no longer maintained and should not be used. MetroCode uint `json:"metro_code,omitzero" maxminddb:"metro_code"` // AccuracyRadius is the approximate accuracy radius in kilometers around // the latitude and longitude. This is the radius where we have a 67% // confidence that the device using the IP address resides within the // circle. AccuracyRadius uint16 `json:"accuracy_radius,omitzero" maxminddb:"accuracy_radius"` } // HasData returns true if the Location has any data. func (l Location) HasData() bool { return l != zeroLocation } // HasCoordinates returns true if both latitude and longitude are present. func (l Location) HasCoordinates() bool { return l.Latitude != nil && l.Longitude != nil } // RepresentedCountry contains data for the represented country associated // with an IP address. The represented country is the country represented // by something like a military base or embassy. type RepresentedCountry struct { // Names contains localized names for the represented country Names Names `json:"names,omitzero" maxminddb:"names"` // ISOCode is the two-character ISO 3166-1 alpha code for the represented // country. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 ISOCode string `json:"iso_code,omitzero" maxminddb:"iso_code"` // Type is a string indicating the type of entity that is representing // the country. Currently this is only "military" but may expand in the future. Type string `json:"type,omitzero" maxminddb:"type"` // GeoNameID for the represented country GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` // IsInEuropeanUnion is true if the represented country is a member // state of the European Union IsInEuropeanUnion bool `json:"is_in_european_union,omitzero" maxminddb:"is_in_european_union"` } // HasData returns true if the RepresentedCountry has any data. func (r RepresentedCountry) HasData() bool { return r != zeroRepresentedCountry } // Enterprise-specific types // EnterpriseCityRecord contains city data for Enterprise database records. type EnterpriseCityRecord struct { // Names contains localized names for the city Names Names `json:"names,omitzero" maxminddb:"names"` // GeoNameID for the city GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` // Confidence is a value from 0-100 indicating MaxMind's confidence that // the city is correct Confidence uint8 `json:"confidence,omitzero" maxminddb:"confidence"` } // HasData returns true if the EnterpriseCityRecord has any data. func (c EnterpriseCityRecord) HasData() bool { return c != zeroEnterpriseCityRecord } // EnterprisePostal contains postal data for Enterprise database records. type EnterprisePostal struct { // Code is the postal code of the location. Postal codes are not // available for all countries. // In some countries, this will only contain part of the postal code. Code string `json:"code,omitzero" maxminddb:"code"` // Confidence is a value from 0-100 indicating MaxMind's confidence that // the postal code is correct Confidence uint8 `json:"confidence,omitzero" maxminddb:"confidence"` } // HasData returns true if the EnterprisePostal has any data. func (p EnterprisePostal) HasData() bool { return p != zeroEnterprisePostal } // EnterpriseSubdivision contains subdivision data for Enterprise database records. type EnterpriseSubdivision struct { // Names contains localized names for the subdivision Names Names `json:"names,omitzero" maxminddb:"names"` // ISOCode is a string up to three characters long containing the // subdivision portion of the ISO 3166-2 code. // See https://en.wikipedia.org/wiki/ISO_3166-2 ISOCode string `json:"iso_code,omitzero" maxminddb:"iso_code"` // GeoNameID for the subdivision GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` // Confidence is a value from 0-100 indicating MaxMind's confidence that // the subdivision is correct Confidence uint8 `json:"confidence,omitzero" maxminddb:"confidence"` } // HasData returns true if the EnterpriseSubdivision has any data. func (s EnterpriseSubdivision) HasData() bool { return s != zeroEnterpriseSubdivision } // EnterpriseCountryRecord contains country data for Enterprise database records. type EnterpriseCountryRecord struct { // Names contains localized names for the country Names Names `json:"names,omitzero" maxminddb:"names"` // ISOCode is the two-character ISO 3166-1 alpha code for the country. // See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 ISOCode string `json:"iso_code,omitzero" maxminddb:"iso_code"` // GeoNameID for the country GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` // Confidence is a value from 0-100 indicating MaxMind's confidence that // the country is correct Confidence uint8 `json:"confidence,omitzero" maxminddb:"confidence"` // IsInEuropeanUnion is true if the country is a member state of the // European Union IsInEuropeanUnion bool `json:"is_in_european_union,omitzero" maxminddb:"is_in_european_union"` } // HasData returns true if the EnterpriseCountryRecord has any data. func (c EnterpriseCountryRecord) HasData() bool { return c != zeroEnterpriseCountryRecord } // EnterpriseTraits contains traits data for Enterprise database records. type EnterpriseTraits struct { // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // AutonomousSystemOrganization for the registered ASN AutonomousSystemOrganization string `json:"autonomous_system_organization,omitzero" maxminddb:"autonomous_system_organization"` //nolint:lll // ConnectionType indicates the connection type. May be Dialup, // Cable/DSL, Corporate, Cellular, or Satellite ConnectionType string `json:"connection_type,omitzero" maxminddb:"connection_type"` // Domain is the second level domain associated with the IP address // (e.g., "example.com") Domain string `json:"domain,omitzero" maxminddb:"domain"` // ISP is the name of the ISP associated with the IP address ISP string `json:"isp,omitzero" maxminddb:"isp"` // MobileCountryCode is the mobile country code (MCC) associated with // the IP address and ISP. // See https://en.wikipedia.org/wiki/Mobile_country_code MobileCountryCode string `json:"mobile_country_code,omitzero" maxminddb:"mobile_country_code"` // MobileNetworkCode is the mobile network code (MNC) associated with // the IP address and ISP. // See https://en.wikipedia.org/wiki/Mobile_network_code MobileNetworkCode string `json:"mobile_network_code,omitzero" maxminddb:"mobile_network_code"` // Organization is the name of the organization associated with the IP // address Organization string `json:"organization,omitzero" maxminddb:"organization"` // UserType indicates the user type associated with the IP address // (business, cafe, cellular, college, etc.) UserType string `json:"user_type,omitzero" maxminddb:"user_type"` // StaticIPScore was added in error and has never been populated. // // Deprecated: This field will be removed in the next major release. StaticIPScore float64 `json:"static_ip_score,omitzero" maxminddb:"static_ip_score"` // AutonomousSystemNumber for the IP address AutonomousSystemNumber uint `json:"autonomous_system_number,omitzero" maxminddb:"autonomous_system_number"` // IsAnycast is true if the IP address belongs to an anycast network. // See https://en.wikipedia.org/wiki/Anycast IsAnycast bool `json:"is_anycast,omitzero" maxminddb:"is_anycast"` // IsLegitimateProxy is true if MaxMind believes this IP address to be a // legitimate proxy, such as an internal VPN used by a corporation. // // Deprecated: MaxMind has deprecated this field. It will be removed in // the next major release. IsLegitimateProxy bool `json:"is_legitimate_proxy,omitzero" maxminddb:"is_legitimate_proxy"` } // HasData returns true if the EnterpriseTraits has any data (excluding Network and IPAddress). func (t EnterpriseTraits) HasData() bool { cmp := t cmp.Network = zeroEnterpriseTraits.Network cmp.IPAddress = zeroEnterpriseTraits.IPAddress return cmp != zeroEnterpriseTraits } // City/Country-specific types // CityRecord contains city data for City database records. type CityRecord struct { // Names contains localized names for the city Names Names `json:"names,omitzero" maxminddb:"names"` // GeoNameID for the city GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` } // HasData returns true if the CityRecord has any data. func (c CityRecord) HasData() bool { return c != zeroCityRecord } // CityPostal contains postal data for City database records. type CityPostal struct { // Code is the postal code of the location. Postal codes are not // available for all countries. // In some countries, this will only contain part of the postal code. Code string `json:"code,omitzero" maxminddb:"code"` } // HasData returns true if the CityPostal has any data. func (p CityPostal) HasData() bool { return p != zeroCityPostal } // CitySubdivision contains subdivision data for City database records. type CitySubdivision struct { Names Names `json:"names,omitzero" maxminddb:"names"` ISOCode string `json:"iso_code,omitzero" maxminddb:"iso_code"` GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` } // HasData returns true if the CitySubdivision has any data. func (s CitySubdivision) HasData() bool { return s != zeroCitySubdivision } // CountryRecord contains country data for City and Country database records. type CountryRecord struct { // Names contains localized names for the country Names Names `json:"names,omitzero" maxminddb:"names"` // ISOCode is the two-character ISO 3166-1 alpha code for the country. // See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 ISOCode string `json:"iso_code,omitzero" maxminddb:"iso_code"` // GeoNameID for the country GeoNameID uint `json:"geoname_id,omitzero" maxminddb:"geoname_id"` // IsInEuropeanUnion is true if the country is a member state of the // European Union IsInEuropeanUnion bool `json:"is_in_european_union,omitzero" maxminddb:"is_in_european_union"` } // HasData returns true if the CountryRecord has any data. func (c CountryRecord) HasData() bool { return c != zeroCountryRecord } // CityTraits contains traits data for City database records. type CityTraits struct { // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the network prefix for this record. This is the largest // network where all of the fields besides IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // IsAnycast is true if the IP address belongs to an anycast network. // See https://en.wikipedia.org/wiki/Anycast IsAnycast bool `json:"is_anycast,omitzero" maxminddb:"is_anycast"` } // HasData returns true if the CityTraits has any data (excluding Network and IPAddress). func (t CityTraits) HasData() bool { cmp := t cmp.Network = zeroCityTraits.Network cmp.IPAddress = zeroCityTraits.IPAddress return cmp != zeroCityTraits } // CountryTraits contains traits data for Country database records. type CountryTraits struct { // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // IsAnycast is true if the IP address belongs to an anycast network. // See https://en.wikipedia.org/wiki/Anycast IsAnycast bool `json:"is_anycast,omitzero" maxminddb:"is_anycast"` } // HasData returns true if the CountryTraits has any data (excluding Network and IPAddress). func (t CountryTraits) HasData() bool { cmp := t cmp.Network = zeroCountryTraits.Network cmp.IPAddress = zeroCountryTraits.IPAddress return cmp != zeroCountryTraits } // The Enterprise struct corresponds to the data in the GeoIP2 Enterprise // database. type Enterprise struct { // Continent contains data for the continent record associated with the IP // address. Continent Continent `json:"continent,omitzero" maxminddb:"continent"` // Subdivisions contains data for the subdivisions associated with the IP // address. The subdivisions array is ordered from largest to smallest. For // instance, the response for Oxford in the United Kingdom would have England // as the first element and Oxfordshire as the second element. Subdivisions []EnterpriseSubdivision `json:"subdivisions,omitzero" maxminddb:"subdivisions"` // Postal contains data for the postal record associated with the IP address. Postal EnterprisePostal `json:"postal,omitzero" maxminddb:"postal"` // RepresentedCountry contains data for the represented country associated // with the IP address. The represented country is the country represented // by something like a military base or embassy. RepresentedCountry RepresentedCountry `json:"represented_country,omitzero" maxminddb:"represented_country"` // Country contains data for the country record associated with the IP // address. This record represents the country where MaxMind believes the IP // is located. Country EnterpriseCountryRecord `json:"country,omitzero" maxminddb:"country"` // RegisteredCountry contains data for the registered country associated // with the IP address. This record represents the country where the ISP has // registered the IP block and may differ from the user's country. RegisteredCountry CountryRecord `json:"registered_country,omitzero" maxminddb:"registered_country"` // City contains data for the city record associated with the IP address. City EnterpriseCityRecord `json:"city,omitzero" maxminddb:"city"` // Location contains data for the location record associated with the IP // address Location Location `json:"location,omitzero" maxminddb:"location"` // Traits contains various traits associated with the IP address Traits EnterpriseTraits `json:"traits,omitzero" maxminddb:"traits"` } // HasData returns true if any GeoIP data was found for the IP in the Enterprise database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (e Enterprise) HasData() bool { return e.Continent.HasData() || e.City.HasData() || e.Postal.HasData() || e.hasSubdivisionsData() || e.RepresentedCountry.HasData() || e.Country.HasData() || e.RegisteredCountry.HasData() || e.Traits.HasData() || e.Location.HasData() } func (e Enterprise) hasSubdivisionsData() bool { for _, sub := range e.Subdivisions { if sub.HasData() { return true } } return false } // The City struct corresponds to the data in the GeoIP2/GeoLite2 City // databases. type City struct { // Traits contains various traits associated with the IP address Traits CityTraits `json:"traits,omitzero" maxminddb:"traits"` // Postal contains data for the postal record associated with the IP address Postal CityPostal `json:"postal,omitzero" maxminddb:"postal"` // Continent contains data for the continent record associated with the IP address Continent Continent `json:"continent,omitzero" maxminddb:"continent"` // City contains data for the city record associated with the IP address City CityRecord `json:"city,omitzero" maxminddb:"city"` // Subdivisions contains data for the subdivisions associated with the IP // address. The subdivisions array is ordered from largest to smallest. For // instance, the response for Oxford in the United Kingdom would have England // as the first element and Oxfordshire as the second element. Subdivisions []CitySubdivision `json:"subdivisions,omitzero" maxminddb:"subdivisions"` // RepresentedCountry contains data for the represented country associated // with the IP address. The represented country is the country represented // by something like a military base or embassy. RepresentedCountry RepresentedCountry `json:"represented_country,omitzero" maxminddb:"represented_country"` // Country contains data for the country record associated with the IP // address. This record represents the country where MaxMind believes the IP // is located. Country CountryRecord `json:"country,omitzero" maxminddb:"country"` // RegisteredCountry contains data for the registered country associated // with the IP address. This record represents the country where the ISP has // registered the IP block and may differ from the user's country. RegisteredCountry CountryRecord `json:"registered_country,omitzero" maxminddb:"registered_country"` // Location contains data for the location record associated with the IP // address Location Location `json:"location,omitzero" maxminddb:"location"` } // HasData returns true if any GeoIP data was found for the IP in the City database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (c City) HasData() bool { return c.Traits.HasData() || c.Postal.HasData() || c.Continent.HasData() || c.City.HasData() || c.hasSubdivisionsData() || c.RepresentedCountry.HasData() || c.Country.HasData() || c.RegisteredCountry.HasData() || c.Location.HasData() } func (c City) hasSubdivisionsData() bool { for _, sub := range c.Subdivisions { if sub.HasData() { return true } } return false } // The Country struct corresponds to the data in the GeoIP2/GeoLite2 // Country databases. type Country struct { // Traits contains various traits associated with the IP address Traits CountryTraits `json:"traits,omitzero" maxminddb:"traits"` // Continent contains data for the continent record associated with the IP address Continent Continent `json:"continent,omitzero" maxminddb:"continent"` // RepresentedCountry contains data for the represented country associated // with the IP address. The represented country is the country represented // by something like a military base or embassy. RepresentedCountry RepresentedCountry `json:"represented_country,omitzero" maxminddb:"represented_country"` // Country contains data for the country record associated with the IP // address. This record represents the country where MaxMind believes the IP // is located. Country CountryRecord `json:"country,omitzero" maxminddb:"country"` // RegisteredCountry contains data for the registered country associated // with the IP address. This record represents the country where the ISP has // registered the IP block and may differ from the user's country. RegisteredCountry CountryRecord `json:"registered_country,omitzero" maxminddb:"registered_country"` } // HasData returns true if any GeoIP data was found for the IP in the Country database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (c Country) HasData() bool { return c.Continent.HasData() || c.RepresentedCountry.HasData() || c.Country.HasData() || c.RegisteredCountry.HasData() || c.Traits.HasData() } // The AnonymousIP struct corresponds to the data in the GeoIP2 // Anonymous IP database. type AnonymousIP struct { // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // IsAnonymous is true if the IP address belongs to any sort of anonymous network. IsAnonymous bool `json:"is_anonymous,omitzero" maxminddb:"is_anonymous"` // IsAnonymousVPN is true if the IP address is registered to an anonymous // VPN provider. If a VPN provider does not register subnets under names // associated with them, we will likely only flag their IP ranges using the // IsHostingProvider attribute. IsAnonymousVPN bool `json:"is_anonymous_vpn,omitzero" maxminddb:"is_anonymous_vpn"` // IsHostingProvider is true if the IP address belongs to a hosting or VPN provider // (see description of IsAnonymousVPN attribute). IsHostingProvider bool `json:"is_hosting_provider,omitzero" maxminddb:"is_hosting_provider"` // IsPublicProxy is true if the IP address belongs to a public proxy. IsPublicProxy bool `json:"is_public_proxy,omitzero" maxminddb:"is_public_proxy"` // IsResidentialProxy is true if the IP address is on a suspected // anonymizing network and belongs to a residential ISP. IsResidentialProxy bool `json:"is_residential_proxy,omitzero" maxminddb:"is_residential_proxy"` // IsTorExitNode is true if the IP address is a Tor exit node. IsTorExitNode bool `json:"is_tor_exit_node,omitzero" maxminddb:"is_tor_exit_node"` } // HasData returns true if any data was found for the IP in the AnonymousIP database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (a AnonymousIP) HasData() bool { return a.IsAnonymous || a.IsAnonymousVPN || a.IsHostingProvider || a.IsPublicProxy || a.IsResidentialProxy || a.IsTorExitNode } // The AnonymousPlus struct corresponds to the data in the GeoIP Anonymous Plus // database. This database provides VPN detection with confidence scoring, // provider identification, and temporal tracking. type AnonymousPlus struct { // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // NetworkLastSeen is the last day the network was sighted in anonymized // network analysis. NetworkLastSeen Date `json:"network_last_seen,omitzero" maxminddb:"network_last_seen"` // ProviderName is the name of the VPN provider (e.g., "NordVPN", "SurfShark"). ProviderName string `json:"provider_name,omitzero" maxminddb:"provider_name"` // AnonymizerConfidence is a score from 1 to 99 indicating the confidence // that the network is part of an actively used VPN. AnonymizerConfidence uint16 `json:"anonymizer_confidence,omitzero" maxminddb:"anonymizer_confidence"` // IsAnonymous is true if the IP address belongs to any sort of anonymous network. IsAnonymous bool `json:"is_anonymous,omitzero" maxminddb:"is_anonymous"` // IsAnonymousVPN is true if the IP address is registered to an anonymous // VPN provider. If a VPN provider does not register subnets under names // associated with them, we will likely only flag their IP ranges using the // IsHostingProvider attribute. IsAnonymousVPN bool `json:"is_anonymous_vpn,omitzero" maxminddb:"is_anonymous_vpn"` // IsHostingProvider is true if the IP address belongs to a hosting or VPN provider // (see description of IsAnonymousVPN attribute). IsHostingProvider bool `json:"is_hosting_provider,omitzero" maxminddb:"is_hosting_provider"` // IsPublicProxy is true if the IP address belongs to a public proxy. IsPublicProxy bool `json:"is_public_proxy,omitzero" maxminddb:"is_public_proxy"` // IsResidentialProxy is true if the IP address is on a suspected // anonymizing network and belongs to a residential ISP. IsResidentialProxy bool `json:"is_residential_proxy,omitzero" maxminddb:"is_residential_proxy"` // IsTorExitNode is true if the IP address is a Tor exit node. IsTorExitNode bool `json:"is_tor_exit_node,omitzero" maxminddb:"is_tor_exit_node"` } // HasData returns true if any data was found for the IP in the AnonymousPlus database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (a AnonymousPlus) HasData() bool { return a.AnonymizerConfidence != 0 || a.IsAnonymous || a.IsAnonymousVPN || a.IsHostingProvider || a.IsPublicProxy || a.IsResidentialProxy || a.IsTorExitNode || !a.NetworkLastSeen.IsZero() || a.ProviderName != "" } // The ASN struct corresponds to the data in the GeoLite2 ASN database. type ASN struct { // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // AutonomousSystemOrganization for the registered autonomous system number. AutonomousSystemOrganization string `json:"autonomous_system_organization,omitzero" maxminddb:"autonomous_system_organization"` //nolint:lll // AutonomousSystemNumber for the IP address. AutonomousSystemNumber uint `json:"autonomous_system_number,omitzero" maxminddb:"autonomous_system_number"` //nolint:lll } // HasData returns true if any data was found for the IP in the ASN database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (a ASN) HasData() bool { return a.AutonomousSystemNumber != 0 || a.AutonomousSystemOrganization != "" } // The ConnectionType struct corresponds to the data in the GeoIP2 // Connection-Type database. type ConnectionType struct { // ConnectionType indicates the connection type. May be Dialup, Cable/DSL, // Corporate, Cellular, or Satellite. Additional values may be added in the // future. ConnectionType string `json:"connection_type,omitzero" maxminddb:"connection_type"` // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` } // HasData returns true if any data was found for the IP in the ConnectionType database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (c ConnectionType) HasData() bool { return c.ConnectionType != "" } // The Domain struct corresponds to the data in the GeoIP2 Domain database. type Domain struct { // Domain is the second level domain associated with the IP address // (e.g., "example.com") Domain string `json:"domain,omitzero" maxminddb:"domain"` // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` } // HasData returns true if any data was found for the IP in the Domain database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (d Domain) HasData() bool { return d.Domain != "" } // The ISP struct corresponds to the data in the GeoIP2 ISP database. type ISP struct { // Network is the largest network prefix where all fields besides // IPAddress have the same value. Network netip.Prefix `json:"network,omitzero"` // IPAddress is the IP address used during the lookup IPAddress netip.Addr `json:"ip_address,omitzero"` // AutonomousSystemOrganization for the registered ASN AutonomousSystemOrganization string `json:"autonomous_system_organization,omitzero" maxminddb:"autonomous_system_organization"` //nolint:lll // ISP is the name of the ISP associated with the IP address ISP string `json:"isp,omitzero" maxminddb:"isp"` // MobileCountryCode is the mobile country code (MCC) associated with the IP address and ISP. // See https://en.wikipedia.org/wiki/Mobile_country_code MobileCountryCode string `json:"mobile_country_code,omitzero" maxminddb:"mobile_country_code"` // MobileNetworkCode is the mobile network code (MNC) associated with the IP address and ISP. // See https://en.wikipedia.org/wiki/Mobile_network_code MobileNetworkCode string `json:"mobile_network_code,omitzero" maxminddb:"mobile_network_code"` // Organization is the name of the organization associated with the IP address Organization string `json:"organization,omitzero" maxminddb:"organization"` // AutonomousSystemNumber for the IP address AutonomousSystemNumber uint `json:"autonomous_system_number,omitzero" maxminddb:"autonomous_system_number"` } // HasData returns true if any data was found for the IP in the ISP database. // This excludes the Network and IPAddress fields which are always populated for found IPs. func (i ISP) HasData() bool { return i.AutonomousSystemOrganization != "" || i.ISP != "" || i.MobileCountryCode != "" || i.MobileNetworkCode != "" || i.Organization != "" || i.AutonomousSystemNumber != 0 } golang-github-oschwald-geoip2-golang-v2-2.2.0/reader.go000066400000000000000000000243471521103116000226120ustar00rootroot00000000000000// Package geoip2 provides an easy-to-use API for the MaxMind GeoIP2 and // GeoLite2 databases; this package does not support GeoIP Legacy databases. // // # Basic Usage // // db, err := geoip2.Open("GeoIP2-City.mmdb") // if err != nil { // log.Fatal(err) // } // defer db.Close() // // ip, err := netip.ParseAddr("81.2.69.142") // if err != nil { // log.Fatal(err) // } // // record, err := db.City(ip) // if err != nil { // log.Fatal(err) // } // // if !record.HasData() { // fmt.Println("No data found for this IP") // return // } // // fmt.Printf("City: %v\n", record.City.Names.English) // fmt.Printf("Country: %v\n", record.Country.Names.English) // // # Database Types // // This library supports all MaxMind database types: // - City: Most comprehensive geolocation data // - Country: Country-level geolocation // - ASN: Autonomous system information // - AnonymousIP: Anonymous network detection // - Enterprise: Enhanced City data with additional fields // - ISP: Internet service provider information // - Domain: Second-level domain data // - ConnectionType: Connection type identification // // # Version 2.0 Features // // Version 2.0 introduces significant improvements: // - Modern API using netip.Addr instead of net.IP // - Network and IPAddress fields in all result structs // - HasData() method for data validation // - Structured Names type for localized names // - JSON serialization support // // See github.com/oschwald/maxminddb-golang/v2 for more advanced use cases. package geoip2 import ( "fmt" "net/netip" "github.com/oschwald/maxminddb-golang/v2" ) type databaseType int const ( isAnonymousIP = 1 << iota isAnonymousPlus isASN isCity isConnectionType isCountry isDomain isEnterprise isISP ) // Reader holds the maxminddb.Reader struct. It can be created using the // Open and OpenBytes functions. type Reader struct { mmdbReader *maxminddb.Reader databaseType databaseType } // Option configures Reader behavior. type Option func(*readerOptions) type readerOptions struct { maxminddb []maxminddb.ReaderOption } func applyOptions(options []Option) []maxminddb.ReaderOption { var opts readerOptions for _, option := range options { if option != nil { option(&opts) } } return opts.maxminddb } // InvalidMethodError is returned when a lookup method is called on a // database that it does not support. For instance, calling the ISP method // on a City database. type InvalidMethodError struct { Method string DatabaseType string } func (e InvalidMethodError) Error() string { return fmt.Sprintf(`geoip2: the %s method does not support the %s database`, e.Method, e.DatabaseType) } // UnknownDatabaseTypeError is returned when an unknown database type is // opened. type UnknownDatabaseTypeError struct { DatabaseType string } func (e UnknownDatabaseTypeError) Error() string { return fmt.Sprintf(`geoip2: reader does not support the %q database type`, e.DatabaseType) } // Open takes a string path to a file and returns a Reader struct or an error. // The database file is opened using a memory map. Use the Close method on the // Reader object to return the resources to the system. func Open(file string, options ...Option) (*Reader, error) { reader, err := maxminddb.Open(file, applyOptions(options)...) if err != nil { return nil, err } dbType, err := getDBType(reader) return &Reader{reader, dbType}, err } // OpenBytes takes a byte slice corresponding to a GeoIP2/GeoLite2 database // file and returns a Reader struct or an error. Note that the byte slice is // used directly; any modification of it after opening the database will result // in errors while reading from the database. func OpenBytes(bytes []byte, options ...Option) (*Reader, error) { reader, err := maxminddb.OpenBytes(bytes, applyOptions(options)...) if err != nil { return nil, err } dbType, err := getDBType(reader) return &Reader{reader, dbType}, err } func getDBType(reader *maxminddb.Reader) (databaseType, error) { switch reader.Metadata.DatabaseType { case "GeoIP-Anonymous-Plus": return isAnonymousPlus, nil case "GeoIP2-Anonymous-IP": return isAnonymousIP, nil case "DBIP-ASN-Lite (compat=GeoLite2-ASN)", "GeoLite2-ASN": return isASN, nil // We allow City lookups on Country for back compat case "DBIP-City-Lite", "DBIP-Country-Lite", "DBIP-Country", "DBIP-Location (compat=City)", "GeoLite2-City", "GeoIP-City-Redacted-US", "GeoIP2-City", "GeoIP2-City-Africa", "GeoIP2-City-Asia-Pacific", "GeoIP2-City-Europe", "GeoIP2-City-North-America", "GeoIP2-City-South-America", "GeoIP2-Precision-City", "GeoLite2-Country", "GeoIP2-Country": return isCity | isCountry, nil case "GeoIP2-Connection-Type": return isConnectionType, nil case "GeoIP2-Domain": return isDomain, nil case "DBIP-ISP (compat=Enterprise)", "DBIP-Location-ISP (compat=Enterprise)", "GeoIP-Enterprise-Redacted-US", "GeoIP2-Enterprise": return isEnterprise | isCity | isCountry, nil case "GeoIP2-ISP", "GeoIP2-Precision-ISP": return isISP | isASN, nil default: return 0, UnknownDatabaseTypeError{reader.Metadata.DatabaseType} } } // Enterprise takes an IP address as a netip.Addr and returns an Enterprise // struct and/or an error. This is intended to be used with the GeoIP2 // Enterprise database. func (r *Reader) Enterprise(ipAddress netip.Addr) (*Enterprise, error) { if isEnterprise&r.databaseType == 0 { return nil, InvalidMethodError{"Enterprise", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var enterprise Enterprise err := result.Decode(&enterprise) if err != nil { return &enterprise, err } enterprise.Traits.IPAddress = ipAddress enterprise.Traits.Network = result.Prefix() return &enterprise, nil } // City takes an IP address as a netip.Addr and returns a City struct // and/or an error. Although this can be used with other databases, this // method generally should be used with the GeoIP2 or GeoLite2 City databases. func (r *Reader) City(ipAddress netip.Addr) (*City, error) { if isCity&r.databaseType == 0 { return nil, InvalidMethodError{"City", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var city City err := result.Decode(&city) if err != nil { return &city, err } city.Traits.IPAddress = ipAddress city.Traits.Network = result.Prefix() return &city, nil } // Country takes an IP address as a netip.Addr and returns a Country struct // and/or an error. Although this can be used with other databases, this // method generally should be used with the GeoIP2 or GeoLite2 Country // databases. func (r *Reader) Country(ipAddress netip.Addr) (*Country, error) { if isCountry&r.databaseType == 0 { return nil, InvalidMethodError{"Country", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var country Country err := result.Decode(&country) if err != nil { return &country, err } country.Traits.IPAddress = ipAddress country.Traits.Network = result.Prefix() return &country, nil } // AnonymousIP takes an IP address as a netip.Addr and returns a // AnonymousIP struct and/or an error. func (r *Reader) AnonymousIP(ipAddress netip.Addr) (*AnonymousIP, error) { if isAnonymousIP&r.databaseType == 0 { return nil, InvalidMethodError{"AnonymousIP", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var anonIP AnonymousIP err := result.Decode(&anonIP) if err != nil { return &anonIP, err } anonIP.IPAddress = ipAddress anonIP.Network = result.Prefix() return &anonIP, nil } // AnonymousPlus takes an IP address as a netip.Addr and returns an // AnonymousPlus struct and/or an error. This is intended to be used with // the GeoIP Anonymous Plus database. func (r *Reader) AnonymousPlus(ipAddress netip.Addr) (*AnonymousPlus, error) { if isAnonymousPlus&r.databaseType == 0 { return nil, InvalidMethodError{"AnonymousPlus", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var anonPlus AnonymousPlus err := result.Decode(&anonPlus) if err != nil { return &anonPlus, err } anonPlus.IPAddress = ipAddress anonPlus.Network = result.Prefix() return &anonPlus, nil } // ASN takes an IP address as a netip.Addr and returns a ASN struct and/or // an error. func (r *Reader) ASN(ipAddress netip.Addr) (*ASN, error) { if isASN&r.databaseType == 0 { return nil, InvalidMethodError{"ASN", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var val ASN err := result.Decode(&val) if err != nil { return &val, err } val.IPAddress = ipAddress val.Network = result.Prefix() return &val, nil } // ConnectionType takes an IP address as a netip.Addr and returns a // ConnectionType struct and/or an error. func (r *Reader) ConnectionType(ipAddress netip.Addr) (*ConnectionType, error) { if isConnectionType&r.databaseType == 0 { return nil, InvalidMethodError{"ConnectionType", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var val ConnectionType err := result.Decode(&val) if err != nil { return &val, err } val.IPAddress = ipAddress val.Network = result.Prefix() return &val, nil } // Domain takes an IP address as a netip.Addr and returns a // Domain struct and/or an error. func (r *Reader) Domain(ipAddress netip.Addr) (*Domain, error) { if isDomain&r.databaseType == 0 { return nil, InvalidMethodError{"Domain", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var val Domain err := result.Decode(&val) if err != nil { return &val, err } val.IPAddress = ipAddress val.Network = result.Prefix() return &val, nil } // ISP takes an IP address as a netip.Addr and returns a ISP struct and/or // an error. func (r *Reader) ISP(ipAddress netip.Addr) (*ISP, error) { if isISP&r.databaseType == 0 { return nil, InvalidMethodError{"ISP", r.Metadata().DatabaseType} } result := r.mmdbReader.Lookup(ipAddress) var val ISP err := result.Decode(&val) if err != nil { return &val, err } val.IPAddress = ipAddress val.Network = result.Prefix() return &val, nil } // Metadata takes no arguments and returns a struct containing metadata about // the MaxMind database in use by the Reader. func (r *Reader) Metadata() maxminddb.Metadata { return r.mmdbReader.Metadata } // Close unmaps the database file from virtual memory and returns the // resources to the system. func (r *Reader) Close() error { return r.mmdbReader.Close() } golang-github-oschwald-geoip2-golang-v2-2.2.0/reader_test.go000066400000000000000000000357001521103116000236440ustar00rootroot00000000000000package geoip2 import ( "math/rand" "net" "net/netip" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestReader(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") require.NoError(t, err) defer reader.Close() testAddr := netip.MustParseAddr("81.2.69.160") record, err := reader.City(testAddr) require.NoError(t, err) m := reader.Metadata() assert.Equal(t, uint(2), m.BinaryFormatMajorVersion) assert.Equal(t, uint(0), m.BinaryFormatMinorVersion) assert.NotZero(t, m.BuildEpoch) assert.Equal(t, "GeoIP2-City", m.DatabaseType) assert.Equal(t, map[string]string{ "en": "GeoIP2 City Test Database (fake GeoIP2 data, for example purposes only)", "zh": "小型数据库", }, m.Description, ) assert.Equal(t, uint(6), m.IPVersion) assert.Equal(t, []string{"en", "zh"}, m.Languages) assert.NotZero(t, m.NodeCount) assert.Equal(t, uint(28), m.RecordSize) assert.Equal(t, uint(2643743), record.City.GeoNameID) expectedNames := Names{ German: "London", English: "London", Spanish: "Londres", French: "Londres", Japanese: "ロンドン", BrazilianPortuguese: "Londres", Russian: "Лондон", } assert.Equal(t, expectedNames, record.City.Names) assert.Equal(t, uint(6255148), record.Continent.GeoNameID) assert.Equal(t, "EU", record.Continent.Code) expectedContinentNames := Names{ German: "Europa", English: "Europe", Spanish: "Europa", French: "Europe", Japanese: "ヨーロッパ", BrazilianPortuguese: "Europa", Russian: "Европа", SimplifiedChinese: "欧洲", } assert.Equal(t, expectedContinentNames, record.Continent.Names) assert.Equal(t, uint(2635167), record.Country.GeoNameID) assert.False(t, record.Country.IsInEuropeanUnion) assert.Equal(t, "GB", record.Country.ISOCode) expectedCountryNames := Names{ German: "Vereinigtes Königreich", English: "United Kingdom", Spanish: "Reino Unido", French: "Royaume-Uni", Japanese: "イギリス", BrazilianPortuguese: "Reino Unido", Russian: "Великобритания", SimplifiedChinese: "英国", } assert.Equal(t, expectedCountryNames, record.Country.Names) assert.Equal(t, uint16(100), record.Location.AccuracyRadius) assert.InEpsilon(t, 51.5142, *record.Location.Latitude, 1e-10) assert.InEpsilon(t, -0.0931, *record.Location.Longitude, 1e-10) assert.Equal(t, "Europe/London", record.Location.TimeZone) assert.Equal(t, uint(6269131), record.Subdivisions[0].GeoNameID) assert.Equal(t, "ENG", record.Subdivisions[0].ISOCode) expectedSubdivisionNames := Names{ English: "England", BrazilianPortuguese: "Inglaterra", French: "Angleterre", Spanish: "Inglaterra", } assert.Equal(t, expectedSubdivisionNames, record.Subdivisions[0].Names) assert.Equal(t, uint(6252001), record.RegisteredCountry.GeoNameID) assert.False(t, record.RegisteredCountry.IsInEuropeanUnion) assert.Equal(t, "US", record.RegisteredCountry.ISOCode) expectedRegisteredCountryNames := Names{ German: "USA", English: "United States", Spanish: "Estados Unidos", French: "États-Unis", Japanese: "アメリカ合衆国", BrazilianPortuguese: "Estados Unidos", Russian: "США", SimplifiedChinese: "美国", } assert.Equal(t, expectedRegisteredCountryNames, record.RegisteredCountry.Names) assert.False(t, record.RepresentedCountry.IsInEuropeanUnion) // Test Network and IPAddress fields assert.Equal(t, testAddr, record.Traits.IPAddress) assert.True(t, record.Traits.Network.IsValid()) assert.True(t, record.Traits.Network.Contains(testAddr)) } func TestIsAnycast(t *testing.T) { for _, test := range []string{"Country", "City", "Enterprise"} { t.Run(test, func(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-" + test + "-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.City(netip.MustParseAddr("214.1.1.0")) require.NoError(t, err) assert.True(t, record.Traits.IsAnycast) }) } } func TestMetroCode(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.City(netip.MustParseAddr("216.160.83.56")) require.NoError(t, err) assert.Equal(t, uint(819), record.Location.MetroCode) } func TestAnonymousIP(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-Anonymous-IP-Test.mmdb") require.NoError(t, err) defer reader.Close() testAddr := netip.MustParseAddr("1.2.0.0") record, err := reader.AnonymousIP(testAddr) require.NoError(t, err) assert.True(t, record.IsAnonymous) assert.True(t, record.IsAnonymousVPN) assert.False(t, record.IsHostingProvider) assert.False(t, record.IsPublicProxy) assert.False(t, record.IsTorExitNode) assert.False(t, record.IsResidentialProxy) // Test Network and IPAddress fields assert.Equal(t, testAddr, record.IPAddress) assert.True(t, record.Network.IsValid()) assert.True(t, record.Network.Contains(testAddr)) } func TestAnonymousPlus(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP-Anonymous-Plus-Test.mmdb") require.NoError(t, err) defer reader.Close() // Test IP with full data testAddrFull := netip.MustParseAddr("1.2.0.1") record, err := reader.AnonymousPlus(testAddrFull) require.NoError(t, err) assert.True(t, record.IsAnonymous) assert.True(t, record.IsAnonymousVPN) assert.False(t, record.IsHostingProvider) assert.False(t, record.IsPublicProxy) assert.False(t, record.IsTorExitNode) assert.False(t, record.IsResidentialProxy) // Anonymous Plus specific fields assert.Equal(t, uint16(30), record.AnonymizerConfidence) assert.Equal(t, "foo", record.ProviderName) expectedDate := time.Date(2025, 4, 14, 0, 0, 0, 0, time.UTC) assert.Equal(t, expectedDate, record.NetworkLastSeen.Time) // Test Network and IPAddress fields assert.Equal(t, testAddrFull, record.IPAddress) assert.True(t, record.Network.IsValid()) assert.True(t, record.Network.Contains(testAddrFull)) // Test HasData assert.True(t, record.HasData()) // Test IP with minimal data testAddrMinimal := netip.MustParseAddr("1.2.0.0") minRecord, err := reader.AnonymousPlus(testAddrMinimal) require.NoError(t, err) assert.True(t, minRecord.IsAnonymous) assert.True(t, minRecord.IsAnonymousVPN) assert.Equal(t, uint16(0), minRecord.AnonymizerConfidence) assert.Empty(t, minRecord.ProviderName) assert.True(t, minRecord.NetworkLastSeen.IsZero()) assert.True(t, minRecord.HasData()) // Test private IP returns empty data privateAddr := netip.MustParseAddr("192.168.1.1") emptyRecord, err := reader.AnonymousPlus(privateAddr) require.NoError(t, err) assert.False(t, emptyRecord.HasData()) // Network and IPAddress should still be set assert.Equal(t, privateAddr, emptyRecord.IPAddress) assert.True(t, emptyRecord.Network.IsValid()) } func TestASN(t *testing.T) { reader, err := Open("test-data/test-data/GeoLite2-ASN-Test.mmdb") require.NoError(t, err) defer reader.Close() testAddr := netip.MustParseAddr("1.128.0.0") record, err := reader.ASN(testAddr) require.NoError(t, err) assert.Equal(t, uint(1221), record.AutonomousSystemNumber) assert.Equal(t, "Telstra Pty Ltd", record.AutonomousSystemOrganization) // Test Network and IPAddress fields assert.Equal(t, testAddr, record.IPAddress) assert.True(t, record.Network.IsValid()) assert.True(t, record.Network.Contains(testAddr)) } func TestConnectionType(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-Connection-Type-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.ConnectionType(netip.MustParseAddr("1.0.1.0")) require.NoError(t, err) assert.Equal(t, "Cellular", record.ConnectionType) } func TestCountry(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-Country-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.Country(netip.MustParseAddr("81.2.69.160")) require.NoError(t, err) assert.False(t, record.Country.IsInEuropeanUnion) assert.False(t, record.RegisteredCountry.IsInEuropeanUnion) assert.False(t, record.RepresentedCountry.IsInEuropeanUnion) } func TestDomain(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-Domain-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.Domain(netip.MustParseAddr("1.2.0.0")) require.NoError(t, err) assert.Equal(t, "maxmind.com", record.Domain) } func TestEnterprise(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-Enterprise-Test.mmdb") require.NoError(t, err) defer reader.Close() testAddr1 := netip.MustParseAddr("74.209.24.0") record, err := reader.Enterprise(testAddr1) require.NoError(t, err) assert.Equal(t, uint8(11), record.City.Confidence) assert.Equal(t, uint(14671), record.Traits.AutonomousSystemNumber) assert.Equal(t, "FairPoint Communications", record.Traits.AutonomousSystemOrganization) assert.Equal(t, "Cable/DSL", record.Traits.ConnectionType) assert.Equal(t, "frpt.net", record.Traits.Domain) assert.InEpsilon(t, float64(0.34), record.Traits.StaticIPScore, 1e-10) testAddr2 := netip.MustParseAddr("149.101.100.0") record, err = reader.Enterprise(testAddr2) require.NoError(t, err) assert.Equal(t, uint(6167), record.Traits.AutonomousSystemNumber) assert.Equal(t, "CELLCO-PART", record.Traits.AutonomousSystemOrganization) assert.Equal(t, "Verizon Wireless", record.Traits.ISP) assert.Equal(t, "310", record.Traits.MobileCountryCode) assert.Equal(t, "004", record.Traits.MobileNetworkCode) // Test Network and IPAddress fields (for the second lookup) assert.Equal(t, testAddr2, record.Traits.IPAddress) assert.True(t, record.Traits.Network.IsValid()) assert.True(t, record.Traits.Network.Contains(testAddr2)) } func TestISP(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-ISP-Test.mmdb") require.NoError(t, err) defer reader.Close() record, err := reader.ISP(netip.MustParseAddr("149.101.100.0")) require.NoError(t, err) assert.Equal(t, uint(6167), record.AutonomousSystemNumber) assert.Equal(t, "CELLCO-PART", record.AutonomousSystemOrganization) assert.Equal(t, "Verizon Wireless", record.ISP) assert.Equal(t, "310", record.MobileCountryCode) assert.Equal(t, "004", record.MobileNetworkCode) assert.Equal(t, "Verizon Wireless", record.Organization) } // This ensures the compiler does not optimize away the function call. var cityResult *City func BenchmarkCity(b *testing.B) { db, err := Open("GeoLite2-City.mmdb") if err != nil { b.Fatal(err) } defer db.Close() //nolint:gosec // this is just a benchmark r := rand.New(rand.NewSource(0)) var city *City ip := make(net.IP, 4) for b.Loop() { randomIPv4Address(r, ip) addr, _ := netip.AddrFromSlice(ip) city, err = db.City(addr) if err != nil { b.Fatal(err) } } cityResult = city } // This ensures the compiler does not optimize away the function call. var asnResult *ASN func BenchmarkASN(b *testing.B) { db, err := Open("GeoLite2-ASN.mmdb") if err != nil { b.Fatal(err) } defer db.Close() //nolint:gosec // this is just a benchmark r := rand.New(rand.NewSource(0)) var asn *ASN ip := make(net.IP, 4) for b.Loop() { randomIPv4Address(r, ip) addr, _ := netip.AddrFromSlice(ip) asn, err = db.ASN(addr) if err != nil { b.Fatal(err) } } asnResult = asn } func randomIPv4Address(r *rand.Rand, ip net.IP) { num := r.Uint32() ip[0] = byte(num >> 24) ip[1] = byte(num >> 16) ip[2] = byte(num >> 8) ip[3] = byte(num) } func TestIsZero(t *testing.T) { reader, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb") require.NoError(t, err) defer reader.Close() // Test with an IP that has data ipWithData := netip.MustParseAddr("81.2.69.160") record, err := reader.City(ipWithData) require.NoError(t, err) assert.True(t, record.HasData(), "Record with data should have data") // Test with an IP that has no data (private IP) ipWithoutData := netip.MustParseAddr("192.168.1.1") emptyRecord, err := reader.City(ipWithoutData) require.NoError(t, err) assert.False(t, emptyRecord.HasData(), "Record without data should not have data") // Verify IPAddress and Network are always set, even when no data is found assert.Equal( t, ipWithData, record.Traits.IPAddress, "IPAddress should be set for records with data", ) assert.NotEqual( t, netip.Prefix{}, record.Traits.Network, "Network should be set for records with data", ) assert.Equal( t, ipWithoutData, emptyRecord.Traits.IPAddress, "IPAddress should be set even when no data found", ) assert.NotEqual( t, netip.Prefix{}, emptyRecord.Traits.Network, "Network should be set even when no data found", ) // Test Names HasData var emptyNames Names assert.False(t, emptyNames.HasData(), "Empty Names should not have data") nonEmptyNames := Names{English: "Test"} assert.True(t, nonEmptyNames.HasData(), "Names with data should have data") // Test other struct types var emptyASN ASN assert.False(t, emptyASN.HasData(), "Empty ASN should not have data") nonEmptyASN := ASN{AutonomousSystemNumber: 123} assert.True(t, nonEmptyASN.HasData(), "ASN with data should have data") } func TestIPAddressAndNetworkAlwaysSet(t *testing.T) { // Test that IPAddress and Network are always set for ASN lookups asnReader, err := Open("test-data/test-data/GeoLite2-ASN-Test.mmdb") require.NoError(t, err) defer asnReader.Close() // Test with an IP that has ASN data ipWithData := netip.MustParseAddr("1.128.0.0") asnRecord, err := asnReader.ASN(ipWithData) require.NoError(t, err) assert.Equal(t, ipWithData, asnRecord.IPAddress, "ASN IPAddress should be set") assert.NotEqual(t, netip.Prefix{}, asnRecord.Network, "ASN Network should be set") // Test with an IP that has no ASN data (private IP) ipWithoutData := netip.MustParseAddr("192.168.1.1") asnEmptyRecord, err := asnReader.ASN(ipWithoutData) require.NoError(t, err) assert.Equal( t, ipWithoutData, asnEmptyRecord.IPAddress, "ASN IPAddress should be set even when no data found", ) assert.NotEqual( t, netip.Prefix{}, asnEmptyRecord.Network, "ASN Network should be set even when no data found", ) } func TestAllStructsHaveHasData(t *testing.T) { // Ensure all result structs have HasData methods var city City var country Country var enterprise Enterprise var anonymousIP AnonymousIP var anonymousPlus AnonymousPlus var asn ASN var connectionType ConnectionType var domain Domain var isp ISP var names Names // These should all compile and return false for zero values (no data) assert.False(t, city.HasData()) assert.False(t, country.HasData()) assert.False(t, enterprise.HasData()) assert.False(t, anonymousIP.HasData()) assert.False(t, anonymousPlus.HasData()) assert.False(t, asn.HasData()) assert.False(t, connectionType.HasData()) assert.False(t, domain.HasData()) assert.False(t, isp.HasData()) assert.False(t, names.HasData()) }