mirror of
https://github.com/plausible/analytics.git
synced 2024-08-17 05:40:24 +03:00
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.
This commit is contained in:
parent
2a4061de8c
commit
166748dcf2
1
.gitignore
vendored
1
.gitignore
vendored
@ -63,6 +63,7 @@ plausible-report.xml
|
|||||||
|
|
||||||
# Geolocation databases
|
# Geolocation databases
|
||||||
/priv/geodb/*.mmdb
|
/priv/geodb/*.mmdb
|
||||||
|
/priv/geodb/*.mmdb.gz
|
||||||
|
|
||||||
# Auto-generated tracker files
|
# Auto-generated tracker files
|
||||||
/priv/tracker/js/*.js
|
/priv/tracker/js/*.js
|
||||||
|
@ -11,3 +11,4 @@ SELFHOST=false
|
|||||||
SITE_LIMIT=3
|
SITE_LIMIT=3
|
||||||
HCAPTCHA_SITEKEY=test
|
HCAPTCHA_SITEKEY=test
|
||||||
HCAPTCHA_SECRET=scottiger
|
HCAPTCHA_SECRET=scottiger
|
||||||
|
IP_GEOLOCATION_DB=test/priv/GeoLite2-City-Test.mmdb
|
||||||
|
@ -142,11 +142,13 @@ geolite2_country_db =
|
|||||||
get_var_from_path_or_env(
|
get_var_from_path_or_env(
|
||||||
config_dir,
|
config_dir,
|
||||||
"GEOLITE2_COUNTRY_DB",
|
"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)
|
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")
|
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
|
if System.get_env("DISABLE_AUTH") do
|
||||||
require Logger
|
require Logger
|
||||||
@ -433,17 +435,38 @@ config :kaffy,
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
if config_env() != :test do
|
geo_opts =
|
||||||
config :geolix,
|
cond do
|
||||||
databases: [
|
maxmind_license_key ->
|
||||||
%{
|
[
|
||||||
id: :geolocation,
|
license_key: maxmind_license_key,
|
||||||
adapter: Geolix.Adapter.MMDB2,
|
edition: maxmind_edition,
|
||||||
source: ip_geolocation_db,
|
async: true
|
||||||
result_as: :raw
|
]
|
||||||
}
|
|
||||||
]
|
ip_geolocation_db ->
|
||||||
end
|
[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
|
if geonames_source_file do
|
||||||
config :location, :geonames_source_file, geonames_source_file
|
config :location, :geonames_source_file, geonames_source_file
|
||||||
|
@ -24,45 +24,6 @@ config :plausible, :google,
|
|||||||
|
|
||||||
config :bamboo, :refute_timeout, 10
|
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,
|
config :plausible,
|
||||||
session_timeout: 0,
|
session_timeout: 0,
|
||||||
http_impl: Plausible.HTTPClient.Mock,
|
http_impl: Plausible.HTTPClient.Mock,
|
||||||
|
@ -34,7 +34,7 @@ defmodule Mix.Tasks.DownloadCountryDatabase do
|
|||||||
|
|
||||||
if res.status_code == 200 do
|
if res.status_code == 200 do
|
||||||
File.mkdir("priv/geodb")
|
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")
|
Logger.info("Downloaded and saved the database successfully")
|
||||||
else
|
else
|
||||||
Logger.error("Unable to download and save the database. Response: #{inspect(res)}")
|
Logger.error("Unable to download and save the database. Response: #{inspect(res)}")
|
||||||
|
@ -30,9 +30,14 @@ defmodule Plausible.Application do
|
|||||||
]
|
]
|
||||||
|
|
||||||
opts = [strategy: :one_for_one, name: Plausible.Supervisor]
|
opts = [strategy: :one_for_one, name: Plausible.Supervisor]
|
||||||
|
|
||||||
setup_sentry()
|
setup_sentry()
|
||||||
setup_opentelemetry()
|
setup_opentelemetry()
|
||||||
|
|
||||||
|
setup_geolocation()
|
||||||
Location.load_all()
|
Location.load_all()
|
||||||
|
Plausible.Geo.await_loader()
|
||||||
|
|
||||||
Supervisor.start_link(children, opts)
|
Supervisor.start_link(children, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -119,4 +124,9 @@ defmodule Plausible.Application do
|
|||||||
OpentelemetryEcto.setup([:plausible, :clickhouse_repo])
|
OpentelemetryEcto.setup([:plausible, :clickhouse_repo])
|
||||||
OpentelemetryOban.setup()
|
OpentelemetryOban.setup()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp setup_geolocation do
|
||||||
|
opts = Application.fetch_env!(:plausible, Plausible.Geo)
|
||||||
|
:ok = Plausible.Geo.load_db(opts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
189
lib/plausible/geo.ex
Normal file
189
lib/plausible/geo.ex
Normal file
@ -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
|
@ -168,7 +168,7 @@ defmodule Plausible.Ingestion.Event do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp put_geolocation(%__MODULE__{} = event) do
|
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)
|
update_attrs(event, result)
|
||||||
end
|
end
|
||||||
|
@ -1,33 +1,39 @@
|
|||||||
defmodule Plausible.Ingestion.Geolocation do
|
defmodule Plausible.Ingestion.Geolocation do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
alias Plausible.Ingestion.CityOverrides
|
|
||||||
|
|
||||||
def lookup(remote_ip) do
|
def lookup(ip_address) do
|
||||||
result = Geolix.lookup(remote_ip, where: :geolocation)
|
case Plausible.Geo.lookup(ip_address) do
|
||||||
|
%{} = entry ->
|
||||||
|
country_code =
|
||||||
|
entry
|
||||||
|
|> get_in(["country", "iso_code"])
|
||||||
|
|> ignore_unknown_country()
|
||||||
|
|
||||||
country_code =
|
city_geoname_id = country_code && get_in(entry, ["city", "geoname_id"])
|
||||||
get_in(result, [:country, :iso_code])
|
city_geoname_id = Plausible.Ingestion.CityOverrides.get(city_geoname_id, city_geoname_id)
|
||||||
|> ignore_unknown_country()
|
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
%{
|
nil ->
|
||||||
country_code: country_code,
|
nil
|
||||||
subdivision1_code: subdivision1_code(country_code, result),
|
end
|
||||||
subdivision2_code: subdivision2_code(country_code, result),
|
|
||||||
city_geoname_id: city_geoname_id
|
|
||||||
}
|
|
||||||
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
|
when not is_nil(country_code) do
|
||||||
country_code <> "-" <> iso_code
|
country_code <> "-" <> iso_code
|
||||||
end
|
end
|
||||||
|
|
||||||
defp subdivision1_code(_, _), do: nil
|
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
|
when not is_nil(country_code) do
|
||||||
country_code <> "-" <> iso_code
|
country_code <> "-" <> iso_code
|
||||||
end
|
end
|
||||||
|
@ -86,14 +86,7 @@ defmodule PlausibleWeb.Api.ExternalController do
|
|||||||
|> Keyword.take([:version, :commit, :created, :tags])
|
|> Keyword.take([:version, :commit, :created, :tags])
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
|
|
||||||
geo_database =
|
geo_database = Plausible.Geo.database_type() || "(not configured)"
|
||||||
case Geolix.metadata(where: :geolocation) do
|
|
||||||
%{database_type: type} ->
|
|
||||||
type
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
"(not configured)"
|
|
||||||
end
|
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
geo_database: geo_database,
|
geo_database: geo_database,
|
||||||
|
@ -316,14 +316,13 @@ defmodule PlausibleWeb.StatsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp is_dbip() do
|
defp is_dbip() do
|
||||||
if Application.get_env(:plausible, :is_selfhost) do
|
is_or_nil =
|
||||||
case Geolix.metadata(where: :geolocation) do
|
if Application.get_env(:plausible, :is_selfhost) do
|
||||||
%{database_type: type} ->
|
if type = Plausible.Geo.database_type() do
|
||||||
String.starts_with?(type, "DBIP")
|
String.starts_with?(type, "DBIP")
|
||||||
|
end
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
!!is_or_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
3
mix.exs
3
mix.exs
@ -80,9 +80,8 @@ defmodule Plausible.MixProject do
|
|||||||
{:floki, "~> 0.32.0", only: :test},
|
{:floki, "~> 0.32.0", only: :test},
|
||||||
{:fun_with_flags, "~> 1.9.0"},
|
{:fun_with_flags, "~> 1.9.0"},
|
||||||
{:fun_with_flags_ui, "~> 0.8"},
|
{:fun_with_flags_ui, "~> 0.8"},
|
||||||
|
{:locus, "~> 2.3"},
|
||||||
{:gen_cycle, "~> 1.0"},
|
{:gen_cycle, "~> 1.0"},
|
||||||
{:geolix, "~> 2.0"},
|
|
||||||
{:geolix_adapter_mmdb2, "~> 0.6.0"},
|
|
||||||
{:hackney, "~> 1.8"},
|
{:hackney, "~> 1.8"},
|
||||||
{:hammer, "~> 6.0"},
|
{:hammer, "~> 6.0"},
|
||||||
{:httpoison, "~> 1.4"},
|
{:httpoison, "~> 1.4"},
|
||||||
|
4
mix.lock
4
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"},
|
"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_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"},
|
"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"},
|
"gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
|
||||||
"gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"},
|
"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"},
|
"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"},
|
"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"},
|
"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", []},
|
"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": {: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_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"},
|
"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"},
|
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
"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"},
|
"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"},
|
"mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
|
||||||
"nanoid": {:hex, :nanoid, "2.0.5", "1d2948d8967ef2d948a58c3fef02385040bd9823fc6394bd604b8d98e5516b22", [:mix], [], "hexpm", "956e8876321104da72aa48770539ff26b36b744cd26753ec8e7a8a37e53d5f58"},
|
"nanoid": {:hex, :nanoid, "2.0.5", "1d2948d8967ef2d948a58c3fef02385040bd9823fc6394bd604b8d98e5516b22", [:mix], [], "hexpm", "956e8876321104da72aa48770539ff26b36b744cd26753ec8e7a8a37e53d5f58"},
|
||||||
"nimble_options": {:hex, :nimble_options, "0.5.1", "5c166f7669e40333191bea38e3bd3811cc13f459f1e4be49e89128a21b5d8c4d", [:mix], [], "hexpm", "d176cf7baa4fef0ceb301ca3eb8b55bd7de3e45f489c4f8b4f2849f1f114ef3e"},
|
"nimble_options": {:hex, :nimble_options, "0.5.1", "5c166f7669e40333191bea38e3bd3811cc13f459f1e4be49e89128a21b5d8c4d", [:mix], [], "hexpm", "d176cf7baa4fef0ceb301ca3eb8b55bd7de3e45f489c4f8b4f2849f1f114ef3e"},
|
||||||
|
@ -651,7 +651,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
assert event.referrer == ""
|
assert event.referrer == ""
|
||||||
end
|
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
|
test "looks up location data from the ip address", %{conn: conn, domain: domain} do
|
||||||
params = %{
|
params = %{
|
||||||
name: "pageview",
|
name: "pageview",
|
||||||
@ -660,15 +660,15 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn
|
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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
|
|
||||||
assert pageview.country_code == "FR"
|
assert pageview.country_code == "GB"
|
||||||
assert pageview.subdivision1_code == "FR-IDF"
|
assert pageview.subdivision1_code == "GB-ENG"
|
||||||
assert pageview.subdivision2_code == "FR-75"
|
assert pageview.subdivision2_code == "GB-WBK"
|
||||||
assert pageview.city_geoname_id == 2_988_507
|
assert pageview.city_geoname_id == 2_655_045
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ignores unknown country code ZZ", %{conn: conn, domain: domain} do
|
test "ignores unknown country code ZZ", %{conn: conn, domain: domain} do
|
||||||
@ -736,7 +736,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn
|
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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
@ -752,12 +752,12 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn
|
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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
|
|
||||||
assert pageview.country_code == "US"
|
assert pageview.country_code == "JP"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "works with ipv6 with a port number in x-forwarded-for", %{conn: conn, domain: domain} do
|
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
|
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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
|
|
||||||
assert pageview.country_code == "US"
|
assert pageview.country_code == "JP"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "uses cloudflare's special header for client IP address if present", %{
|
test "uses cloudflare's special header for client IP address if present", %{
|
||||||
@ -788,7 +788,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_req_header("x-forwarded-for", "0.0.0.0")
|
|> 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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
@ -808,7 +808,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_req_header("x-forwarded-for", "0.0.0.0")
|
|> 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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
@ -827,7 +827,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn
|
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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
@ -845,13 +845,13 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
conn
|
conn
|
||||||
|> put_req_header(
|
|> put_req_header(
|
||||||
"forwarded",
|
"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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
pageview = get_event(domain)
|
pageview = get_event(domain)
|
||||||
|
|
||||||
assert pageview.country_code == "US"
|
assert pageview.country_code == "JP"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "URL is decoded", %{conn: conn, domain: domain} do
|
test "URL is decoded", %{conn: conn, domain: domain} do
|
||||||
@ -1042,7 +1042,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
|||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_req_header("user-agent", @user_agent)
|
|> 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)
|
|> post("/api/event", params)
|
||||||
|
|
||||||
[one, two] = get_events(domain)
|
[one, two] = get_events(domain)
|
||||||
|
BIN
test/priv/GeoLite2-City-Test.mmdb
Normal file
BIN
test/priv/GeoLite2-City-Test.mmdb
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
1
test/priv/README.md
Normal file
1
test/priv/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
`GeoLite2-City-Test.mmdb` is downloaded from https://github.com/maxmind/MaxMind-DB
|
Loading…
Reference in New Issue
Block a user