mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 05:49:28 -07:00
GPS feature webapp side: GPS mode selector, fixed mode lat/lon, API endpoint. Merging with Wifi client and webdav features
This commit is contained in:
committed by
Will Greenberg
parent
ac33ebaf53
commit
c107314194
@@ -36,6 +36,12 @@ pub struct Config {
|
||||
pub min_space_to_start_recording_mb: u64,
|
||||
/// Minimum disk space required to continue a recording
|
||||
pub min_space_to_continue_recording_mb: u64,
|
||||
/// GPS mode: 0=Disabled, 1=Fixed coordinates, 2=API endpoint
|
||||
pub gps_mode: u8,
|
||||
/// Fixed latitude used when gps_mode=1
|
||||
pub gps_fixed_latitude: Option<f64>,
|
||||
/// Fixed longitude used when gps_mode=1
|
||||
pub gps_fixed_longitude: Option<f64>,
|
||||
/// Wifi client SSID
|
||||
pub wifi_ssid: Option<String>,
|
||||
/// Wifi client password
|
||||
@@ -100,6 +106,9 @@ impl Default for Config {
|
||||
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
min_space_to_start_recording_mb: 1,
|
||||
min_space_to_continue_recording_mb: 1,
|
||||
gps_mode: 0,
|
||||
gps_fixed_latitude: None,
|
||||
gps_fixed_longitude: None,
|
||||
wifi_ssid: None,
|
||||
wifi_password: None,
|
||||
wifi_security: None,
|
||||
@@ -153,6 +162,49 @@ fn resolve_bin(name: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn wifi_config(&self) -> wifi_station::WifiConfig {
|
||||
let (wpa_bin, hostapd_conf, ctrl_interface) = match self.device {
|
||||
Device::Tmobile | Device::Wingtech => (
|
||||
Some("/usr/sbin/wpa_supplicant".into()),
|
||||
Some("/data/configs/hostapd.conf".into()),
|
||||
None,
|
||||
),
|
||||
Device::Uz801 => (
|
||||
Some("/system/bin/wpa_supplicant".into()),
|
||||
Some("/data/misc/wifi/hostapd.conf".into()),
|
||||
Some("/data/misc/wifi/sockets".into()),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
wifi_station::WifiConfig {
|
||||
wifi_enabled: self.wifi_enabled,
|
||||
dns_servers: self.dns_servers.clone(),
|
||||
wifi_ssid: self.wifi_ssid.clone(),
|
||||
wifi_password: self.wifi_password.clone(),
|
||||
security_type: self.wifi_security,
|
||||
wpa_supplicant_bin: wpa_bin.or_else(|| resolve_bin("wpa_supplicant")),
|
||||
hostapd_conf,
|
||||
ctrl_interface,
|
||||
udhcpc_hook_path: Some("/data/rayhunter/udhcpc-hook.sh".into()),
|
||||
dhcp_lease_path: Some("/data/rayhunter/dhcp_lease".into()),
|
||||
wpa_conf_path: Some("/data/rayhunter/wpa_sta.conf".into()),
|
||||
iw_bin: resolve_bin("iw"),
|
||||
udhcpc_bin: resolve_bin("udhcpc"),
|
||||
crash_log_dir: Some("/data/rayhunter/crash-logs".into()),
|
||||
wakelock_name: Some("rayhunter".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_bin(name: &str) -> Option<String> {
|
||||
let local = format!("/data/rayhunter/bin/{name}");
|
||||
if std::path::Path::new(&local).exists() {
|
||||
return Some(local);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
|
||||
39
daemon/src/gps.rs
Normal file
39
daemon/src/gps.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::server::ServerState;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GpsData {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
pub async fn post_gps(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Json(gps_data): Json<GpsData>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
if state.config.gps_mode != 2 {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
"GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(),
|
||||
));
|
||||
}
|
||||
let mut gps = state.gps_state.write().await;
|
||||
*gps = Some(gps_data);
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn get_gps(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<GpsData>, StatusCode> {
|
||||
let gps = state.gps_state.read().await;
|
||||
match gps.as_ref() {
|
||||
Some(data) => Ok(Json(data.clone())),
|
||||
None => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod crypto_provider;
|
||||
pub mod diag;
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
pub mod gps;
|
||||
pub mod key_input;
|
||||
pub mod notifications;
|
||||
pub mod pcap;
|
||||
|
||||
@@ -5,6 +5,7 @@ mod crypto_provider;
|
||||
mod diag;
|
||||
mod display;
|
||||
mod error;
|
||||
mod gps;
|
||||
mod key_input;
|
||||
mod notifications;
|
||||
mod pcap;
|
||||
@@ -23,6 +24,7 @@ use crate::error::RayhunterError;
|
||||
use crate::notifications::{NotificationService, run_notification_worker};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::gps::{get_gps, post_gps};
|
||||
use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
|
||||
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
|
||||
@@ -78,6 +80,8 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/time", get(get_time))
|
||||
.route("/api/time-offset", post(set_time_offset))
|
||||
.route("/api/debug/display-state", post(debug_set_display_state))
|
||||
.route("/api/gps", get(get_gps))
|
||||
.route("/api/gps", post(post_gps))
|
||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||
.route("/{*path}", get(serve_static))
|
||||
}
|
||||
@@ -296,6 +300,18 @@ async fn run_with_config(
|
||||
config.webdav.clone().into(),
|
||||
);
|
||||
}
|
||||
let initial_gps = if config.gps_mode == 1 {
|
||||
match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
|
||||
(Some(lat), Some(lon)) => Some(gps::GpsData {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
timestamp: "fixed".to_string(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let state = Arc::new(ServerState {
|
||||
config_path: args.config_path.clone(),
|
||||
@@ -308,6 +324,7 @@ async fn run_with_config(
|
||||
ui_update_sender: Some(ui_update_tx),
|
||||
wifi_status,
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
gps_state: Arc::new(tokio::sync::RwLock::new(initial_gps)),
|
||||
});
|
||||
run_server(&task_tracker, state, shutdown_token.clone()).await;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::config::Config;
|
||||
use crate::diag::DiagDeviceCtrlMessage;
|
||||
use crate::display::DisplayState;
|
||||
use crate::notifications::DEFAULT_NOTIFICATION_TIMEOUT;
|
||||
use crate::gps::GpsData;
|
||||
use crate::pcap::generate_pcap_data;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
|
||||
@@ -40,6 +41,7 @@ pub struct ServerState {
|
||||
pub ui_update_sender: Option<Sender<DisplayState>>,
|
||||
pub wifi_status: Arc<RwLock<wifi_station::WifiStatus>>,
|
||||
pub wifi_scan_lock: tokio::sync::Mutex<()>,
|
||||
pub gps_state: Arc<RwLock<Option<GpsData>>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
@@ -566,6 +568,7 @@ mod tests {
|
||||
ui_update_sender: None,
|
||||
wifi_status: Arc::new(RwLock::new(wifi_station::WifiStatus::default())),
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
gps_state: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -781,6 +781,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">GPS Settings</h3>
|
||||
|
||||
<div>
|
||||
<label for="gps_mode" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
GPS Mode
|
||||
</label>
|
||||
<select
|
||||
id="gps_mode"
|
||||
bind:value={config.gps_mode}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
>
|
||||
<option value={0}>0 - Disabled</option>
|
||||
<option value={1}>1 - Fixed coordinates</option>
|
||||
<option value={2}>2 - API Endpoint</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{#if config.gps_mode === 2}
|
||||
POST latitude, longitude, and timestamp to <code>/api/gps</code> from
|
||||
any device on the network.
|
||||
{:else if config.gps_mode === 1}
|
||||
GPS coordinates are fixed to the values below.
|
||||
{:else}
|
||||
GPS is disabled; no coordinates will be tracked.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if config.gps_mode === 1}
|
||||
<div>
|
||||
<label
|
||||
for="gps_fixed_latitude"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Fixed Latitude
|
||||
</label>
|
||||
<input
|
||||
id="gps_fixed_latitude"
|
||||
type="number"
|
||||
min="-90"
|
||||
max="90"
|
||||
step="any"
|
||||
required
|
||||
bind:value={config.gps_fixed_latitude}
|
||||
placeholder="e.g. 37.7749"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -90 to 90</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="gps_fixed_longitude"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Fixed Longitude
|
||||
</label>
|
||||
<input
|
||||
id="gps_fixed_longitude"
|
||||
type="number"
|
||||
min="-180"
|
||||
max="180"
|
||||
step="any"
|
||||
required
|
||||
bind:value={config.gps_fixed_longitude}
|
||||
placeholder="e.g. -122.4194"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -180 to 180</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -46,6 +46,9 @@ export interface Config {
|
||||
firewall_restrict_outbound: boolean;
|
||||
firewall_allowed_ports: number[] | null;
|
||||
webdav: WebdavConfig;
|
||||
gps_mode: number;
|
||||
gps_fixed_latitude: number | null;
|
||||
gps_fixed_longitude: number | null;
|
||||
}
|
||||
|
||||
export interface WifiStatus {
|
||||
@@ -153,3 +156,20 @@ export interface TimeResponse {
|
||||
export async function get_daemon_time(): Promise<TimeResponse> {
|
||||
return JSON.parse(await req('GET', '/api/time'));
|
||||
}
|
||||
|
||||
export interface GpsData {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export async function get_gps(): Promise<GpsData | null> {
|
||||
const response = await fetch('/api/gps');
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import { get_manifest, get_system_stats } from '$lib/utils.svelte';
|
||||
import { get_manifest, get_system_stats, get_gps, get_config, type GpsData } from '$lib/utils.svelte';
|
||||
import ManifestTable from '$lib/components/ManifestTable.svelte';
|
||||
import Card from '$lib/components/ManifestCard.svelte';
|
||||
import type { SystemStats } from '$lib/systemStats';
|
||||
@@ -22,7 +22,13 @@
|
||||
let update_error: string | undefined = $state(undefined);
|
||||
let logview_shown: boolean = $state(false);
|
||||
let config_shown: boolean = $state(false);
|
||||
let gps_data: GpsData | null = $state(null);
|
||||
let gps_mode: number = $state(0);
|
||||
$effect(() => {
|
||||
get_config().then((c) => {
|
||||
gps_mode = c.gps_mode;
|
||||
});
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
// Don't update UI if browser tab isn't visible
|
||||
@@ -40,6 +46,7 @@
|
||||
current_entry = new_manifest.current_entry;
|
||||
|
||||
system_stats = await get_system_stats();
|
||||
gps_data = await get_gps();
|
||||
update_error = undefined;
|
||||
loaded = true;
|
||||
} catch (error) {
|
||||
@@ -283,6 +290,48 @@
|
||||
{/if}
|
||||
<SystemStatsTable stats={system_stats!} />
|
||||
</div>
|
||||
{#if gps_mode !== 0}
|
||||
<div class="bg-white border border-gray-200 drop-shadow rounded-md p-4 flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold flex flex-row items-center gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-rayhunter-blue"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.906 1.994a8.002 8.002 0 0 1 8.09 8.421 7.996 7.996 0 0 1-1.297 3.957.996.996 0 0 1-.133.204l-.108.129c-.178.243-.37.477-.573.699l-5.112 6.224a1 1 0 0 1-1.545 0L5.982 15.26l-.002-.002a18.146 18.146 0 0 1-.309-.38l-.133-.163a.999.999 0 0 1-.13-.202 7.995 7.995 0 0 1 6.498-12.518ZM15 9.997a3 3 0 1 1-5.999 0 3 3 0 0 1 5.999 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
GPS Status
|
||||
</span>
|
||||
{#if gps_data}
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-1 pr-4 text-gray-500 font-medium">Latitude</td>
|
||||
<td class="py-1 font-mono">{gps_data.latitude.toFixed(6)}</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-1 pr-4 text-gray-500 font-medium">Longitude</td>
|
||||
<td class="py-1 font-mono">{gps_data.longitude.toFixed(6)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500 font-medium">GPS Timestamp</td>
|
||||
<td class="py-1 font-mono">{gps_data.timestamp}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<span class="text-gray-400 text-sm">Awaiting GPS data...</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="text-xl flex-1">History</div>
|
||||
|
||||
Reference in New Issue
Block a user