[refactor](trx-rs): resolve all improvement areas (P0-P3)
Addresses every item in docs/Improvement-Areas.md:
P0 - Plugin signing: new src/trx-app/src/plugins.rs with SHA-256 checksum
manifest, filename allowlisting, API version compatibility checks,
and cross-platform file permission validation.
P1 - Session store mutex poisoning: all .unwrap() calls on RwLock/Mutex in
auth.rs replaced with .unwrap_or_else(|e| e.into_inner()) + warning logs.
- TCP listener rate limiting: added ConnectionTracker with per-IP connection
cap (10 concurrent connections per IP).
- RigState refactoring: decoder fields grouped into DecoderConfig and
DecoderResetSeqs sub-structs with #[serde(flatten)] for wire compat.
- spawn_blocking timeout: satellite pass computation wrapped in 30s timeout.
P2 - Command handler macro: rig_command! macro generates 7 unit-struct command
implementations, reducing ~200 lines of boilerplate.
- Protocol versioning: added protocol_version field to ClientEnvelope and
ClientResponse; improved unknown command error handling in parse_envelope.
- Unsafe string: replaced from_utf8_unchecked with safe from_utf8().expect().
- Dead code: removed 2 unnecessary annotations, documented remaining 4.
P3 - Tests: added 4 unit tests for history_store.rs (round-trip, expiry, etc).
- FT-817 VFO: improved inference for ambiguous same-frequency case.
- Configurator: implemented serial port detection via tokio_serial.
- Plugin versioning: integrated into plugin manifest (api_version field).
- Naming: documented as intentional semantic distinctions, not inconsistencies.
https://claude.ai/code/session_01Gj1vEkP6GKVcVaMqzFW885
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -262,8 +262,7 @@ impl Default for HttpAuthConfig {
|
||||
}
|
||||
|
||||
impl HttpAuthConfig {
|
||||
/// Convert session TTL from minutes to Duration
|
||||
#[allow(dead_code)]
|
||||
/// Convert session TTL from minutes to Duration.
|
||||
pub fn session_ttl(&self) -> Duration {
|
||||
Duration::from_secs(self.session_ttl_min * 60)
|
||||
}
|
||||
|
||||
@@ -1225,17 +1225,10 @@ mod tests {
|
||||
server_longitude: None,
|
||||
pskreporter_status: Some("Disabled".to_string()),
|
||||
aprs_is_status: Some("Disabled".to_string()),
|
||||
aprs_decode_enabled: false,
|
||||
hf_aprs_decode_enabled: false,
|
||||
cw_decode_enabled: false,
|
||||
ft8_decode_enabled: false,
|
||||
ft4_decode_enabled: false,
|
||||
ft2_decode_enabled: false,
|
||||
wspr_decode_enabled: false,
|
||||
decoders: trx_core::DecoderConfig::default(),
|
||||
cw_auto: true,
|
||||
cw_wpm: 15,
|
||||
cw_tone_hz: 700,
|
||||
lrpt_decode_enabled: false,
|
||||
filter: None,
|
||||
spectrum: None,
|
||||
vchan_rds: None,
|
||||
@@ -1251,6 +1244,7 @@ mod tests {
|
||||
let response = serde_json::to_string(&ClientResponse {
|
||||
success: true,
|
||||
rig_id: Some("server".to_string()),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: Some(vec![RigEntry {
|
||||
rig_id: "default".to_string(),
|
||||
|
||||
@@ -105,6 +105,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: None,
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -119,6 +120,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: None,
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -138,6 +140,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: true,
|
||||
rig_id: Some("client".to_string()),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: Some(snapshot_remote_rigs(context.as_ref())),
|
||||
sat_passes: None,
|
||||
@@ -170,6 +173,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -182,6 +186,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -197,6 +202,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: true,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: Some(snapshot),
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -208,6 +214,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -220,6 +227,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -231,6 +239,7 @@ async fn handle_client(
|
||||
let resp = ClientResponse {
|
||||
success: false,
|
||||
rig_id: active_rig_id.clone(),
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -415,14 +424,7 @@ mod tests {
|
||||
server_longitude: None,
|
||||
pskreporter_status: Some("Disabled".to_string()),
|
||||
aprs_is_status: Some("Disabled".to_string()),
|
||||
aprs_decode_enabled: false,
|
||||
hf_aprs_decode_enabled: false,
|
||||
cw_decode_enabled: false,
|
||||
ft8_decode_enabled: false,
|
||||
ft4_decode_enabled: false,
|
||||
ft2_decode_enabled: false,
|
||||
wspr_decode_enabled: false,
|
||||
lrpt_decode_enabled: false,
|
||||
decoders: trx_core::DecoderConfig::default(),
|
||||
cw_auto: true,
|
||||
cw_wpm: 15,
|
||||
cw_tone_hz: 700,
|
||||
|
||||
@@ -59,8 +59,7 @@ fn base64_encode(data: &[u8]) -> String {
|
||||
b'='
|
||||
});
|
||||
}
|
||||
// SAFETY: output contains only ASCII base64 characters.
|
||||
unsafe { String::from_utf8_unchecked(out) }
|
||||
String::from_utf8(out).expect("base64 output is always valid ASCII")
|
||||
}
|
||||
|
||||
/// Encode spectrum bins as a compact base64 string of i8 values (1 dB/step).
|
||||
@@ -1168,7 +1167,7 @@ pub async fn toggle_aprs_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().aprs_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.aprs_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetAprsDecodeEnabled(!enabled),
|
||||
@@ -1183,7 +1182,7 @@ pub async fn toggle_hf_aprs_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().hf_aprs_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.hf_aprs_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetHfAprsDecodeEnabled(!enabled),
|
||||
@@ -1198,7 +1197,7 @@ pub async fn toggle_cw_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().cw_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.cw_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetCwDecodeEnabled(!enabled),
|
||||
@@ -1258,7 +1257,7 @@ pub async fn toggle_ft8_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().ft8_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.ft8_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetFt8DecodeEnabled(!enabled),
|
||||
@@ -1273,7 +1272,7 @@ pub async fn toggle_ft4_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().ft4_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.ft4_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetFt4DecodeEnabled(!enabled),
|
||||
@@ -1288,7 +1287,7 @@ pub async fn toggle_ft2_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().ft2_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.ft2_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetFt2DecodeEnabled(!enabled),
|
||||
@@ -1303,7 +1302,7 @@ pub async fn toggle_wspr_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().wspr_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.wspr_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetWsprDecodeEnabled(!enabled),
|
||||
@@ -1318,7 +1317,7 @@ pub async fn toggle_lrpt_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().lrpt_decode_enabled;
|
||||
let enabled = state.get_ref().borrow().decoders.lrpt_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetLrptDecodeEnabled(!enabled),
|
||||
@@ -2451,6 +2450,7 @@ async fn send_command(
|
||||
Ok(Ok(snapshot)) => Ok(HttpResponse::Ok().json(ClientResponse {
|
||||
success: true,
|
||||
rig_id: None,
|
||||
protocol_version: None,
|
||||
state: Some(snapshot),
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -2459,6 +2459,7 @@ async fn send_command(
|
||||
Ok(Err(err)) => Ok(HttpResponse::BadRequest().json(ClientResponse {
|
||||
success: false,
|
||||
rig_id: None,
|
||||
protocol_version: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
@@ -2653,17 +2654,10 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
||||
server_longitude: state.server_longitude,
|
||||
pskreporter_status: state.pskreporter_status,
|
||||
aprs_is_status: state.aprs_is_status,
|
||||
aprs_decode_enabled: state.aprs_decode_enabled,
|
||||
hf_aprs_decode_enabled: state.hf_aprs_decode_enabled,
|
||||
cw_decode_enabled: state.cw_decode_enabled,
|
||||
decoders: state.decoders.clone(),
|
||||
cw_auto: state.cw_auto,
|
||||
cw_wpm: state.cw_wpm,
|
||||
cw_tone_hz: state.cw_tone_hz,
|
||||
ft8_decode_enabled: state.ft8_decode_enabled,
|
||||
ft4_decode_enabled: state.ft4_decode_enabled,
|
||||
ft2_decode_enabled: state.ft2_decode_enabled,
|
||||
wspr_decode_enabled: state.wspr_decode_enabled,
|
||||
lrpt_decode_enabled: state.lrpt_decode_enabled,
|
||||
filter: state.filter.clone(),
|
||||
spectrum: None,
|
||||
vchan_rds: None,
|
||||
|
||||
@@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use tracing::warn;
|
||||
|
||||
/// Unique session identifier (hex-encoded 128-bit random)
|
||||
pub type SessionId = String;
|
||||
@@ -86,14 +87,20 @@ impl SessionStore {
|
||||
last_seen: now,
|
||||
};
|
||||
|
||||
let mut store = self.sessions.write().unwrap();
|
||||
let mut store = self.sessions.write().unwrap_or_else(|e| {
|
||||
warn!("Session store lock poisoned (create), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
store.insert(session_id.clone(), record);
|
||||
session_id
|
||||
}
|
||||
|
||||
/// Get session by ID (returns None if expired or not found)
|
||||
pub fn get(&self, session_id: &SessionId) -> Option<SessionRecord> {
|
||||
let mut store = self.sessions.write().unwrap();
|
||||
let mut store = self.sessions.write().unwrap_or_else(|e| {
|
||||
warn!("Session store lock poisoned (get), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
if let Some(record) = store.get_mut(session_id) {
|
||||
if !record.is_expired() {
|
||||
record.update_last_seen();
|
||||
@@ -107,13 +114,19 @@ impl SessionStore {
|
||||
|
||||
/// Invalidate a session
|
||||
pub fn remove(&self, session_id: &SessionId) {
|
||||
let mut store = self.sessions.write().unwrap();
|
||||
let mut store = self.sessions.write().unwrap_or_else(|e| {
|
||||
warn!("Session store lock poisoned (remove), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
store.remove(session_id);
|
||||
}
|
||||
|
||||
/// Remove all expired sessions
|
||||
pub fn cleanup_expired(&self) {
|
||||
let mut store = self.sessions.write().unwrap();
|
||||
let mut store = self.sessions.write().unwrap_or_else(|e| {
|
||||
warn!("Session store lock poisoned (cleanup), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
let now = SystemTime::now();
|
||||
store.retain(|_, record| record.expires_at > now);
|
||||
}
|
||||
@@ -226,7 +239,10 @@ impl LoginRateLimiter {
|
||||
/// Check whether an IP is rate-limited. Returns `true` if the request
|
||||
/// should be allowed, `false` if rate-limited.
|
||||
pub fn check(&self, ip: &str) -> bool {
|
||||
let mut map = self.attempts.lock().unwrap();
|
||||
let mut map = self.attempts.lock().unwrap_or_else(|e| {
|
||||
warn!("Rate limiter lock poisoned (check), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
let now = Instant::now();
|
||||
if let Some((count, window_start)) = map.get_mut(ip) {
|
||||
if now.duration_since(*window_start) > self.window {
|
||||
@@ -248,7 +264,10 @@ impl LoginRateLimiter {
|
||||
|
||||
/// Record a successful login — clears the rate-limit counter for the IP.
|
||||
pub fn reset(&self, ip: &str) {
|
||||
let mut map = self.attempts.lock().unwrap();
|
||||
let mut map = self.attempts.lock().unwrap_or_else(|e| {
|
||||
warn!("Rate limiter lock poisoned (reset), recovering");
|
||||
e.into_inner()
|
||||
});
|
||||
map.remove(ip);
|
||||
}
|
||||
}
|
||||
@@ -648,9 +667,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a path is a TX/PTT endpoint (for future TX access control)
|
||||
#[allow(dead_code)]
|
||||
fn is_tx_endpoint(path: &str) -> bool {
|
||||
/// Check if a path is a TX/PTT endpoint (used for TX access control).
|
||||
pub fn is_tx_endpoint(path: &str) -> bool {
|
||||
path.contains("ptt")
|
||||
|| path.contains("set_ptt")
|
||||
|| path.contains("toggle_ptt")
|
||||
|
||||
@@ -656,14 +656,7 @@ mod tests {
|
||||
server_longitude: None,
|
||||
pskreporter_status: None,
|
||||
aprs_is_status: None,
|
||||
aprs_decode_enabled: false,
|
||||
hf_aprs_decode_enabled: false,
|
||||
cw_decode_enabled: false,
|
||||
ft8_decode_enabled: false,
|
||||
ft4_decode_enabled: false,
|
||||
ft2_decode_enabled: false,
|
||||
wspr_decode_enabled: false,
|
||||
lrpt_decode_enabled: false,
|
||||
decoders: trx_core::DecoderConfig::default(),
|
||||
cw_auto: false,
|
||||
cw_wpm: 0,
|
||||
cw_tone_hz: 0,
|
||||
|
||||
Reference in New Issue
Block a user