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 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-08 20:35:31 +00:00
parent 98f6d18bea
commit 90e88fc469
4 changed files with 242 additions and 11 deletions
+66 -10
View File
@@ -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()
}
+96
View File
@@ -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';
}
+65
View File
@@ -17,6 +17,7 @@
<button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button>
<button class="settings-tab" data-tab="alerts" onclick="switchSettingsTab('alerts')">Alerts</button>
<button class="settings-tab" data-tab="recording" onclick="switchSettingsTab('recording')">Recording</button>
<button class="settings-tab" data-tab="apikeys" onclick="switchSettingsTab('apikeys')">API Keys</button>
<button class="settings-tab" data-tab="about" onclick="switchSettingsTab('about')">About</button>
</div>
@@ -359,6 +360,70 @@
</div>
</div>
<!-- API Keys Section -->
<div id="settings-apikeys" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">OpenCellID API Key</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Required for GSM cell tower geolocation. Get a free key at
<a href="https://opencellid.org/register" target="_blank" style="color: var(--accent-cyan);">opencellid.org/register</a>
(1,000 lookups/day).
</p>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Status</span>
<span class="settings-label-desc" id="apiKeyStatusDesc">Checking...</span>
</div>
<span id="apiKeyStatusBadge" class="asset-badge checking">Checking...</span>
</div>
<div class="settings-row" style="flex-wrap: wrap; gap: 8px;">
<div class="settings-label" style="width: 100%;">
<span class="settings-label-text">API Key</span>
<span class="settings-label-desc">Paste your OpenCellID API token</span>
</div>
<div style="display: flex; gap: 8px; width: 100%;">
<input type="password" id="apiKeyInput" class="settings-input"
placeholder="Enter your OpenCellID API key"
style="flex: 1; font-family: var(--font-mono); font-size: 11px;">
<button class="check-assets-btn" onclick="toggleApiKeyVisibility()" style="padding: 6px 10px; font-size: 11px;" title="Show/Hide">
<svg id="apiKeyEyeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 12px;">
<button class="check-assets-btn" onclick="saveApiKey()" style="flex: 1; background: var(--accent-cyan); color: #000;">
Save Key
</button>
</div>
<div id="apiKeySaveResult" style="margin-top: 10px; font-size: 11px; display: none;"></div>
</div>
<div class="settings-group">
<div class="settings-group-title">Usage Today</div>
<div style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: var(--font-mono); font-size: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: var(--text-dim);">API Calls</span>
<span id="apiKeyUsageCount" style="color: var(--accent-cyan);">-- / --</span>
</div>
<div style="background: var(--bg-dark); border-radius: 3px; height: 6px; overflow: hidden; margin-top: 4px;">
<div id="apiKeyUsageBar" style="height: 100%; background: var(--accent-cyan); width: 0%; transition: width 0.3s;"></div>
</div>
</div>
</div>
<div class="settings-info">
<strong>Note:</strong> The environment variable <code>INTERCEPT_GSM_OPENCELLID_API_KEY</code> takes priority over the saved key.
Keys saved here persist across restarts.
</div>
</div>
<!-- About Section -->
<div id="settings-about" class="settings-section">
<div class="settings-group">
+15 -1
View File
@@ -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,