From 166748dcf25afb56fd3a149b5e96cad7517bdedd Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Tue, 17 Jan 2023 22:05:09 +0700 Subject: [PATCH] Replace Geolix with Locus (#2362) This PR replaces geolix with locus to simplify self-hosted setup. locus can auto-update maxmind dbs which are recommended for self-hosters if they want city-level geolocation. locus is also a bit faster. This PR also uses a test mmdb file from https://github.com/maxmind/MaxMind-DB for e2e geolocation tests without stubs. --- .gitignore | 1 + config/.env.test | 1 + config/runtime.exs | 47 +++-- config/test.exs | 39 ---- lib/mix/tasks/download_country_database.ex | 2 +- lib/plausible/application.ex | 10 + lib/plausible/geo.ex | 189 ++++++++++++++++++ lib/plausible/ingestion/event.ex | 2 +- lib/plausible/ingestion/geolocation.ex | 38 ++-- .../controllers/api/external_controller.ex | 9 +- .../controllers/stats_controller.ex | 13 +- mix.exs | 3 +- mix.lock | 4 +- .../api/external_controller_test.exs | 34 ++-- test/priv/GeoLite2-City-Test.mmdb | Bin 0 -> 20809 bytes test/priv/README.md | 1 + 16 files changed, 287 insertions(+), 106 deletions(-) create mode 100644 lib/plausible/geo.ex create mode 100644 test/priv/GeoLite2-City-Test.mmdb create mode 100644 test/priv/README.md diff --git a/.gitignore b/.gitignore index c38e85477..11ba8cc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ plausible-report.xml # Geolocation databases /priv/geodb/*.mmdb +/priv/geodb/*.mmdb.gz # Auto-generated tracker files /priv/tracker/js/*.js diff --git a/config/.env.test b/config/.env.test index 4f5107ae5..cb773330e 100644 --- a/config/.env.test +++ b/config/.env.test @@ -11,3 +11,4 @@ SELFHOST=false SITE_LIMIT=3 HCAPTCHA_SITEKEY=test HCAPTCHA_SECRET=scottiger +IP_GEOLOCATION_DB=test/priv/GeoLite2-City-Test.mmdb diff --git a/config/runtime.exs b/config/runtime.exs index cfb89a941..53ed3ca92 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -142,11 +142,13 @@ geolite2_country_db = get_var_from_path_or_env( config_dir, "GEOLITE2_COUNTRY_DB", - Application.app_dir(:plausible, "/priv/geodb/dbip-country.mmdb") + Application.app_dir(:plausible, "/priv/geodb/dbip-country.mmdb.gz") ) ip_geolocation_db = get_var_from_path_or_env(config_dir, "IP_GEOLOCATION_DB", geolite2_country_db) geonames_source_file = get_var_from_path_or_env(config_dir, "GEONAMES_SOURCE_FILE") +maxmind_license_key = get_var_from_path_or_env(config_dir, "MAXMIND_LICENSE_KEY") +maxmind_edition = get_var_from_path_or_env(config_dir, "MAXMIND_EDITION", "GeoLite2-City") if System.get_env("DISABLE_AUTH") do require Logger @@ -433,17 +435,38 @@ config :kaffy, ] ] -if config_env() != :test do - config :geolix, - databases: [ - %{ - id: :geolocation, - adapter: Geolix.Adapter.MMDB2, - source: ip_geolocation_db, - result_as: :raw - } - ] -end +geo_opts = + cond do + maxmind_license_key -> + [ + license_key: maxmind_license_key, + edition: maxmind_edition, + async: true + ] + + ip_geolocation_db -> + [path: ip_geolocation_db] + + true -> + raise """ + Missing geolocation database configuration. + + Please set the IP_GEOLOCATION_DB environment value to the location of + your IP geolocation .mmdb file: + + IP_GEOLOCATION_DB=/etc/plausible/dbip-city.mmdb + + Or authenticate with MaxMind by + configuring MAXMIND_LICENSE_KEY and (optionally) MAXMIND_EDITION environment + variables: + + MAXMIND_LICENSE_KEY=LNpsJCCKPis6XvBP + MAXMIND_EDITION=GeoLite2-City # this is the default edition + + """ + end + +config :plausible, Plausible.Geo, geo_opts if geonames_source_file do config :location, :geonames_source_file, geonames_source_file diff --git a/config/test.exs b/config/test.exs index df2aba638..093cb8319 100644 --- a/config/test.exs +++ b/config/test.exs @@ -24,45 +24,6 @@ config :plausible, :google, config :bamboo, :refute_timeout, 10 -geolix_sample_lookup = %{ - city: %{geoname_id: 2_988_507, names: %{en: "Paris"}}, - continent: %{code: "EU", geoname_id: 6_255_148, names: %{en: "Europe"}}, - country: %{ - geoname_id: 3_017_382, - is_in_european_union: true, - iso_code: "FR", - names: %{en: "France"} - }, - ip_address: {2, 2, 2, 2}, - location: %{ - latitude: 48.8566, - longitude: 2.35222, - time_zone: "Europe/Paris", - weather_code: "FRXX0076" - }, - postal: %{code: "75000"}, - subdivisions: [ - %{geoname_id: 3_012_874, iso_code: "IDF", names: %{en: "Île-de-France"}}, - %{geoname_id: 2_968_815, iso_code: "75", names: %{en: "Paris"}} - ] -} - -config :geolix, - databases: [ - %{ - id: :geolocation, - adapter: Geolix.Adapter.Fake, - data: %{ - {1, 1, 1, 1} => %{country: %{iso_code: "US"}}, - {2, 2, 2, 2} => geolix_sample_lookup, - {1, 1, 1, 1, 1, 1, 1, 1} => %{country: %{iso_code: "US"}}, - {0, 0, 0, 0} => %{country: %{iso_code: "ZZ"}, city: %{geoname_id: 123_123}}, - {0, 0, 0, 1} => %{country: %{iso_code: "XX"}, subdivisions: [%{iso_code: "IDF"}]}, - {0, 0, 0, 2} => %{country: %{iso_code: "T1"}, subdivisions: [%{}, %{iso_code: "IDF"}]} - } - } - ] - config :plausible, session_timeout: 0, http_impl: Plausible.HTTPClient.Mock, diff --git a/lib/mix/tasks/download_country_database.ex b/lib/mix/tasks/download_country_database.ex index 5a2027af7..d81855d6e 100644 --- a/lib/mix/tasks/download_country_database.ex +++ b/lib/mix/tasks/download_country_database.ex @@ -34,7 +34,7 @@ defmodule Mix.Tasks.DownloadCountryDatabase do if res.status_code == 200 do File.mkdir("priv/geodb") - File.write!("priv/geodb/dbip-country.mmdb", res.body) + File.write!("priv/geodb/dbip-country.mmdb.gz", res.body) Logger.info("Downloaded and saved the database successfully") else Logger.error("Unable to download and save the database. Response: #{inspect(res)}") diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex index 1a6c36d5d..4e2a9aed0 100644 --- a/lib/plausible/application.ex +++ b/lib/plausible/application.ex @@ -30,9 +30,14 @@ defmodule Plausible.Application do ] opts = [strategy: :one_for_one, name: Plausible.Supervisor] + setup_sentry() setup_opentelemetry() + + setup_geolocation() Location.load_all() + Plausible.Geo.await_loader() + Supervisor.start_link(children, opts) end @@ -119,4 +124,9 @@ defmodule Plausible.Application do OpentelemetryEcto.setup([:plausible, :clickhouse_repo]) OpentelemetryOban.setup() end + + defp setup_geolocation do + opts = Application.fetch_env!(:plausible, Plausible.Geo) + :ok = Plausible.Geo.load_db(opts) + end end diff --git a/lib/plausible/geo.ex b/lib/plausible/geo.ex new file mode 100644 index 000000000..4d4fff7f4 --- /dev/null +++ b/lib/plausible/geo.ex @@ -0,0 +1,189 @@ +defmodule Plausible.Geo do + @moduledoc """ + This module provides an API for fetching IP geolocation. + """ + + require Logger + + @db :geolocation + + @doc """ + Starts the geodatabase loading process. Two modes are supported: local file + and MaxMind license key. + + ## Options + + * `:path` - the path to the .mmdb database local file. When present, + `:license_key` and `:edition` are not required. + + * `:license_key` - the [license key](https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key) + from MaxMind to authenticate requests to MaxMind. + + * `:edition` - the name of the MaxMind database to be downloaded from MaxMind + servers. Defaults to `GeoLite2-City`. + + * `:async` - when used, configures the database loading to run + asynchronously. + + ## Examples + + Loading from a local file: + + iex> load_db(path: "/etc/plausible/dbip-city.mmdb") + :ok + + Downloading a MaxMind DB (this license key is no longer active): + + iex> load_db(license_key: "LNpsJCCKPis6XvBP", edition: "GeoLite2-City", async: true) + :ok + + """ + def load_db(opts) do + cond do + license_key = opts[:license_key] -> + edition = opts[:edition] || "GeoLite2-City" + :ok = :locus.start_loader(@db, {:maxmind, edition}, license_key: license_key) + + path = opts[:path] -> + :ok = :locus.start_loader(@db, path) + + true -> + raise "failed to load geolocation db: need :path or :license_key to be provided" + end + + unless opts[:async] do + {:ok, _version} = :locus.await_loader(@db) + end + + :ok + end + + @doc """ + Waits for the database to start after calling `load_db/1` with the async option. + """ + def await_loader, do: :locus.await_loader(@db) + + @doc """ + Returns geodatabase type. + + Used for deciding whether to show the DB-IP disclaimer or not. + + ## Examples + + In the case of a DB-IP database: + + iex> database_type() + "DBIP-City-Lite" + + In the case of a MaxMind database: + + iex> database_type() + "GeoLite2-City" + + """ + def database_type do + case :locus.get_info(@db, :metadata) do + {:ok, %{database_type: type}} -> type + _other -> nil + end + end + + @doc """ + Looks up geo info about an IP address. + + ## Examples + + iex> lookup("8.7.6.5") + %{ + "city" => %{ + "geoname_id" => 5349755, + "names" => %{ + "de" => "Fontana", + "en" => "Fontana", + "ja" => "フォンタナ", + "ru" => "Фонтана" + } + }, + "continent" => %{ + "code" => "NA", + "geoname_id" => 6255149, + "names" => %{ + "de" => "Nordamerika", + "en" => "North America", + "es" => "Norteamérica", + "fr" => "Amérique du Nord", + "ja" => "北アメリカ", + "pt-BR" => "América do Norte", + "ru" => "Северная Америка", + "zh-CN" => "北美洲" + } + }, + "country" => %{ + "geoname_id" => 6252001, + "iso_code" => "US", + "names" => %{ + "de" => "Vereinigte Staaten", + "en" => "United States", + "es" => "Estados Unidos", + "fr" => "États Unis", + "ja" => "アメリカ", + "pt-BR" => "EUA", + "ru" => "США", + "zh-CN" => "美国" + } + }, + "location" => %{ + "accuracy_radius" => 50, + "latitude" => 34.1211, + "longitude" => -117.4362, + "metro_code" => 803, + "time_zone" => "America/Los_Angeles" + }, + "postal" => %{"code" => "92336"}, + "registered_country" => %{ + "geoname_id" => 6252001, + "iso_code" => "US", + "names" => %{ + "de" => "Vereinigte Staaten", + "en" => "United States", + "es" => "Estados Unidos", + "fr" => "États Unis", + "ja" => "アメリカ", + "pt-BR" => "EUA", + "ru" => "США", + "zh-CN" => "美国" + } + }, + "subdivisions" => [ + %{ + "geoname_id" => 5332921, + "iso_code" => "CA", + "names" => %{ + "de" => "Kalifornien", + "en" => "California", + "es" => "California", + "fr" => "Californie", + "ja" => "カリフォルニア州", + "pt-BR" => "Califórnia", + "ru" => "Калифорния", + "zh-CN" => "加州" + } + } + ] + } + + """ + def lookup(ip_address) do + case :locus.lookup(@db, ip_address) do + {:ok, entry} -> + entry + + :not_found -> + nil + + {:error, reason} -> + Logger.error("failed to lookup ip address: " <> inspect(reason)) + nil + end + end +end diff --git a/lib/plausible/ingestion/event.ex b/lib/plausible/ingestion/event.ex index 84d79ed9c..528a070f1 100644 --- a/lib/plausible/ingestion/event.ex +++ b/lib/plausible/ingestion/event.ex @@ -168,7 +168,7 @@ defmodule Plausible.Ingestion.Event do end defp put_geolocation(%__MODULE__{} = event) do - result = Plausible.Ingestion.Geolocation.lookup(event.request.remote_ip) + result = Plausible.Ingestion.Geolocation.lookup(event.request.remote_ip) || %{} update_attrs(event, result) end diff --git a/lib/plausible/ingestion/geolocation.ex b/lib/plausible/ingestion/geolocation.ex index f30f30af1..43a564de6 100644 --- a/lib/plausible/ingestion/geolocation.ex +++ b/lib/plausible/ingestion/geolocation.ex @@ -1,33 +1,39 @@ defmodule Plausible.Ingestion.Geolocation do @moduledoc false - alias Plausible.Ingestion.CityOverrides - def lookup(remote_ip) do - result = Geolix.lookup(remote_ip, where: :geolocation) + def lookup(ip_address) do + case Plausible.Geo.lookup(ip_address) do + %{} = entry -> + country_code = + entry + |> get_in(["country", "iso_code"]) + |> ignore_unknown_country() - country_code = - get_in(result, [:country, :iso_code]) - |> ignore_unknown_country() + city_geoname_id = country_code && get_in(entry, ["city", "geoname_id"]) + city_geoname_id = Plausible.Ingestion.CityOverrides.get(city_geoname_id, city_geoname_id) - city_geoname_id = country_code && get_in(result, [:city, :geoname_id]) - city_geoname_id = CityOverrides.get(city_geoname_id, city_geoname_id) + %{ + country_code: country_code, + subdivision1_code: subdivision1_code(country_code, entry), + subdivision2_code: subdivision2_code(country_code, entry), + city_geoname_id: city_geoname_id + } - %{ - country_code: country_code, - subdivision1_code: subdivision1_code(country_code, result), - subdivision2_code: subdivision2_code(country_code, result), - city_geoname_id: city_geoname_id - } + nil -> + nil + end end - defp subdivision1_code(country_code, %{subdivisions: [%{iso_code: iso_code} | _rest]}) + defp subdivision1_code(country_code, %{"subdivisions" => [%{"iso_code" => iso_code} | _rest]}) when not is_nil(country_code) do country_code <> "-" <> iso_code end defp subdivision1_code(_, _), do: nil - defp subdivision2_code(country_code, %{subdivisions: [_first, %{iso_code: iso_code} | _rest]}) + defp subdivision2_code(country_code, %{ + "subdivisions" => [_first, %{"iso_code" => iso_code} | _rest] + }) when not is_nil(country_code) do country_code <> "-" <> iso_code end diff --git a/lib/plausible_web/controllers/api/external_controller.ex b/lib/plausible_web/controllers/api/external_controller.ex index 29e5dd906..21ccf0a69 100644 --- a/lib/plausible_web/controllers/api/external_controller.ex +++ b/lib/plausible_web/controllers/api/external_controller.ex @@ -86,14 +86,7 @@ defmodule PlausibleWeb.Api.ExternalController do |> Keyword.take([:version, :commit, :created, :tags]) |> Map.new() - geo_database = - case Geolix.metadata(where: :geolocation) do - %{database_type: type} -> - type - - _ -> - "(not configured)" - end + geo_database = Plausible.Geo.database_type() || "(not configured)" json(conn, %{ geo_database: geo_database, diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 2e719c72a..bb35bdb8b 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -316,14 +316,13 @@ defmodule PlausibleWeb.StatsController do end defp is_dbip() do - if Application.get_env(:plausible, :is_selfhost) do - case Geolix.metadata(where: :geolocation) do - %{database_type: type} -> + is_or_nil = + if Application.get_env(:plausible, :is_selfhost) do + if type = Plausible.Geo.database_type() do String.starts_with?(type, "DBIP") - - _ -> - false + end end - end + + !!is_or_nil end end diff --git a/mix.exs b/mix.exs index 3c0b4e1d4..4fe851e16 100644 --- a/mix.exs +++ b/mix.exs @@ -80,9 +80,8 @@ defmodule Plausible.MixProject do {:floki, "~> 0.32.0", only: :test}, {:fun_with_flags, "~> 1.9.0"}, {:fun_with_flags_ui, "~> 0.8"}, + {:locus, "~> 2.3"}, {:gen_cycle, "~> 1.0"}, - {:geolix, "~> 2.0"}, - {:geolix_adapter_mmdb2, "~> 0.6.0"}, {:hackney, "~> 1.8"}, {:hammer, "~> 6.0"}, {:httpoison, "~> 1.4"}, diff --git a/mix.lock b/mix.lock index 9addbb7fb..18ebc549d 100644 --- a/mix.lock +++ b/mix.lock @@ -49,8 +49,6 @@ "fun_with_flags_ui": {:hex, :fun_with_flags_ui, "0.8.0", "70587e344ba2035516a639e7bd8cb2ce8d54ee6c5f5dfd3e4e6f6a776e14ac1d", [:mix], [{:cowboy, ">= 2.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.8", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 2.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "95e1e5afae77e259a4a43f930f3da97ff3c388a72d4622f7848cb7ee34de6338"}, "gen_cycle": {:hex, :gen_cycle, "1.0.3", "ee0117b9c65814f58827abfe84967d4cb5380db0fbac992358e5b669b3ebcbcb", [:rebar3], [], "hexpm", "fffdadf7fb73de75e3429eb8a6982768e7240803561942602350b689991749e4"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, - "geolix": {:hex, :geolix, "2.0.0", "7e65764bedfc98d08a3ddb24c417657c9d438eff163280b45fbb7de289626acd", [:mix], [], "hexpm", "8742bf588ed0bb7def2c443204d09d355990846c6efdff96ded66aac24c301df"}, - "geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.6.0", "6ab9dbf6ea395817aa1fd2597be25d0dda1853c7f664e62716e47728d18bc4f9", [:mix], [{:geolix, "~> 2.0", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm", "06ff962feae8a310cffdf86b74bfcda6e2d0dccb439bb1f62df2b657b1c0269b"}, "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, "gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"}, "grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~>1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~>0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~>0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~>0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"}, @@ -67,6 +65,7 @@ "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "kaffy": {:hex, :kaffy, "0.9.0", "bef34c9729f6a3af4d0dea8eede8bcb9e11371a83ac9a8b393991bce81839517", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "d18ff57b8e68feb433aed11e71510cd357abc7034e75358af5deff7d0d4c6ed3"}, "location": {:git, "https://github.com/plausible/location.git", "8faf4f08b06905adde43554dc1d9d35675654816", []}, + "locus": {:hex, :locus, "2.3.6", "c9f53fd5df872fca66a54dc0aa2f8b2d3640388e56a0c39a741be0df6d8854bf", [:rebar3], [{:tls_certificate_check, "~> 1.9", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "6087aa9a69673e7011837fb4b3d7f756560adde76892c32f5f93904ee30064e2"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, @@ -75,7 +74,6 @@ "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, - "mmdb2_decoder": {:hex, :mmdb2_decoder, "3.0.1", "78e3aedde88035c6873ada5ceaf41b7f15a6259ed034e0eaca72ccfa937798f0", [:mix], [], "hexpm", "316af0f388fac824782d944f54efe78e7c9691bbbdb0afd5cccdd0510adf559d"}, "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, "nanoid": {:hex, :nanoid, "2.0.5", "1d2948d8967ef2d948a58c3fef02385040bd9823fc6394bd604b8d98e5516b22", [:mix], [], "hexpm", "956e8876321104da72aa48770539ff26b36b744cd26753ec8e7a8a37e53d5f58"}, "nimble_options": {:hex, :nimble_options, "0.5.1", "5c166f7669e40333191bea38e3bd3811cc13f459f1e4be49e89128a21b5d8c4d", [:mix], [], "hexpm", "d176cf7baa4fef0ceb301ca3eb8b55bd7de3e45f489c4f8b4f2849f1f114ef3e"}, diff --git a/test/plausible_web/controllers/api/external_controller_test.exs b/test/plausible_web/controllers/api/external_controller_test.exs index 061be8b57..e63fb40b4 100644 --- a/test/plausible_web/controllers/api/external_controller_test.exs +++ b/test/plausible_web/controllers/api/external_controller_test.exs @@ -651,7 +651,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do assert event.referrer == "" end - # Fake data is set up in config/test.exs + # Fake geo is loaded from test/priv/GeoLite2-City-Test.mmdb test "looks up location data from the ip address", %{conn: conn, domain: domain} do params = %{ name: "pageview", @@ -660,15 +660,15 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do } conn - |> put_req_header("x-forwarded-for", "2.2.2.2") + |> put_req_header("x-forwarded-for", "2.125.160.216") |> post("/api/event", params) pageview = get_event(domain) - assert pageview.country_code == "FR" - assert pageview.subdivision1_code == "FR-IDF" - assert pageview.subdivision2_code == "FR-75" - assert pageview.city_geoname_id == 2_988_507 + assert pageview.country_code == "GB" + assert pageview.subdivision1_code == "GB-ENG" + assert pageview.subdivision2_code == "GB-WBK" + assert pageview.city_geoname_id == 2_655_045 end test "ignores unknown country code ZZ", %{conn: conn, domain: domain} do @@ -736,7 +736,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do } conn - |> put_req_header("x-forwarded-for", "1.1.1.1:123") + |> put_req_header("x-forwarded-for", "216.160.83.56:123") |> post("/api/event", params) pageview = get_event(domain) @@ -752,12 +752,12 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do } conn - |> put_req_header("x-forwarded-for", "1:1:1:1:1:1:1:1") + |> put_req_header("x-forwarded-for", "2001:218:1:1:1:1:1:1") |> post("/api/event", params) pageview = get_event(domain) - assert pageview.country_code == "US" + assert pageview.country_code == "JP" end test "works with ipv6 with a port number in x-forwarded-for", %{conn: conn, domain: domain} do @@ -768,12 +768,12 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do } conn - |> put_req_header("x-forwarded-for", "[1:1:1:1:1:1:1:1]:123") + |> put_req_header("x-forwarded-for", "[2001:218:1:1:1:1:1:1]:123") |> post("/api/event", params) pageview = get_event(domain) - assert pageview.country_code == "US" + assert pageview.country_code == "JP" end test "uses cloudflare's special header for client IP address if present", %{ @@ -788,7 +788,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do conn |> put_req_header("x-forwarded-for", "0.0.0.0") - |> put_req_header("cf-connecting-ip", "1.1.1.1") + |> put_req_header("cf-connecting-ip", "216.160.83.56") |> post("/api/event", params) pageview = get_event(domain) @@ -808,7 +808,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do conn |> put_req_header("x-forwarded-for", "0.0.0.0") - |> put_req_header("b-forwarded-for", "1.1.1.1,9.9.9.9") + |> put_req_header("b-forwarded-for", "216.160.83.56,9.9.9.9") |> post("/api/event", params) pageview = get_event(domain) @@ -827,7 +827,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do } conn - |> put_req_header("forwarded", "by=0.0.0.0;for=1.1.1.1;host=somehost.com;proto=https") + |> put_req_header("forwarded", "by=0.0.0.0;for=216.160.83.56;host=somehost.com;proto=https") |> post("/api/event", params) pageview = get_event(domain) @@ -845,13 +845,13 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do conn |> put_req_header( "forwarded", - "by=0.0.0.0;for=\"[1:1:1:1:1:1:1:1]\",for=0.0.0.0;host=somehost.com;proto=https" + "by=0.0.0.0;for=\"[2001:218:1:1:1:1:1:1]\",for=0.0.0.0;host=somehost.com;proto=https" ) |> post("/api/event", params) pageview = get_event(domain) - assert pageview.country_code == "US" + assert pageview.country_code == "JP" end test "URL is decoded", %{conn: conn, domain: domain} do @@ -1042,7 +1042,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do conn |> put_req_header("user-agent", @user_agent) - |> put_req_header("x-forwarded-for", "982.32.12.1") + |> put_req_header("x-forwarded-for", "82.32.12.1") |> post("/api/event", params) [one, two] = get_events(domain) diff --git a/test/priv/GeoLite2-City-Test.mmdb b/test/priv/GeoLite2-City-Test.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..ed9adc6547fd91b24329c903ace3cc60cab8a408 GIT binary patch literal 20809 zcmZ{q2Yggj_Q&tM_XZS2r7EH@0fYfT6zrAM$)pie04c^Hc_ahLOqj_CVRzjWMS4++ zf&!t1A`*)9qK>+{wu!E~?y8H1BxGIJvUXSf|L!^WWoG>U|HFrT-#z!9_s%`{-1?pn zHX-cGgwTMsLg+%+!a^9FvNO6cx-!l~k;Ous*V3m6wNx-%|fT+FzH(Sy;G(TmZW z(TCBOaVg_6MnA^ojQ)%(7*{d|Fa|Olj0DCY#$d(}##M}=jA0BXV>n|3<7&o8#wf-$ zjM0p18P_q!Fvc>jXWYQJk#Q5_X2vayTN$@8ZfD%VxRY@gV;m!qk;F)5q%cw$X$%)5 zo#AF=FftigjBG{@V>~05aW|U(_d?`xDxXonC}d1vOk_-A+{2j6n8KLKn8vu5QN*~9 z;SoZ-E5vl5R)}I2mM}~R9}r>&r%I73qbQb<>}BChf)t2Zlqbp=6$~H4FGOXuZ$QHVcAg*7aMb#*ZCRUzsDq#mRq z;-P5C`Fxj$g;>CO3!{09gjgI+EfHdAG_{QNmNQlev69svi57X3^Hwod3$cds&cltQ z<&VLngF-ycsdZ7k^+G(s!Y3IU7#kUz7!3q+!Db=0uyAWsxJ`)dEQD)hu^rL8okHx2 zrgpP|PYJOHChZsEX(77x6JoCr7xd)3CdNL(1?Wo$NL`kaw|<6kD0!$Q0eO}!|@OI+knjF(~VbbjnBj6V|~6tAITZwc{w zRQ(Mh-ekQ9qq#E=52NS3&G-xB9fHhw4>$WS3;!y_`<;5?Z$kVXd96Zx5EXvNB|l>P zgK>uOG2;_P3*#(7KBx^h`&5W_x|uk~=wSSlAWPxDg!q>bpK;#58J|b^=Y7xkAzI`|F7gwh`&c1<#+ZfP_zM6h{|Y3_c~96z2^$ErY#Kv{ zaHy~uz-7W_XJHpcSH^ja^BLV37ceelbZ1<|xR`MXqX%w%sj&6rR4+zvMju9Bf^-)C zE7&-MWy*Fr32ps_?Fv@7k}-fWuv5?G;F1Z#Hi+{EGloQqT-Eu6VNsz|*oH?_BZTd0 z)*IO=RF>JU5w_8scP-;O#u(UnBR_qtuwBo>8wjdKRF#{A?Pkuqr8CcVE9>1RY_~^+ zcd+nIVY`bJ#zpfIg)ONwh5Oo4I#ae(VM~K8d93bYqzjvy?~=jDj4EUaTQ&=GqQdd4 zm&>?YT*Ote<#WjbMj>MYV+6t>yXR6y8*T%;;046zaS3)>vdn;Xrm z<~*MNE*?oK%W9tgY!7geKQd|x;fLkyn(Y!7p40b?O!5kcH1Y)jarrHo~a z<%|`Km5fIik1|#fZ!L54> zTMO>_jj){sI@qLEMw_s;bIEgrOJIw9&_7Y~-@^7Or~bwGjL<{%;?Gg!D`ESB^S+GB z`wt7hCiJ|MQ{TcCyRdx+y`O~bdlvq{_>o{e{l6%w3ER&o`K#y|0e&ICGEE@QW~F4F z6zU8k_AXi%lNkdkW8;K3Kp|0RHv)GE?Iz$>q20_SZy{JF z-NwS(V})oRq1`F8yEtzg!Fpa2^4vm82GWFjIPy+SMEk}A)` zDOHDJPI1rCc>W_JW(ch`dKcaXYF?qu#F<8&&}N~Uzhhf?|EKZ(PviZc<`-Heid1ot z*+L8OgMx%!=y7<87DAD^Lc5<+a|pe+a;lm!kI+Z?`uBLcF0?;D?}X4E0G<@uAAw~; zs{tMsS}pLP(CS#dp4E{jpZ5@BKB4b*oLT^d#X?)isYRTpe71yzJpWy)acVgf)(C9{ z@TkyMvhWeYrE56F^PjewQdVtcxUCb~V_f8M!euH@c>dF#V4<>e0~GcMZ6mNzrv`>Hzp|t>Q zLOaVPTM3p)?JPV;uxV&C0>`hP|PwxwbD}{b3 zaJkSg<0AbCDwg#AEWCo?xQ8#+6Q^;gt7Qvx> zhWSswTj=9CFE>shkA?XJHQMNfP?#$83BWx9Ud}5b zSS{^g;dFvkY4mVCD0CAj7y1mqEA&z>Sr%6k^B)FWeHKB@ReA*!DuwRjJb#?RY!(Ix zgH?Q1LE(2o4*_$9em@tPL$LZ0&wu(n5?UUV^WPtZ{s&fgfM8Xc`=(wScNffm#=}B? z5I`S$2F=nn&HguVb+F7$=K5~1__r!S_vSlxFiKW`af@NrJ9fWo6fU&*OQ z2t#mB>7G@L)r3CEvb9jyAan$@zFz1&|LNcn z3%3$1%eF(|sL&gM{X*XX>=pV>;3=W+VujrVt8?vP;nRexRM&1|;XZ=ZoetnGhlGBR z^PY*zJIul(EQGJ6CyqhkIibU6`YEBGVBtwZzh60Zn(-{b8nd2ypxGw{C9Uj^P0`fI?OLVukV-XK^VA;Q9Df>pn_q42KI|H66i5Uft} z9t;0UuzdYDD10pRzXKl${R1xYA;GfjA1pjW7**a( z_^;6a349~;Pl2z5{x9Hjp?}5-|0YdnGFd^2SWd)~N=^LYMKH8T3+W`l%r z1u#$;S8|a7gyDEPBH3^-5(sK0F$P0ns4#|b-cCQ zlwa;*;W&czpd=`y3L}~GQsNZSSm+|Ca5LOcC=*5oaE~xDfjnVk0po>{%?dej3b`!2 zn_#s=J`^Shqk!`Y32OamOl09C!tl2^H5m#XVN3z;6~EueowIO$@8D_$2cM8KjT4R)UiT6!KyTmpT_(+ z14|7>A*7Oc+Nv z?(4--E*6gz;C-d!Jz0`F9q6 zKu}vf<0B|^2;(2XC&DYu2W*^8x2f=#! zAf$#1`(R+Gun*xPR}oZyun%LQlb}vA>?4pGCG1yo-bl`y%c*M^qvM3vAvI3e#{jno z`&a<40j~#8u^R|#gtp(r!kYDatOw;H@P8sQVShj!$pCSj=txT zO3bv>P{3d5fdXHdNoLUDN}3UH+?_PjGrpyMM@#*Qmij#{_4O_FD^n}0MkVDTKM=}_ ztd8t&u8EwAoQ^c5&MO<0oRik}RAbx0{jOqv$X6ApZozY$H*8Gz2K`0!WLFYDbK(Ru zV0wMt(keVFGkgeFNEesEh@3*7x5_MWWO{w2CH@K+lbMI2eh1_wewdY+=dbpJD$G$3 zz`(4Qx~(mBPq)a!r~{70dQ<(UsQ&cO-lXClt2S zuWqT|*;0Q1PH25-U+W`FP(eqC%7q&}@JUW&EBpW-G|yLFXgjbPg|dR7=_TI#y+J%X zs2Q@!igvu}*qNM~<8t&pE8{5DS6c4zmEgG0A8EsG;Z9g&rhXJFc~<^?dU#<4W( z+_uwaPwZ`dXmLkTXAMT%m3q)>kunu!%!h|#2Ep?k) z>W<*ABa|Ahl_dh9^vH_HVO8<7$JQZ89qXjQy{^0*{!S`TeIcnVROOL(9-NQcrX_`{ z!VNy$8_AiTyq5YkvM#WjB%`UY$h)? zlt?GJpKH#!?e!bsq|=Arnek@gSG^Ky6BZ{lQe2uJ!z+dP?3Lug{6suCTa6d_Rfx+V ztjI}4=PU6CdBBjyWQG?aGbjg)AaxeRM|}g07`t0nEI8LNpIY*&OJQGHq#?2+vH}g* zerg%SgVJoqph8r9NGRS~Jz?sQw-bCrK4SZZd^RZUs?z_XT(|$HoOI;y0W%$aTj9uz zEU#}?_)sOrJXGnbS79{%%=Ao>M#@foxTUUL&y2# zFDi8MQ`t%B`Nd^(%o6G>Y5D3Zn3f+hixCVn0=NqAz?2{#Rd=+dZoBoIhw&UM#9b(~ zQ>DPOe?2fk2KngQp1u6WPd-hk%~hjgewDv? zR++!Nf&+czBL&LZ6lHB@iW#a37MJm8<8!&p0AjitjhdcVZgSYq2)Gl=Wunx_W+S>V z1Jpm*QM09^ZhOq&r*z7s-MNuRX^=S)GYhnyI;J{;hm0>Veuu<@UlS%_S@>{+lB_sOfJK~QCeK~;%+&bxRc8~ zG8K|z$*|pUx4UEY;kL)NwH~R1bvcnn8krwP$2{4*1i{y_`ZRJK&OzsRnCidpO@w}O z4!3G*a+%jBXY64=!Z-LcBS0eQ5DYwfD4ZddW70YddUEbN>R9R&ePeoB>K9wy>vqR% z*-}LXJDw|eWqv|!CJjgFp1^dkFQIYRIT`)ZIRkt3#^c9~Vee#Sc+0&$M}C>rSc+MF}2I-{WSg^k<$qJ!@1@ATTd-+efkvFv06qWwhZzo>v(SsU#dE0W2`Lq zBs`8uI(6*z*N(X^q0uQ;=LgJ63}9F)#ZT5nog>eXpHix-E6wx@m|f)wRJU`(Pg*vF zeUp~$EwA$Xs8OGgRyHqz`)RC`^H@$Iyjc_&rCgJP4a~MR4Y`_pTb2S8(61 zj0&?Vz)QQ$`q+fpd}TV;LBS#{4$N{h$S%I+=CL_zCwiHug1PTd>Rvl8Dsdgi`g(q+_n=Jv}SJK)xY+6mgNC(P=kD(w%2h7ceJiL=)B0nTU zBCB<4W5>ov+nQF=crbzTy}!|vgJWUj>Bu1~8rDTlAvB($b#lkXoluRbSyGLVKN>U6 z@TIC*zUdE@%bL9qQ?q_mCs4D3iR|dK0*`kNd)jD%r(H75%PU-l_sTptyLEX}+tE{w zkQ0UE(70I+UWiz^bj8r4a@uIaBV zH-`nKN2TYB9sU^(HNplb#ke`wUl}U*1XP|JuDZiujH-?h4Yp1(L=Cbn=rgpONvvU` z5DJr(eT`20wJ|>G|3V&ol#T_n+>E8CW0x@t|6!P0UZdG%BIvZAK^x1za8bwh$J>tV zg^Q4sE_#$AQSNmR{V>7)LyU`VRL_M8Qy1+zvh9s23AH&q&g0fa^8WCW{dw3VS7UOx zAN|87*p#D|pXnH4=9s0thLG1DIU~Dp6*5a{?TO3#!#hgl^n;zqGP(S;Rua}4!rA2k zn3@}TlEMq)Pvn4Fjkhh?)w-iL#^CYfGHO#C4#^)TCbfhZA1s&sr^|#G4;l}A0c+h^ zD$3l!fX6I{|D0X>O2^^vjDSl@!@J92VIFN5kIL;*9k#Z+sdLCu(vy!x4%04)25yXn zXOC|?w{U;!g0(UB-BrUz!J&!w@uhNTh8YM2Jy@K;#IA;NXu`1=T^#(#pNvrh5=6I{-nE|yiOU(~?=cxsqHyD&HF`gE5 z*c%_^l@WG>SjX+*-C!QC<040~l50YZd0#A7b6a-Dnp^|gz$$4Hg z+AduR!aE_s21j0-;k|N?7rI8>`fTgIcDIjPyVFS?@^bH&hw`$n|Oj_$?bBUuoUibHI8|QC0V$1S)CV{htx!mP9ouyr4xs|ka}hW8 z_2p_u^BAp%bZB%$!p_=6RlBvu z7~HMrC}nV(Tg6S9*XL7l-2`5Kk5IzjDtHT0yLR{?U$F(dTzC_ob1Nwp~LCM@Kq6C+2N>dip)hF zr@8}Jk~y3)RfR^lp#tm7;i&I4jc=0|-bsrkxeM{qEonbu4o728GiXQ7aeF*E-PUjF ztd}D+^*2x0ET15^DllLR9F`t=4C6Q!eU7D*&YoD*_QYEGcMXmOn>z0j!P?iEZ9>Lkj zURpKav|&-F4USMc%Azw;U(AVY!9k|9Tkb^UZtCpOCDfo8JUd7D^cu2OjwhDgjn0d2 zQRc;+hh=(tFMt2%n^5#w`|@9!su}})-sIRE6V=^*dR0Yrx|*lb3Vo%XfSgHP=^=SZ zhbZzgz~_Z7xws5ieRH`SXzDPV;=fJ?cA2)a^(m7efEn*Zb_&wqGbfTiC$3Q(*YsF072h_LvR{ZWRT znhJ0Z-M)Me>KM~>c%3JIvOVw?l`c`8Z(^*w;F=m`T@nt^WpufdLgi=zwl7^JW5Ai|EePTh`grPqr@Fg!aHOZj5JBYuHS9 z_7=K|86)%kGrS(OM~_EU1qQzzqZzYU?bNi+@{&TuWgZ-rpfv}-9aH-e&mXmFuMYL( zBvGo)uIVx)U+SbMVSXq@mnR3v5*R<=01Tm+DBu9R(K*q%dFXjkt?n={G%vBX4(_(S zt5DsxwTEJic1P!GtARSFIot$m<8myrojq5=XzO&%SbjAIg90^Bx(dQ40<%mnI!{_b z2v^j1a5WptX9_2}wnls$LcrYE5#Po6d|tX4b6AaaUg~arV&l0Ql#R7G`QfEop6{_h zhJIqzp4}S@*C%vZ?8aHPZ#wO)d!@y?IoP1woT!#WnS3B=y(@AhhJsZxRuE;quar(s z3?Yp<4o;rZQYSk)bp@FJo5fLjxK`=P74-_d zsc8b5-X~+M*Z0bPxv;q4n?>+=qFTM;q;|U1e_eddhQABLm4H_f@`MXZ2(11W$FJz* zEuw?)3UrNq(VjfMxw1@ki8mqPJ@Ojnr&0kp9Xn%mYFLi_acs0W`>aP?X5hWZ5kBR>q|tGF znKX8H>>_mBKRcbAs4mqEmt=YaAvvt}3C4uT@b6w!A(ErqoSEars{qX5mf>fop(oXOI58L`51d5zT}b8`h&2)06Y65SbWNJ zMJ(OqLR6m0rk#`PnYfL^I2wdxv|mbB7>=w#C9y)Dy5mF?b?Mtb*5^~?HaOEdNXiV& z!AmMs!P)l*8Q}PfILY+S!TzucHe>6HGs4by84IvKL=n0BT8HJ9JbXCXTn9I|H?G9d zL;FI0O~*^hJE8&MIzt9p$Kw1{Xx3!kIC3^C;Bf0x19|MnCD2aksw{N}OApn++E#uLfib9tHfN~QaypLQ*it{= zY9Y5&utuX?94g5p)q~OTD6Re2_O`X#V;TS-63L&eT#~v}2djw{c<+UJ^&5zK(W~$@ z*#LnNytYV7oT1(x&=tKlaD{6jq^Z>r_TP< zI-cCn`D#4jjlaNoS!(k)O~XJ|jZG2^E@Yhq}!K;1mOtiUAj&;?t@h9QpHz&>E zTJ#C|`hp6{D4*87ee0>SC)S-ka=iT%7A?ncQcWvR86hq8*f?N4@=X1FO*bCD~5`{548@#>XBD8McOIdduLgE_*Y$Om! zYJZPDVHK4JrE>S922!;gZC|iYPGa$+X$_l><*0+MPz!7NN|A@zGIaV^#B_=Cz9~nm zCaT?Uc4816)Z;CYEh#6lj6@tNLNJqdgPAH-&BydvoDAeLNtCV22XRxo{$1 z<0AqNx0sI!3c?FXJxDji6sW!`!25F*t{vOg;N>o&%{q7GyU9-yesebr-2EdP@PX1I zOi4Jn!K^8_Sn{)&XYgJQ5zvH8Ihr4lf<1Uqhc9F>(AUYiby8=bpQ~Mk?-)Ap!OZH@ z@lhW0egxm8$)EKq3en+_5q@Ql81HGj=S%Np^VpJ}7^pDSaOvV}8rcU_pc~7~@J{3+ z4!G`!0en>Pzt&|a*7@a$tP)-}T05*P^iBDh1$}uE+vLjB*y)JBl(`3=QQc4D;ehcm zJ=59cX4NxO)Qc2%3ci@ZR{{Zfl;cVfMV`4X6t@`ILT$W=)?YlhJWS`Qp@gfiXbWt}Ah zq3p;L%}XOEM>-;>Mn(3^gWUBeTKBJy@pYzyY{$&=EItI5zl7R6Wx6z?$Umb9Mo8OF z#@IgOwItZ?!ttj35XkNFP7ip>@j@K7I~|MBh2&qnu)<#&?JzE2V~e2P7is@ONz`&e{q?i z72i1R@7X1uD$jIJ&@7U#{#4viNJnKTfKLZ8rucp3)x$Hqm7OMcabt2R k4IzqxHUp2z@Zm#F{+#f6``XNaS&S7&QP4ZjY`Efo05a-MN&o-= literal 0 HcmV?d00001 diff --git a/test/priv/README.md b/test/priv/README.md new file mode 100644 index 000000000..eb9993723 --- /dev/null +++ b/test/priv/README.md @@ -0,0 +1 @@ +`GeoLite2-City-Test.mmdb` is downloaded from https://github.com/maxmind/MaxMind-DB