From de0a76e01d9ee5aec1f02afc64793407980e5077 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Tue, 26 May 2026 07:33:18 +0200 Subject: [PATCH] fix(onvif): extract SOAP fault reason + log auth state + NTP setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SOAP errors now extract fault Reason/Text/Code from XML instead of dumping raw envelope. Logs whether ONVIF password was decrypted (has_pass=true/false). Added NTP config to pi-gen (pool.ntp.org + Google/Cloudflare fallback) — WSSE PasswordDigest fails with clock skew. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../01-install-kiosk/01-run-chroot.sh | 9 +++++ kiosk/src/onvif_events.rs | 40 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/01-run-chroot.sh b/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/01-run-chroot.sh index 6fd79c7..b3d4279 100755 --- a/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/01-run-chroot.sh +++ b/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/01-run-chroot.sh @@ -152,6 +152,15 @@ Inherits=betterframe-empty CURSOR chown bfkiosk:bfkiosk /home/bfkiosk/.icons/default/index.theme +# --- NTP — critical for WSSE auth (camera checks timestamp) --- +mkdir -p /etc/systemd/timesyncd.conf.d +cat > /etc/systemd/timesyncd.conf.d/betterframe.conf <<'NTP' +[Time] +NTP=0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org +FallbackNTP=time.google.com time.cloudflare.com +NTP +systemctl enable systemd-timesyncd 2>/dev/null || true + # --- Enable services, disable noise --- systemctl enable seatd systemctl enable betterframe-kiosk.service diff --git a/kiosk/src/onvif_events.rs b/kiosk/src/onvif_events.rs index 4452db0..8d5e187 100644 --- a/kiosk/src/onvif_events.rs +++ b/kiosk/src/onvif_events.rs @@ -136,7 +136,8 @@ fn run_subscription( let pass = password.unwrap_or(""); let event_url = format!("http://{host}:{port}/onvif/event_service"); - info!("onvif-events: cam {} ({}) subscribing at {event_url}", cam.id, cam.name); + let has_pass = !pass.is_empty(); + info!("onvif-events: cam {} ({}) subscribing at {event_url} user={user} has_pass={has_pass}", cam.id, cam.name); loop { if generation.upgrade().is_none() { @@ -258,12 +259,45 @@ fn soap_post(url: &str, action: &str, body: &str) -> Result { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().unwrap_or_default(); - let preview: String = body.chars().take(500).collect(); - return Err(format!("soap HTTP {status}: {preview}")); + let fault = extract_soap_fault(&body); + return Err(format!("soap HTTP {status}: {fault}")); } resp.text().map_err(|e| format!("soap body: {e}")) } +/// Extract a human-readable fault reason from SOAP XML, stripping envelope noise. +fn extract_soap_fault(xml: &str) -> String { + // Try common SOAP fault tags + for tag in &["Reason", "Text", "faultstring", "Detail", "Subcode"] { + if let Some(val) = extract_tag_ns(xml, tag) { + let trimmed = val.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + } + // Try Code/Value + if let Some(val) = extract_tag_ns(xml, "Value") { + let trimmed = val.trim(); + if !trimmed.is_empty() { + return format!("Code: {trimmed}"); + } + } + // Fallback: first 300 chars stripped of XML tags + let stripped: String = xml.replace(|c: char| c == '<', "\n<") + .lines() + .filter(|l| !l.trim_start().starts_with('<')) + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .collect::>() + .join(" "); + if stripped.is_empty() { + xml.chars().take(300).collect() + } else { + stripped.chars().take(300).collect() + } +} + fn create_pullpoint(url: &str, user: &str, pass: &str) -> Result { let header = wsse_header(user, pass); let body = soap_envelope(