From 90e88fc4695d235939faf7c2f53257d33305ffe9 Mon Sep 17 00:00:00 2001 From: Smittix Date: Sun, 8 Feb 2026 20:35:31 +0000 Subject: [PATCH] Fix tshark hex parsing and add API key settings UI Parse tshark GSM field values with int(value, 0) instead of int(value) to auto-detect hex 0x-prefixed output (e.g. 0x039e for TMSI/LAC/CID). Without this, every tshark line with hex values fails to parse, causing 0 devices to be captured during monitoring. Also add API Keys tab to Settings modal for configuring OpenCellID key via the UI (in addition to env var), with status display and usage bar. Co-Authored-By: Claude Opus 4.6 --- routes/gsm_spy.py | 76 +++++++++++++++++--- static/js/core/settings-manager.js | 96 ++++++++++++++++++++++++++ templates/partials/settings-modal.html | 65 +++++++++++++++++ utils/gsm_geocoding.py | 16 ++++- 4 files changed, 242 insertions(+), 11 deletions(-) diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index 90b419f..fee519e 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -89,9 +89,18 @@ def increment_api_usage(): return current + 1 +def get_opencellid_api_key(): + """Get OpenCellID API key, checking env var first, then database setting.""" + env_key = config.GSM_OPENCELLID_API_KEY + if env_key: + return env_key + from utils.database import get_setting + return get_setting('gsm.opencellid.api_key', '') + + def can_use_api(): """Check if we can make an API call within daily limit.""" - if not config.GSM_OPENCELLID_API_KEY: + if not get_opencellid_api_key(): return False current_usage = get_api_usage_today() return current_usage < config.GSM_API_DAILY_LIMIT @@ -140,8 +149,8 @@ def geocoding_worker(): # Check API key and rate limit if not can_use_api(): - if not config.GSM_OPENCELLID_API_KEY: - logger.warning("OpenCellID API key not configured (set INTERCEPT_GSM_OPENCELLID_API_KEY)") + if not get_opencellid_api_key(): + logger.warning("OpenCellID API key not configured (set INTERCEPT_GSM_OPENCELLID_API_KEY or configure in Settings > API Keys)") else: current_usage = get_api_usage_today() logger.warning(f"OpenCellID API daily limit reached ({current_usage}/{config.GSM_API_DAILY_LIMIT})") @@ -807,7 +816,53 @@ def status(): 'selected_arfcn': app_module.gsm_spy_selected_arfcn, 'api_usage_today': api_usage, 'api_limit': config.GSM_API_DAILY_LIMIT, - 'api_remaining': config.GSM_API_DAILY_LIMIT - api_usage + 'api_remaining': config.GSM_API_DAILY_LIMIT - api_usage, + 'api_key_configured': bool(get_opencellid_api_key()) + }) + + +@gsm_spy_bp.route('/settings/api_key', methods=['GET', 'POST']) +def settings_api_key(): + """Get or set OpenCellID API key configuration.""" + from utils.database import get_setting, set_setting + + if request.method == 'GET': + env_key = config.GSM_OPENCELLID_API_KEY + db_key = get_setting('gsm.opencellid.api_key', '') + + if env_key: + source = 'env' + configured = True + elif db_key: + source = 'database' + configured = True + else: + source = 'none' + configured = False + + usage_today = get_api_usage_today() + + return jsonify({ + 'configured': configured, + 'source': source, + 'usage_today': usage_today, + 'api_limit': config.GSM_API_DAILY_LIMIT + }) + + # POST: save key to database + data = request.get_json() or {} + key = data.get('key', '').strip() + + if not key: + return jsonify({'error': 'API key cannot be empty'}), 400 + + set_setting('gsm.opencellid.api_key', key) + logger.info("OpenCellID API key saved to database") + + return jsonify({ + 'status': 'saved', + 'configured': True, + 'source': 'database' }) @@ -844,7 +899,8 @@ def lookup_cell(): }) # Check API key and usage limit - if not config.GSM_OPENCELLID_API_KEY: + api_key = get_opencellid_api_key() + if not api_key: return jsonify({'error': 'OpenCellID API key not configured'}), 503 if not can_use_api(): current_usage = get_api_usage_today() @@ -857,7 +913,7 @@ def lookup_cell(): # Call OpenCellID API api_url = config.GSM_OPENCELLID_API_URL params = { - 'key': config.GSM_OPENCELLID_API_KEY, + 'key': api_key, 'mcc': mcc, 'mnc': mnc, 'lac': lac, @@ -1416,15 +1472,15 @@ def parse_tshark_output(line: str, field_order: list[str] | None = None) -> dict for i, logical_name in enumerate(field_order): field_map[logical_name] = parts[i] if parts[i] else None - # Convert types + # Convert types (use base 0 to auto-detect hex 0x prefix from tshark) ta_raw = field_map.get('ta') data = { 'type': 'device', - 'ta_value': int(ta_raw) if ta_raw else None, + 'ta_value': int(ta_raw, 0) if ta_raw else None, 'tmsi': field_map.get('tmsi'), 'imsi': field_map.get('imsi'), - 'lac': int(field_map['lac']) if field_map.get('lac') else None, - 'cid': int(field_map['cid']) if field_map.get('cid') else None, + 'lac': int(field_map['lac'], 0) if field_map.get('lac') else None, + 'cid': int(field_map['cid'], 0) if field_map.get('cid') else None, 'timestamp': datetime.now().isoformat() } diff --git a/static/js/core/settings-manager.js b/static/js/core/settings-manager.js index 3d2cb8b..69ed89b 100644 --- a/static/js/core/settings-manager.js +++ b/static/js/core/settings-manager.js @@ -930,5 +930,101 @@ function switchSettingsTab(tabName) { if (typeof RecordingUI !== 'undefined') { RecordingUI.refresh(); } + } else if (tabName === 'apikeys') { + loadApiKeyStatus(); } } + +/** + * Load API key status into the API Keys settings tab + */ +function loadApiKeyStatus() { + const badge = document.getElementById('apiKeyStatusBadge'); + const desc = document.getElementById('apiKeyStatusDesc'); + const usage = document.getElementById('apiKeyUsageCount'); + const bar = document.getElementById('apiKeyUsageBar'); + + if (!badge) return; + + fetch('/gsm_spy/settings/api_key') + .then(r => r.json()) + .then(data => { + if (data.configured) { + badge.textContent = 'Configured'; + badge.className = 'asset-badge available'; + desc.textContent = 'Source: ' + (data.source === 'env' ? 'Environment variable' : 'Database'); + } else { + badge.textContent = 'Not configured'; + badge.className = 'asset-badge missing'; + desc.textContent = 'No API key set'; + } + if (usage) { + usage.textContent = (data.usage_today || 0) + ' / ' + (data.api_limit || 1000); + } + if (bar) { + const pct = Math.min(100, ((data.usage_today || 0) / (data.api_limit || 1000)) * 100); + bar.style.width = pct + '%'; + bar.style.background = pct > 90 ? 'var(--accent-red)' : pct > 70 ? 'var(--accent-yellow)' : 'var(--accent-cyan)'; + } + }) + .catch(() => { + badge.textContent = 'Error'; + badge.className = 'asset-badge missing'; + desc.textContent = 'Could not load status'; + }); +} + +/** + * Save API key from the settings input + */ +function saveApiKey() { + const input = document.getElementById('apiKeyInput'); + const result = document.getElementById('apiKeySaveResult'); + if (!input || !result) return; + + const key = input.value.trim(); + if (!key) { + result.style.display = 'block'; + result.style.color = 'var(--accent-red)'; + result.textContent = 'Please enter an API key.'; + return; + } + + result.style.display = 'block'; + result.style.color = 'var(--text-dim)'; + result.textContent = 'Saving...'; + + fetch('/gsm_spy/settings/api_key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: key }) + }) + .then(r => r.json()) + .then(data => { + if (data.error) { + result.style.color = 'var(--accent-red)'; + result.textContent = data.error; + } else { + result.style.color = 'var(--accent-green)'; + result.textContent = 'API key saved successfully.'; + input.value = ''; + loadApiKeyStatus(); + // Hide the banner if visible + const banner = document.getElementById('apiKeyBanner'); + if (banner) banner.style.display = 'none'; + } + }) + .catch(() => { + result.style.color = 'var(--accent-red)'; + result.textContent = 'Error saving API key.'; + }); +} + +/** + * Toggle API key input visibility + */ +function toggleApiKeyVisibility() { + const input = document.getElementById('apiKeyInput'); + if (!input) return; + input.type = input.type === 'password' ? 'text' : 'password'; +} diff --git a/templates/partials/settings-modal.html b/templates/partials/settings-modal.html index b9c951b..ca890ab 100644 --- a/templates/partials/settings-modal.html +++ b/templates/partials/settings-modal.html @@ -17,6 +17,7 @@ + @@ -359,6 +360,70 @@ + +
+
+
OpenCellID API Key
+

+ Required for GSM cell tower geolocation. Get a free key at + opencellid.org/register + (1,000 lookups/day). +

+ +
+
+ Status + Checking... +
+ Checking... +
+ +
+
+ API Key + Paste your OpenCellID API token +
+
+ + +
+
+ +
+ +
+ + +
+ +
+
Usage Today
+
+
+ API Calls + -- / -- +
+
+
+
+
+
+ +
+ Note: The environment variable INTERCEPT_GSM_OPENCELLID_API_KEY takes priority over the saved key. + Keys saved here persist across restarts. +
+
+
diff --git a/utils/gsm_geocoding.py b/utils/gsm_geocoding.py index 006368d..feaf164 100644 --- a/utils/gsm_geocoding.py +++ b/utils/gsm_geocoding.py @@ -65,6 +65,15 @@ def lookup_cell_coordinates(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, return None +def _get_api_key() -> str: + """Get OpenCellID API key at runtime (env var first, then database).""" + env_key = config.GSM_OPENCELLID_API_KEY + if env_key: + return env_key + from utils.database import get_setting + return get_setting('gsm.opencellid.api_key', '') + + def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, Any] | None: """ Lookup cell tower from OpenCellID API and cache result. @@ -81,9 +90,14 @@ def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, An Returns None if API call fails or cell not found. """ try: + api_key = _get_api_key() + if not api_key: + logger.warning("OpenCellID API key not configured") + return None + api_url = config.GSM_OPENCELLID_API_URL params = { - 'key': config.GSM_OPENCELLID_API_KEY, + 'key': api_key, 'mcc': mcc, 'mnc': mnc, 'lac': lac,