From 200ad83649746ed2985d27bc5dd031fd919632b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 15:48:54 +0000 Subject: [PATCH 1/6] Initial plan From 5069c136b056da2b6fe88ce6e9c3782c4dddf410 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 15:51:53 +0000 Subject: [PATCH 2/6] Add Docker release workflow for GHCR and GitHub releases Agent-Logs-Url: https://github.com/hichbox/mmdb-server/sessions/fb644c6f-dc09-4f90-b253-5bf95fcaa1e4 Co-authored-by: itsrofly <99050170+itsrofly@users.noreply.github.com> --- .github/workflows/docker-release.yml | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/docker-release.yml diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..c3c1ce0 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,109 @@ +name: Docker Release + +on: + push: + +jobs: + docker-release: + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Compute version tag + id: version + run: | + COMMIT_DATE="$(git show -s --date=format:%Y.%m.%d --format=%cd "$GITHUB_SHA")" + SHORT_SHA="$(git rev-parse --short=7 "$GITHUB_SHA")" + VERSION_TAG="v${COMMIT_DATE}.${SHORT_SHA}" + echo "version_tag=${VERSION_TAG}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/mmdb-server:latest + ghcr.io/${{ github.repository_owner }}/mmdb-server:${{ steps.version.outputs.version_tag }} + + - name: Create tag and release if needed + uses: actions/github-script@v7 + env: + VERSION_TAG: ${{ steps.version.outputs.version_tag }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const tag = process.env.VERSION_TAG; + const sha = context.sha; + + let tagExists = false; + try { + const existingRef = await github.rest.git.getRef({ + owner, + repo, + ref: `tags/${tag}`, + }); + + tagExists = true; + if (existingRef.data.object.sha !== sha) { + core.setFailed(`Tag ${tag} already exists on ${existingRef.data.object.sha}, expected ${sha}.`); + return; + } + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + + if (!tagExists) { + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/tags/${tag}`, + sha, + }); + core.info(`Created tag ${tag} on ${sha}.`); + } else { + core.info(`Tag ${tag} already exists on ${sha}, skipping tag creation.`); + } + + try { + await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag, + }); + core.info(`Release ${tag} already exists, skipping release creation.`); + } catch (error) { + if (error.status !== 404) { + throw error; + } + + await github.rest.repos.createRelease({ + owner, + repo, + tag_name: tag, + target_commitish: sha, + name: tag, + generate_release_notes: true, + }); + core.info(`Created release ${tag}.`); + } From b4cdbafdc9c5f4eac622b92d7b8b1d292744a3d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 15:53:20 +0000 Subject: [PATCH 3/6] Refine workflow trigger and version date format Agent-Logs-Url: https://github.com/hichbox/mmdb-server/sessions/fb644c6f-dc09-4f90-b253-5bf95fcaa1e4 Co-authored-by: itsrofly <99050170+itsrofly@users.noreply.github.com> --- .github/workflows/docker-release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index c3c1ce0..fc01e38 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -2,10 +2,11 @@ name: Docker Release on: push: + branches: + - main jobs: docker-release: - if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) runs-on: ubuntu-latest permissions: contents: write @@ -20,7 +21,7 @@ jobs: - name: Compute version tag id: version run: | - COMMIT_DATE="$(git show -s --date=format:%Y.%m.%d --format=%cd "$GITHUB_SHA")" + COMMIT_DATE="$(git show -s --date=format:%Y-%m-%d --format=%cd "$GITHUB_SHA")" SHORT_SHA="$(git rev-parse --short=7 "$GITHUB_SHA")" VERSION_TAG="v${COMMIT_DATE}.${SHORT_SHA}" echo "version_tag=${VERSION_TAG}" >> "$GITHUB_OUTPUT" From 3695a720f2eac5e6e0b0413d7f4fd98d4ec779c2 Mon Sep 17 00:00:00 2001 From: itsrofly <99050170+itsrofly@users.noreply.github.com> Date: Thu, 21 May 2026 18:56:28 +0100 Subject: [PATCH 4/6] chore: switch to a smaller image, download the latest db on run --- Dockerfile | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 55f179b..5af3faf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,24 @@ -FROM ubuntu:24.04 +FROM python:3.12-slim + LABEL authors="Erik Andri Budiman, Steve Clement" LABEL optimized-by="Gordon" WORKDIR /app COPY . . -# Prepare to install required packages RUN apt-get update && apt-get install -y --no-install-recommends \ + wget \ curl \ git \ nano \ - python3 \ - python3-pip \ - wget \ + ca-certificates \ + libmaxminddb0 \ && rm -rf /var/lib/apt/lists/* -# Update the database -RUN chmod +x db/update.sh && db/update.sh +ENV PATH="/root/.local/bin:$PATH" + +RUN curl -sSL https://install.python-poetry.org | python3 - +RUN poetry install --only main --no-interaction --no-ansi -# Installing the Application -ENV PATH=$PATH:/root/.local/bin -RUN curl -sSL https://install.python-poetry.org | python3 - \ - && poetry install --no-interaction --no-ansi \ - && cp /app/etc/server.conf.sample /app/etc/server.conf +RUN cp /app/etc/server.conf.sample /app/etc/server.conf -ENTRYPOINT ["poetry", "run", "serve"] +CMD ["sh", "-c", "db/update.sh && poetry run serve"] From 38a797cc57ab83de9682aabedba14465aea16705 Mon Sep 17 00:00:00 2001 From: itsrofly <99050170+itsrofly@users.noreply.github.com> Date: Thu, 21 May 2026 18:57:40 +0100 Subject: [PATCH 5/6] chore: improve useful logging --- db/update.sh | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) mode change 100644 => 100755 db/update.sh diff --git a/db/update.sh b/db/update.sh old mode 100644 new mode 100755 index 9f0d580..a575c80 --- a/db/update.sh +++ b/db/update.sh @@ -1,6 +1,29 @@ #!/bin/sh -wget https://cra.circl.lu/opendata/geo-open/mmdb-country/latest.mmdb -mv latest.mmdb GeoOpen-Country.mmdb -wget https://cra.circl.lu/opendata/geo-open/mmdb-country-asn/latest.mmdb -mv latest.mmdb GeoOpen-Country-ASN.mmdb +set -u + +download_and_replace() { + url="$1" + target="$2" + tmp="${target}.tmp" + + echo "[INFO] Downloading $url -> $target" + + if wget --progress=dot:giga --tries=3 -O "$tmp" "$url"; then + echo "[OK] Download successful, replacing $target" + mv -f "$tmp" "$target" + echo "[OK] Updated $target" + else + echo "[WARN] Download failed for $url, keeping existing $target" + rm -f "$tmp" + return 1 + fi +} + +download_and_replace \ + "https://cra.circl.lu/opendata/geo-open/mmdb-country/latest.mmdb" \ + "GeoOpen-Country.mmdb" + +download_and_replace \ + "https://cra.circl.lu/opendata/geo-open/mmdb-country-asn/latest.mmdb" \ + "GeoOpen-Country-ASN.mmdb" From 0c377fd067e24a5d874c8c802e3b123e62c5d77e Mon Sep 17 00:00:00 2001 From: itsrofly <99050170+itsrofly@users.noreply.github.com> Date: Thu, 21 May 2026 18:58:32 +0100 Subject: [PATCH 6/6] fix: log "serving on port" not printing --- mmdb_server/mmdb_server.py | 88 +++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/mmdb_server/mmdb_server.py b/mmdb_server/mmdb_server.py index f3cff3a..727361e 100644 --- a/mmdb_server/mmdb_server.py +++ b/mmdb_server/mmdb_server.py @@ -5,11 +5,11 @@ # The server is released under the AGPL version 3 or later. # # Copyright (C) 2022-2025 Alexandre Dulaunoy - import configparser +import json +import sys import time from ipaddress import ip_address -import json from wsgiref.simple_server import make_server import falcon @@ -17,11 +17,11 @@ version = "0.6" config = configparser.ConfigParser() -config.read('etc/server.conf') -mmdb_file = config['global'].get('mmdb_file') -pubsub = config['global'].getboolean('lookup_pubsub') -port = config['global'].getint('port') -country_file = config['global'].get('country_file') +config.read("etc/server.conf") +mmdb_file = config["global"].get("mmdb_file") +pubsub = config["global"].getboolean("lookup_pubsub") +port = config["global"].getint("port") +country_file = config["global"].get("country_file") mmdb_files = mmdb_file.split(",") @@ -30,18 +30,19 @@ if pubsub: import redis - rdb = redis.Redis(host='127.0.0.1') + + rdb = redis.Redis(host="127.0.0.1") mmdbs = [] for mmdb_file in mmdb_files: meta = {} - meta['reader'] = maxminddb.open_database(mmdb_file, maxminddb.MODE_MEMORY) - meta['description'] = meta['reader'].metadata().description - meta['build_db'] = time.strftime( - '%Y-%m-%d %H:%M:%S', time.localtime(meta['reader'].metadata().build_epoch) + meta["reader"] = maxminddb.open_database(mmdb_file, maxminddb.MODE_MEMORY) + meta["description"] = meta["reader"].metadata().description + meta["build_db"] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(meta["reader"].metadata().build_epoch) ) - meta['db_source'] = meta['reader'].metadata().database_type - meta['nb_nodes'] = meta['reader'].metadata().node_count + meta["db_source"] = meta["reader"].metadata().database_type + meta["nb_nodes"] = meta["reader"].metadata().node_count mmdbs.append(meta) @@ -56,12 +57,12 @@ def validIPAddress(IP: str) -> bool: def pubLookup(value: str) -> bool: if not pubsub: return False - rdb.publish('mmdb-server::lookup', f'{value}') + rdb.publish("mmdb-server::lookup", f"{value}") return True def countryLookup(country: str) -> dict: - if country != 'None' or country is not None or country != 'Unknown': + if country != "None" or country is not None or country != "Unknown": if country in country_info: return country_info[country] else: @@ -73,24 +74,26 @@ def countryLookup(country: str) -> dict: class GeoLookup: def on_get(self, req, resp, value): ret = [] - ua = req.get_header('User-Agent') + ua = req.get_header("User-Agent") ips = req.access_route if not validIPAddress(value): resp.status = falcon.HTTP_422 resp.media = "IPv4 or IPv6 address is in an incorrect format. Dotted decimal for IPv4 or textual representation for IPv6 are required." return - pubLookup(value=f'{value} via {ips} using {ua}') + pubLookup(value=f"{value} via {ips} using {ua}") for mmdb in mmdbs: m = {} - georesult = mmdb['reader'].get(value) + georesult = mmdb["reader"].get(value) m = mmdb.copy() - del m['reader'] - georesult['meta'] = m - georesult['ip'] = value - if georesult['country']['iso_code'] != 'None': - georesult['country_info'] = countryLookup(country=georesult['country']['iso_code']) + del m["reader"] + georesult["meta"] = m + georesult["ip"] = value + if georesult["country"]["iso_code"] != "None": + georesult["country_info"] = countryLookup( + country=georesult["country"]["iso_code"] + ) else: - georesult['country_info'] = {} + georesult["country_info"] = {} ret.append(georesult) resp.media = ret return @@ -101,40 +104,45 @@ def on_get(self, req, resp): ret = [] ips = req.access_route for mmdb in mmdbs: - georesult = mmdb['reader'].get(ips[0]) + georesult = mmdb["reader"].get(ips[0]) m = mmdb.copy() - del m['reader'] - georesult['meta'] = m - georesult['ip'] = ips[0] - if georesult['country']['iso_code'] != 'None': - georesult['country_info'] = countryLookup(country=georesult['country']['iso_code']) + del m["reader"] + georesult["meta"] = m + georesult["ip"] = ips[0] + if georesult["country"]["iso_code"] != "None": + georesult["country_info"] = countryLookup( + country=georesult["country"]["iso_code"] + ) else: - georesult['country_info'] = {} + georesult["country_info"] = {} ret.append(georesult) resp.media = ret return + class MyRawLookup: def on_get(self, req, resp): ips = req.access_route resp.text = ips[0] return + def on_head(self, req, resp): ips = req.access_route - resp.append_header('X-IP', ips[0]) + resp.append_header("X-IP", ips[0]) + app = falcon.App() -app.add_route('/geolookup/{value}', GeoLookup()) -app.add_route('/', MyGeoLookup()) -app.add_route('/raw', MyRawLookup()) +app.add_route("/geolookup/{value}", GeoLookup()) +app.add_route("/", MyGeoLookup()) +app.add_route("/raw", MyRawLookup()) + def main(): - with make_server('', port, app) as httpd: - print(f'Serving on port {port}...') + with make_server("", port, app) as httpd: + print(f"Serving on port {port}...", file=sys.stderr) httpd.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": main() -