mirror of
https://github.com/smittix/intercept.git
synced 2026-06-09 22:43:32 -07:00
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:
+66
-10
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user