Push Tracker
ria-ran--sim-drop/plan/03-utils.md
A ashkan@beigi.net 8672b0de6b Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.

Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.

Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
  3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
  (SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
  via SQLite datetime() so the window check is chronologically correct.

Add .gitignore for venv, caches, runtime config.yaml, and databases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 05:56:31 +00:00

2.3 KiB

Unit 03 — Utilities

Goal

Shared helper functions used across server modules. No business logic — pure helpers.

File

  • server/utils.py

Functions

validate_mac(mac: str) -> bool

import re
MAC_RE = re.compile(r'^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$')
def validate_mac(mac: str) -> bool:
    return bool(MAC_RE.match(mac)) if mac else False

normalize_mac(mac: str) -> str

Lowercase, colon-separated. Input may be with dashes or no separator.

def normalize_mac(mac: str) -> str:
    clean = mac.lower().replace('-', ':').replace('.', ':')
    # handle no-separator format (12 hex chars)
    if ':' not in clean and len(clean) == 12:
        clean = ':'.join(clean[i:i+2] for i in range(0, 12, 2))
    return clean

validate_imsi(imsi: str) -> bool

def validate_imsi(imsi: str) -> bool:
    return bool(imsi) and imsi.isdigit() and len(imsi) == 15

load_config(path: str) -> dict

import yaml, os

REQUIRED_KEYS = ['network', 'server', 'database', 'rate_limiting', 'profile']

def load_config(path: str) -> dict:
    if not os.path.exists(path):
        raise FileNotFoundError(f"Config file not found: {path}")
    with open(path) as f:
        cfg = yaml.safe_load(f)
    for key in REQUIRED_KEYS:
        if key not in cfg:
            raise ValueError(f"Missing required config section: {key}")
    return cfg

hex_to_bytes(h: str) -> bytes

def hex_to_bytes(h: str) -> bytes:
    return bytes.fromhex(h)

bytes_to_hex(b: bytes) -> str

def bytes_to_hex(b: bytes) -> str:
    return b.hex()

get_client_ip(request) -> str

Flask request helper — returns real IP behind proxy:

def get_client_ip(request) -> str:
    if request.headers.get('X-Forwarded-For'):
        return request.headers['X-Forwarded-For'].split(',')[0].strip()
    return request.remote_addr

Verification

python -c "
from server.utils import validate_mac, normalize_mac, validate_imsi, load_config

assert validate_mac('aa:bb:cc:dd:ee:ff') == True
assert validate_mac('AA:BB:CC:DD:EE:FF') == True
assert validate_mac('bad') == False
assert normalize_mac('AABBCCDDEEFF') == 'aa:bb:cc:dd:ee:ff'
assert validate_imsi('302720123456789') == True
assert validate_imsi('12345') == False
print('All checks passed')
"