Merge pull request #351 from untitaker/restart

Config change and restart UI
This commit is contained in:
Markus Unterwaditzer
2025-06-24 19:23:30 +02:00
committed by GitHub
10 changed files with 454 additions and 67 deletions
+4 -4
View File
@@ -1,10 +1,10 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
use rayhunter::analysis::analyzer::AnalyzerConfig; use rayhunter::analysis::analyzer::AnalyzerConfig;
use crate::error::RayhunterError; use crate::error::RayhunterError;
#[derive(Debug, Deserialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
pub qmdl_store_path: String, pub qmdl_store_path: String,
@@ -32,11 +32,11 @@ impl Default for Config {
} }
} }
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
{ {
if let Ok(config_file) = std::fs::read_to_string(&path) { if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?) Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
} else { } else {
Ok(Config::default()) Ok(Config::default())
+88 -42
View File
@@ -10,13 +10,17 @@ mod qmdl_store;
mod server; mod server;
mod stats; mod stats;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::config::{parse_args, parse_config}; use crate::config::{parse_args, parse_config};
use crate::diag::run_diag_read_thread; use crate::diag::run_diag_read_thread;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use crate::pcap::get_pcap; use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
use crate::server::{get_qmdl, get_zip, serve_static, ServerState}; use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState};
use crate::stats::get_system_stats; use crate::stats::{get_qmdl_manifest, get_system_stats};
use analysis::{ use analysis::{
get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus,
@@ -31,10 +35,8 @@ use diag::{
use log::{error, info}; use log::{error, info};
use qmdl_store::RecordingStoreError; use qmdl_store::RecordingStoreError;
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use stats::get_qmdl_manifest;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::select;
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender};
use tokio::sync::{oneshot, RwLock}; use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@@ -56,6 +58,8 @@ fn get_router() -> AppRouter {
.route("/api/analysis-report/{name}", get(get_analysis_report)) .route("/api/analysis-report/{name}", get(get_analysis_report))
.route("/api/analysis", get(get_analysis_status)) .route("/api/analysis", get(get_analysis_status))
.route("/api/analysis/{name}", post(start_analysis)) .route("/api/analysis/{name}", post(start_analysis))
.route("/api/config", get(get_config))
.route("/api/config", post(set_config))
.route("/", get(|| async { Redirect::permanent("/index.html") })) .route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/{*path}", get(serve_static)) .route("/{*path}", get(serve_static))
} }
@@ -65,14 +69,14 @@ fn get_router() -> AppRouter {
// (i.e. user hit ctrl+c) // (i.e. user hit ctrl+c)
async fn run_server( async fn run_server(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config,
state: Arc<ServerState>, state: Arc<ServerState>,
server_shutdown_rx: oneshot::Receiver<()>, server_shutdown_rx: oneshot::Receiver<()>,
) -> JoinHandle<()> { ) -> JoinHandle<()> {
info!("spinning up server"); info!("spinning up server");
let app = get_router().with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
let listener = TcpListener::bind(&addr).await.unwrap(); let listener = TcpListener::bind(&addr).await.unwrap();
let app = get_router().with_state(state);
task_tracker.spawn(async move { task_tracker.spawn(async move {
info!("The orca is hunting for stingrays..."); info!("The orca is hunting for stingrays...");
axum::serve(listener, app) axum::serve(listener, app)
@@ -118,46 +122,61 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
// Start a thread that'll track when user hits ctrl+c. When that happens, // Start a thread that'll track when user hits ctrl+c. When that happens,
// trigger various cleanup tasks, including sending signals to other threads to // trigger various cleanup tasks, including sending signals to other threads to
// shutdown // shutdown
fn run_ctrl_c_thread( fn run_shutdown_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>, diag_device_sender: Sender<DiagDeviceCtrlMessage>,
daemon_restart_rx: oneshot::Receiver<()>,
should_restart_flag: Arc<AtomicBool>,
server_shutdown_tx: oneshot::Sender<()>, server_shutdown_tx: oneshot::Sender<()>,
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>, maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
maybe_key_input_shutdown_tx: Option<oneshot::Sender<()>>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_tx: Sender<AnalysisCtrlMessage>, analysis_tx: Sender<AnalysisCtrlMessage>,
) -> JoinHandle<Result<(), RayhunterError>> { ) -> JoinHandle<Result<(), RayhunterError>> {
info!("create shutdown thread");
task_tracker.spawn(async move { task_tracker.spawn(async move {
match tokio::signal::ctrl_c().await { select! {
Ok(()) => { res = tokio::signal::ctrl_c() => {
let mut qmdl_store = qmdl_store_lock.write().await; if let Err(err) = res {
if qmdl_store.current_entry.is_some() { error!("Unable to listen for shutdown signal: {}", err);
info!("Closing current QMDL entry...");
qmdl_store.close_current_entry().await?;
info!("Done!");
} }
server_shutdown_tx should_restart_flag.store(false, Ordering::Relaxed);
.send(()) }
.expect("couldn't send server shutdown signal"); res = daemon_restart_rx => {
info!("sending UI shutdown"); if let Err(err) = res {
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx { error!("Unable to listen for shutdown signal: {}", err);
ui_shutdown_tx
.send(())
.expect("couldn't send ui shutdown signal");
} }
diag_device_sender
.send(DiagDeviceCtrlMessage::Exit) should_restart_flag.store(true, Ordering::Relaxed);
.await
.expect("couldn't send Exit message to diag thread");
analysis_tx
.send(AnalysisCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to analysis thread");
}
Err(err) => {
error!("Unable to listen for shutdown signal: {}", err);
} }
};
let mut qmdl_store = qmdl_store_lock.write().await;
if qmdl_store.current_entry.is_some() {
info!("Closing current QMDL entry...");
qmdl_store.close_current_entry().await?;
info!("Done!");
} }
server_shutdown_tx
.send(())
.expect("couldn't send server shutdown signal");
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
let _ = ui_shutdown_tx.send(());
}
if let Some(key_input_shutdown_tx) = maybe_key_input_shutdown_tx {
let _ = key_input_shutdown_tx.send(());
}
diag_device_sender
.send(DiagDeviceCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to diag thread");
analysis_tx
.send(AnalysisCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to analysis thread");
Ok(()) Ok(())
}) })
} }
@@ -167,8 +186,19 @@ async fn main() -> Result<(), RayhunterError> {
env_logger::init(); env_logger::init();
let args = parse_args(); let args = parse_args();
let config = parse_config(&args.config_path)?;
loop {
let config = parse_config(&args.config_path).await?;
if !run_with_config(&args, config).await? {
return Ok(());
}
}
}
async fn run_with_config(
args: &config::Args,
config: config::Config,
) -> Result<bool, RayhunterError> {
// TaskTrackers give us an interface to spawn tokio threads, and then // TaskTrackers give us an interface to spawn tokio threads, and then
// eventually await all of them ending // eventually await all of them ending
let task_tracker = TaskTracker::new(); let task_tracker = TaskTracker::new();
@@ -181,6 +211,7 @@ async fn main() -> Result<(), RayhunterError> {
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1); let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5); let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None; let mut maybe_ui_shutdown_tx = None;
let mut maybe_key_input_shutdown_tx = None;
if !config.debug_mode { if !config.debug_mode {
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel(); let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
maybe_ui_shutdown_tx = Some(ui_shutdown_tx); maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
@@ -206,10 +237,18 @@ async fn main() -> Result<(), RayhunterError> {
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
info!("Starting Key Input service"); info!("Starting Key Input service");
key_input::run_key_input_thread(&task_tracker, &config, diag_tx.clone()); let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel();
maybe_key_input_shutdown_tx = Some(key_input_shutdown_tx);
key_input::run_key_input_thread(
&task_tracker,
&config,
diag_tx.clone(),
key_input_shutdown_rx,
);
} }
let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>();
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>(); let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
info!("create shutdown thread");
let analysis_status_lock = Arc::new(RwLock::new(analysis_status)); let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
run_analysis_thread( run_analysis_thread(
&task_tracker, &task_tracker,
@@ -219,29 +258,36 @@ async fn main() -> Result<(), RayhunterError> {
config.enable_dummy_analyzer, config.enable_dummy_analyzer,
config.analyzers.clone(), config.analyzers.clone(),
); );
run_ctrl_c_thread( let should_restart_flag = Arc::new(AtomicBool::new(false));
run_shutdown_thread(
&task_tracker, &task_tracker,
diag_tx.clone(), diag_tx.clone(),
daemon_restart_rx,
should_restart_flag.clone(),
server_shutdown_tx, server_shutdown_tx,
maybe_ui_shutdown_tx, maybe_ui_shutdown_tx,
maybe_key_input_shutdown_tx,
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_tx.clone(), analysis_tx.clone(),
); );
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
config_path: args.config_path.clone(),
config,
qmdl_store_lock: qmdl_store_lock.clone(), qmdl_store_lock: qmdl_store_lock.clone(),
diag_device_ctrl_sender: diag_tx, diag_device_ctrl_sender: diag_tx,
ui_update_sender: ui_update_tx, ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode,
analysis_status_lock, analysis_status_lock,
analysis_sender: analysis_tx, analysis_sender: analysis_tx,
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
}); });
run_server(&task_tracker, &config, state, server_shutdown_rx).await; run_server(&task_tracker, state, server_shutdown_rx).await;
task_tracker.close(); task_tracker.close();
task_tracker.wait().await; task_tracker.wait().await;
info!("see you space cowboy..."); info!("see you space cowboy...");
Ok(()) Ok(should_restart_flag.load(Ordering::Relaxed))
} }
#[cfg(test)] #[cfg(test)]
+4 -4
View File
@@ -158,7 +158,7 @@ pub fn run_diag_read_thread(
pub async fn start_recording( pub async fn start_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
@@ -179,7 +179,7 @@ pub async fn start_recording(
pub async fn stop_recording( pub async fn stop_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
state state
@@ -199,7 +199,7 @@ pub async fn delete_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>, Path(qmdl_name): Path<String>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await; let mut qmdl_store = state.qmdl_store_lock.write().await;
@@ -244,7 +244,7 @@ pub async fn delete_recording(
pub async fn delete_all_recordings( pub async fn delete_all_recordings(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
state state
+14 -4
View File
@@ -1,8 +1,9 @@
use log::error; use log::{error, info};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config;
@@ -20,6 +21,7 @@ pub fn run_key_input_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
diag_tx: Sender<DiagDeviceCtrlMessage>, diag_tx: Sender<DiagDeviceCtrlMessage>,
mut ui_shutdown_rx: oneshot::Receiver<()>,
) { ) {
if config.key_input_mode == 0 { if config.key_input_mode == 0 {
return; return;
@@ -40,9 +42,17 @@ pub fn run_key_input_thread(
let mut last_event_time: Option<Instant> = None; let mut last_event_time: Option<Instant> = None;
loop { loop {
if let Err(e) = file.read_exact(&mut buffer).await { tokio::select! {
error!("failed to read key input: {}", e); _ = &mut ui_shutdown_rx => {
return; info!("received key input shutdown");
return;
}
result = file.read_exact(&mut buffer) => {
if let Err(e) = result {
error!("failed to read key input: {}", e);
return;
}
}
} }
let event = parse_event(buffer); let event = parse_event(buffer);
+64 -10
View File
@@ -8,27 +8,32 @@ use axum::extract::State;
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE}; use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderValue, StatusCode}; use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::Json;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use log::error; use log::error;
use std::sync::Arc; use std::sync::Arc;
use tokio::fs::write;
use tokio::io::{copy, duplex, AsyncReadExt}; use tokio::io::{copy, duplex, AsyncReadExt};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::RwLock; use tokio::sync::{oneshot, RwLock};
use tokio_util::compat::FuturesAsyncWriteCompatExt; use tokio_util::compat::FuturesAsyncWriteCompatExt;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
use crate::config::Config;
use crate::pcap::generate_pcap_data; use crate::pcap::generate_pcap_data;
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
use crate::{display, DiagDeviceCtrlMessage}; use crate::{display, DiagDeviceCtrlMessage};
pub struct ServerState { pub struct ServerState {
pub config_path: String,
pub config: Config,
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>, pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>, pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub ui_update_sender: Sender<display::DisplayState>, pub ui_update_sender: Sender<display::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>, pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>, pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool, pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
} }
pub async fn get_qmdl( pub async fn get_qmdl(
@@ -41,12 +46,15 @@ pub async fn get_qmdl(
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
format!("couldn't find qmdl file with name {}", qmdl_idx), format!("couldn't find qmdl file with name {}", qmdl_idx),
))?; ))?;
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await.map_err(|e| { let qmdl_file = qmdl_store
( .open_entry_qmdl(entry_index)
StatusCode::INTERNAL_SERVER_ERROR, .await
format!("error opening QMDL file: {}", e), .map_err(|err| {
) (
})?; StatusCode::INTERNAL_SERVER_ERROR,
format!("error opening QMDL file: {}", err),
)
})?;
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
let qmdl_stream = ReaderStream::new(limited_qmdl_file); let qmdl_stream = ReaderStream::new(limited_qmdl_file);
@@ -84,6 +92,51 @@ pub async fn serve_static(
} }
} }
pub async fn get_config(
State(state): State<Arc<ServerState>>,
) -> Result<Json<Config>, (StatusCode, String)> {
Ok(Json(state.config.clone()))
}
pub async fn set_config(
State(state): State<Arc<ServerState>>,
Json(config): Json<Config>,
) -> Result<(StatusCode, String), (StatusCode, String)> {
let config_str = toml::to_string_pretty(&config).map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to serialize config as TOML: {}", err),
)
})?;
write(&state.config_path, config_str).await.map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to write config file: {}", err),
)
})?;
// Trigger daemon restart after writing config
let mut restart_tx = state.daemon_restart_tx.write().await;
if let Some(sender) = restart_tx.take() {
sender.send(()).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"couldn't send restart signal".to_string(),
)
})?;
Ok((
StatusCode::ACCEPTED,
"wrote config and triggered restart".to_string(),
))
} else {
Ok((
StatusCode::ACCEPTED,
"wrote config but restart already triggered".to_string(),
))
}
}
pub async fn get_zip( pub async fn get_zip(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
Path(entry_name): Path<String>, Path(entry_name): Path<String>,
@@ -180,7 +233,6 @@ mod tests {
use super::*; use super::*;
use async_zip::base::read::mem::ZipFileReader; use async_zip::base::read::mem::ZipFileReader;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use std::io::Cursor;
use tempfile::TempDir; use tempfile::TempDir;
async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) { async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) {
@@ -235,12 +287,14 @@ mod tests {
}; };
Arc::new(ServerState { Arc::new(ServerState {
config_path: "/tmp/test_config.toml".to_string(),
config: Config::default(),
qmdl_store_lock: store_lock, qmdl_store_lock: store_lock,
diag_device_ctrl_sender: tx, diag_device_ctrl_sender: tx,
ui_update_sender: ui_tx, ui_update_sender: ui_tx,
analysis_status_lock: Arc::new(RwLock::new(analysis_status)), analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
analysis_sender: analysis_tx, analysis_sender: analysis_tx,
debug_mode: true, daemon_restart_tx: Arc::new(RwLock::new(None)),
}) })
} }
@@ -0,0 +1,194 @@
<script lang="ts">
import { get_config, set_config, type Config } from '../utils.svelte';
let config = $state<Config | null>(null);
let loading = $state(false);
let saving = $state(false);
let message = $state("");
let messageType = $state<"success" | "error" | null>(null);
let showConfig = $state(false);
async function loadConfig() {
try {
loading = true;
config = await get_config();
message = "";
messageType = null;
} catch (error) {
message = `Failed to load config: ${error}`;
messageType = "error";
} finally {
loading = false;
}
}
async function saveConfig() {
if (!config) return;
try {
saving = true;
await set_config(config);
message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.";
messageType = "success";
} catch (error) {
message = `Failed to save config: ${error}`;
messageType = "error";
} finally {
saving = false;
}
}
// Load config when first shown
$effect(() => {
if (showConfig && !config) {
loadConfig();
}
});
</script>
<div class="bg-white rounded-lg shadow-md p-6 m-4">
<button
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
onclick={() => showConfig = !showConfig}
>
<span>Configuration</span>
<svg class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
{#if showConfig}
{#if loading}
<div class="text-center py-4">Loading config...</div>
{:else if config}
<form class="space-y-4" onsubmit={(e) => { e.preventDefault(); saveConfig(); }}>
<div>
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
Device UI Level
</label>
<select
id="ui_level"
bind:value={config.ui_level}
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 - Invisible mode</option>
<option value={1}>1 - Subtle mode (colored line)</option>
<option value={2}>2 - Demo mode (orca gif)</option>
<option value={3}>3 - EFF logo</option>
</select>
</div>
<div>
<label for="key_input_mode" class="block text-sm font-medium text-gray-700 mb-1">
Device Input Mode
</label>
<select
id="key_input_mode"
bind:value={config.key_input_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 - Disable button control</option>
<option value={1}>1 - Double-tap power button to start/stop recording</option>
</select>
</div>
<div class="space-y-3">
<div class="flex items-center">
<input
id="colorblind_mode"
type="checkbox"
bind:checked={config.colorblind_mode}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="colorblind_mode" class="ml-2 block text-sm text-gray-700">
Colorblind Mode
</label>
</div>
</div>
<div class="border-t pt-4 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analyzer Heuristic Settings</h3>
<div class="space-y-3">
<div class="flex items-center">
<input
id="imsi_requested"
type="checkbox"
bind:checked={config.analyzers.imsi_requested}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="imsi_requested" class="ml-2 block text-sm text-gray-700">
IMSI Requested Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="connection_redirect_2g_downgrade"
type="checkbox"
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="connection_redirect_2g_downgrade" class="ml-2 block text-sm text-gray-700">
Connection Redirect 2G Downgrade Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="lte_sib6_and_7_downgrade"
type="checkbox"
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="lte_sib6_and_7_downgrade" class="ml-2 block text-sm text-gray-700">
LTE SIB6 and SIB7 Downgrade Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="null_cipher"
type="checkbox"
bind:checked={config.analyzers.null_cipher}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="null_cipher" class="ml-2 block text-sm text-gray-700">
Null Cipher Heuristic
</label>
</div>
</div>
</div>
<div class="flex gap-2 pt-4">
<button
type="submit"
disabled={saving}
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
>
{#if saving}
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Saving...
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Apply and restart
{/if}
</button>
</div>
</form>
{#if message}
<div class="mt-4 p-3 rounded {messageType === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}">
{message}
</div>
{/if}
{:else}
<div class="text-center py-4 text-red-600">
Failed to load configuration. Please try reloading the page.
</div>
{/if}
{/if}
</div>
+33
View File
@@ -1,6 +1,20 @@
import { Manifest } from "./manifest.svelte"; import { Manifest } from "./manifest.svelte";
import type { SystemStats } from "./systemStats"; import type { SystemStats } from "./systemStats";
export interface AnalyzerConfig {
imsi_requested: boolean;
connection_redirect_2g_downgrade: boolean;
lte_sib6_and_7_downgrade: boolean;
null_cipher: boolean;
}
export interface Config {
ui_level: number;
colorblind_mode: boolean;
key_input_mode: number;
analyzers: AnalyzerConfig;
}
export async function req(method: string, url: string): Promise<string> { export async function req(method: string, url: string): Promise<string> {
const response = await fetch(url, { const response = await fetch(url, {
method: method, method: method,
@@ -21,3 +35,22 @@ export async function get_manifest(): Promise<Manifest> {
export async function get_system_stats(): Promise<SystemStats> { export async function get_system_stats(): Promise<SystemStats> {
return JSON.parse(await req('GET', '/api/system-stats')); return JSON.parse(await req('GET', '/api/system-stats'));
} }
export async function get_config(): Promise<Config> {
return JSON.parse(await req('GET', '/api/config'));
}
export async function set_config(config: Config): Promise<void> {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config)
});
if (!response.ok) {
const error = await response.text();
throw new Error(error);
}
}
+2
View File
@@ -8,6 +8,7 @@
import SystemStatsTable from "$lib/components/SystemStatsTable.svelte"; import SystemStatsTable from "$lib/components/SystemStatsTable.svelte";
import DeleteAllButton from "$lib/components/DeleteAllButton.svelte"; import DeleteAllButton from "$lib/components/DeleteAllButton.svelte";
import RecordingControls from "$lib/components//RecordingControls.svelte"; import RecordingControls from "$lib/components//RecordingControls.svelte";
import ConfigForm from "$lib/components/ConfigForm.svelte";
let manager: AnalysisManager = new AnalysisManager(); let manager: AnalysisManager = new AnalysisManager();
let loaded = $state(false); let loaded = $state(false);
@@ -75,6 +76,7 @@
<ManifestTable entries={entries} server_is_recording={recording} /> <ManifestTable entries={entries} server_is_recording={recording} />
</div> </div>
<DeleteAllButton/> <DeleteAllButton/>
<ConfigForm />
{:else} {:else}
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">
<img src="/rayhunter_orca_only.png" class="h-48 animate-spin"/> <img src="/rayhunter_orca_only.png" class="h-48 animate-spin"/>
+1 -1
View File
@@ -11,7 +11,7 @@ use super::{
null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
}; };
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)] #[serde(default)]
pub struct AnalyzerConfig { pub struct AnalyzerConfig {
pub imsi_requested: bool, pub imsi_requested: bool,
+50 -2
View File
@@ -7,12 +7,14 @@ use crate::log_codes;
use deku::prelude::*; use deku::prelude::*;
use futures::TryStream; use futures::TryStream;
use log::{error, info}; use log::{debug, error, info};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::sleep;
pub type DiagResult<T> = Result<T, DiagDeviceError>; pub type DiagResult<T> = Result<T, DiagDeviceError>;
@@ -85,6 +87,52 @@ pub struct DiagDevice {
impl DiagDevice { impl DiagDevice {
pub async fn new() -> DiagResult<Self> { pub async fn new() -> DiagResult<Self> {
Self::new_with_retries(Duration::from_secs(30)).await
}
pub async fn new_with_retries(max_duration: Duration) -> DiagResult<Self> {
// For some reason the diag device needs a very long time to become available again with in
// the same process, on TP-Link M7350 v3. While process restart would reset it faster.
let start_time = std::time::Instant::now();
let max_delay = Duration::from_secs(5);
let mut delay = Duration::from_millis(100);
let mut num_retries = 0;
loop {
match Self::try_new().await {
Ok(device) => {
info!(
"Diag device initialization succeeded after {} retries",
num_retries
);
return Ok(device);
}
Err(e) => {
num_retries += 1;
if start_time.elapsed() >= max_duration {
error!(
"Failed to initialize diag device after {:?}: {}",
max_duration, e
);
return Err(e);
}
info!(
"Diag device initialization failed {} times, retrying in {:?}: {}",
num_retries, delay, e
);
sleep(delay).await;
// Exponential backoff
delay = std::cmp::min(delay * 2, max_delay);
}
}
}
}
async fn try_new() -> DiagResult<Self> {
let diag_file = File::options() let diag_file = File::options()
.read(true) .read(true)
.write(true) .write(true)
@@ -123,7 +171,7 @@ impl DiagDevice {
.map_err(DiagDeviceError::DeviceReadFailed)?; .map_err(DiagDeviceError::DeviceReadFailed)?;
} }
info!( debug!(
"Parsing messages container size = {:?} [{:?}]", "Parsing messages container size = {:?} [{:?}]",
bytes_read, bytes_read,
&self.read_buf[0..bytes_read] &self.read_buf[0..bytes_read]