Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Docker Release

on:
push:
branches:
- main

jobs:
docker-release:
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}.`);
}
24 changes: 11 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
31 changes: 27 additions & 4 deletions db/update.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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"
88 changes: 48 additions & 40 deletions mmdb_server/mmdb_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
# 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
import maxminddb

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(",")

Expand All @@ -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)


Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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()