feat(geoip): make GeoIP AIO the default source, deprecate GeoLite2 (#820)

The default GeoIP source was MaxMind GeoLite2 via sapics/ip-location-db,
whose URLs kept serving the 2026-06-17 build after that project moved to
GitHub Releases (found in #815). GeoIP AIO (daijro/geoip-all-in-one)
resolves timezones more accurately on real proxy IPs and is rebuilt weekly.

- repos.yml: AIO is the default; GeoLite2 is `deprecated: true`, with the
  Releases URLs from #815 so it still works when picked by name.
- A cache holding a deprecated source it was not explicitly given
  (`camoufox set --geoip` or the GUI) moves to the default and drops the
  old database. An explicit choice is kept, with a FutureWarning.
- needs_update() reads the database's build date instead of the file age:
  refresh once the build is over 8 days old, re-checking at most daily,
  and warn when a fresh download is over 30 days old (a frozen source).
- get_geolocation(geoip_db=...) now reads that source's own database
  rather than the active one's, and no longer makes it the active one.
- tests/test_geoip_sources.py (from #815) downloads every non-deprecated
  source and fails when its build is stale; tests.yml installs the geoip
  extra so it runs, and so gates every release.
- TypeScript twin updated to match; goldens answer in both layouts.

Co-authored-by: lp177 <57773165+lp177@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-09-30 20:02:52 +00:00
committed by GitHub
co-authored by lp177 Claude Opus 5.5
parent 0db845fac0
commit f36390a19e
16 changed files with 573 additions and 74 deletions
+98
View File
@@ -0,0 +1,98 @@
"""Which GeoIP source is active, and when its database is refreshed."""
import os
import time
import warnings
import pytest
from yaml import safe_dump
from camoufox import geolocation
DEFAULT = "GeoIP AIO by daijro"
DEPRECATED = "MaxMind GeoLite2"
@pytest.fixture(autouse=True)
def cache(tmp_path, monkeypatch):
monkeypatch.setattr(geolocation, "GEOIP_DIR", tmp_path)
monkeypatch.setattr(geolocation, "MMDB_DIR", tmp_path / "mmdb")
monkeypatch.setattr(geolocation, "GEOIP_CONFIG", tmp_path / "config.yml")
return tmp_path
def _saved(cache, **fields):
(cache / "config.yml").write_text(safe_dump(fields))
def test_default_is_aio():
assert geolocation.load_geoip_config()["name"] == DEFAULT
repos, _ = geolocation._load_geoip_repos()
assert [r["name"] for r in repos if r.get("deprecated")] == [DEPRECATED]
def test_implicit_deprecated_source_moves_to_default(cache):
# What every cache written before the default changed holds
_saved(cache, name=DEPRECATED)
assert geolocation.load_geoip_config()["name"] == DEFAULT
def test_explicit_deprecated_source_is_kept(cache):
_saved(cache, name=DEPRECATED, explicit=True)
assert geolocation.load_geoip_config()["name"] == DEPRECATED
def test_save_records_explicit_choice(cache):
config = geolocation._get_geoip_config_by_name(DEPRECATED)
geolocation.save_geoip_config(config, explicit=True)
assert geolocation.load_geoip_config()["name"] == DEPRECATED
geolocation.save_geoip_config(config)
assert geolocation.load_geoip_config()["name"] == DEFAULT
def test_refresh_keeps_an_explicit_choice(cache, monkeypatch):
# `camoufox fetch` and the weekly refresh download without naming a source
_saved(cache, name=DEPRECATED, explicit=True)
monkeypatch.setattr(geolocation, "webdl", lambda url, buffer, **kw: buffer.write(b"x"))
monkeypatch.setattr(geolocation, "_build_age_days", lambda path: 1)
geolocation.download_mmdb()
assert geolocation.load_geoip_config()["name"] == DEPRECATED
def test_deprecated_source_warns():
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
geolocation.warn_if_deprecated(geolocation._get_geoip_config_by_name(DEPRECATED))
geolocation.warn_if_deprecated(geolocation._get_geoip_config_by_name(DEFAULT))
assert [w.category for w in caught] == [FutureWarning]
assert DEFAULT in str(caught[0].message)
def _database(cache, checked_days_ago):
config = geolocation.load_geoip_config()
path = geolocation.get_mmdb_path("ipv4", config)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"")
stamp = time.time() - checked_days_ago * 86400
os.utime(path, (stamp, stamp))
return config
@pytest.mark.parametrize(
"checked, built, stale",
[
(0.5, 400, False), # checked today: wait, even for an old build
(2, 3, False), # this week's build
(2, 9, True), # a release has been missed
(40, 3, False), # an old file holding a new build is not stale
(2, None, True), # unreadable database
],
)
def test_needs_update_reads_the_build_date(cache, monkeypatch, checked, built, stale):
config = _database(cache, checked)
monkeypatch.setattr(geolocation, "_build_age_days", lambda path: built)
assert geolocation.needs_update(config) is stale
def test_missing_database_needs_update():
assert geolocation.needs_update() is True
+43
View File
@@ -0,0 +1,43 @@
"""Every GeoIP source camoufox offers must be serving a current build.
The launcher always downloads a source's newest build, so a source that stops
publishing leaves every user on stale data with no error: GeoLite2's URLs
served the 2026-06-17 build for months after sapics/ip-location-db moved to
GitHub Releases (found in daijro/camoufox#815, which this test comes from).
This suite runs in tests.yml, which gates every release.
"""
import time
import pytest
from camoufox import geolocation
maxminddb = pytest.importorskip("maxminddb")
MAX_AGE_DAYS = 30
SOURCES = [repo["name"] for repo in geolocation._load_geoip_repos()[0] if not repo.get("deprecated")]
@pytest.mark.parametrize("name", SOURCES)
def test_source_serves_a_current_build(name, tmp_path, monkeypatch):
monkeypatch.setattr(geolocation, "GEOIP_DIR", tmp_path)
monkeypatch.setattr(geolocation, "MMDB_DIR", tmp_path / "mmdb")
monkeypatch.setattr(geolocation, "GEOIP_CONFIG", tmp_path / "config.yml")
geolocation.download_mmdb(name)
databases = sorted((tmp_path / "mmdb").glob("*.mmdb"))
assert databases, f"{name} downloaded no database"
for database in databases:
with maxminddb.open_database(str(database)) as reader:
age_days = (time.time() - reader.metadata().build_epoch) / 86400
record = reader.get("8.8.8.8")
assert age_days < MAX_AGE_DAYS, (
f"{name}: {database.name} was built {age_days:.0f} days ago, its source is no longer updated"
)
config = geolocation._get_geoip_config_by_name(name)
assert geolocation._find_in(record, config["paths"]["iso_code"]) == "US", (
f"{name}: the configured paths do not read this database"
)