TUI client: WebSocket with HTTP fallback
poll_loop now: 1. Tries WebSocket connection to /v1/ws/<fingerprint> 2. On success: receives messages in real-time (instant push) 3. On disconnect: reconnects after 3 seconds 4. On WS failure: falls back to HTTP polling every 2 seconds Refactored message processing into shared functions: - process_incoming() handles raw bytes - process_wire_message() handles deserialized WireMessage - Used by both WS and HTTP paths Both CLI TUI and web client now use WebSocket: - No more HTTP polling spam in server logs - Messages arrive instantly on both clients - HTTP poll kept as fallback for scripts/mules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
warzone/Cargo.lock
generated
6
warzone/Cargo.lock
generated
@@ -2330,7 +2330,9 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2501,6 +2503,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
|
"native-tls",
|
||||||
"rand",
|
"rand",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -2654,6 +2657,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"libc",
|
"libc",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -2663,8 +2667,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sled",
|
"sled",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"warzone-protocol",
|
"warzone-protocol",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ bincode.workspace = true
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
url = "2"
|
||||||
|
|||||||
@@ -631,7 +631,99 @@ fn normfp(fp: &str) -> String {
|
|||||||
fp.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_lowercase()
|
fp.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll for incoming messages in the background.
|
/// Process a single incoming raw message (shared by WS and HTTP paths).
|
||||||
|
fn process_incoming(
|
||||||
|
raw: &[u8],
|
||||||
|
identity: &IdentityKeyPair,
|
||||||
|
db: &LocalDb,
|
||||||
|
messages: &Arc<Mutex<Vec<ChatLine>>>,
|
||||||
|
) {
|
||||||
|
match bincode::deserialize::<WireMessage>(raw) {
|
||||||
|
Ok(wire) => process_wire_message(wire, identity, db, messages),
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_wire_message(
|
||||||
|
wire: WireMessage,
|
||||||
|
identity: &IdentityKeyPair,
|
||||||
|
db: &LocalDb,
|
||||||
|
messages: &Arc<Mutex<Vec<ChatLine>>>,
|
||||||
|
) {
|
||||||
|
match wire {
|
||||||
|
WireMessage::KeyExchange {
|
||||||
|
sender_fingerprint,
|
||||||
|
sender_identity_encryption_key,
|
||||||
|
ephemeral_public,
|
||||||
|
used_one_time_pre_key_id,
|
||||||
|
ratchet_message,
|
||||||
|
} => {
|
||||||
|
let sender_fp = match Fingerprint::from_hex(&sender_fingerprint) {
|
||||||
|
Ok(fp) => fp,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let spk_secret = match db.load_signed_pre_key(1) {
|
||||||
|
Ok(Some(s)) => s,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let otpk_secret = if let Some(id) = used_one_time_pre_key_id {
|
||||||
|
db.take_one_time_pre_key(id).ok().flatten()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let their_id_x25519 = PublicKey::from(sender_identity_encryption_key);
|
||||||
|
let their_eph = PublicKey::from(ephemeral_public);
|
||||||
|
let shared_secret = match x3dh::respond(
|
||||||
|
identity, &spk_secret, otpk_secret.as_ref(), &their_id_x25519, &their_eph,
|
||||||
|
) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut state = RatchetState::init_bob(shared_secret, spk_secret);
|
||||||
|
match state.decrypt(&ratchet_message) {
|
||||||
|
Ok(plaintext) => {
|
||||||
|
let text = String::from_utf8_lossy(&plaintext).to_string();
|
||||||
|
let _ = db.save_session(&sender_fp, &state);
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
||||||
|
text,
|
||||||
|
is_system: false,
|
||||||
|
is_self: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WireMessage::Message {
|
||||||
|
sender_fingerprint,
|
||||||
|
ratchet_message,
|
||||||
|
} => {
|
||||||
|
let sender_fp = match Fingerprint::from_hex(&sender_fingerprint) {
|
||||||
|
Ok(fp) => fp,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut state = match db.load_session(&sender_fp) {
|
||||||
|
Ok(Some(s)) => s,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
match state.decrypt(&ratchet_message) {
|
||||||
|
Ok(plaintext) => {
|
||||||
|
let text = String::from_utf8_lossy(&plaintext).to_string();
|
||||||
|
let _ = db.save_session(&sender_fp, &state);
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
||||||
|
text,
|
||||||
|
is_system: false,
|
||||||
|
is_self: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real-time message loop via WebSocket (falls back to HTTP polling).
|
||||||
pub async fn poll_loop(
|
pub async fn poll_loop(
|
||||||
messages: Arc<Mutex<Vec<ChatLine>>>,
|
messages: Arc<Mutex<Vec<ChatLine>>>,
|
||||||
our_fp: String,
|
our_fp: String,
|
||||||
@@ -639,97 +731,51 @@ pub async fn poll_loop(
|
|||||||
db: Arc<LocalDb>,
|
db: Arc<LocalDb>,
|
||||||
client: ServerClient,
|
client: ServerClient,
|
||||||
) {
|
) {
|
||||||
|
let fp = normfp(&our_fp);
|
||||||
|
|
||||||
|
// Try WebSocket first
|
||||||
|
let ws_url = client.base_url
|
||||||
|
.replace("http://", "ws://")
|
||||||
|
.replace("https://", "wss://");
|
||||||
|
let ws_url = format!("{}/v1/ws/{}", ws_url, fp);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
match tokio_tungstenite::connect_async(&ws_url).await {
|
||||||
|
Ok((ws_stream, _)) => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: "Real-time connection established".into(),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
});
|
||||||
|
|
||||||
let raw_msgs = match client.poll_messages(&our_fp).await {
|
use futures_util::StreamExt;
|
||||||
Ok(m) => m,
|
let (_, mut read) = ws_stream.split();
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
for raw in &raw_msgs {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
match bincode::deserialize::<WireMessage>(raw) {
|
if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg {
|
||||||
Ok(WireMessage::KeyExchange {
|
process_incoming(&data, &identity, &db, &messages);
|
||||||
sender_fingerprint,
|
|
||||||
sender_identity_encryption_key,
|
|
||||||
ephemeral_public,
|
|
||||||
used_one_time_pre_key_id,
|
|
||||||
ratchet_message,
|
|
||||||
}) => {
|
|
||||||
let sender_fp = match Fingerprint::from_hex(&sender_fingerprint) {
|
|
||||||
Ok(fp) => fp,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let spk_secret = match db.load_signed_pre_key(1) {
|
|
||||||
Ok(Some(s)) => s,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let otpk_secret = if let Some(id) = used_one_time_pre_key_id {
|
|
||||||
db.take_one_time_pre_key(id).ok().flatten()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let their_id_x25519 = PublicKey::from(sender_identity_encryption_key);
|
|
||||||
let their_eph = PublicKey::from(ephemeral_public);
|
|
||||||
|
|
||||||
let shared_secret = match x3dh::respond(
|
|
||||||
&identity,
|
|
||||||
&spk_secret,
|
|
||||||
otpk_secret.as_ref(),
|
|
||||||
&their_id_x25519,
|
|
||||||
&their_eph,
|
|
||||||
) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut state = RatchetState::init_bob(shared_secret, spk_secret);
|
|
||||||
match state.decrypt(&ratchet_message) {
|
|
||||||
Ok(plaintext) => {
|
|
||||||
let text = String::from_utf8_lossy(&plaintext).to_string();
|
|
||||||
let _ = db.save_session(&sender_fp, &state);
|
|
||||||
messages.lock().unwrap().push(ChatLine {
|
|
||||||
sender: sender_fingerprint[..12].to_string(),
|
|
||||||
text,
|
|
||||||
is_system: false,
|
|
||||||
is_self: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(WireMessage::Message {
|
|
||||||
sender_fingerprint,
|
|
||||||
ratchet_message,
|
|
||||||
}) => {
|
|
||||||
let sender_fp = match Fingerprint::from_hex(&sender_fingerprint) {
|
|
||||||
Ok(fp) => fp,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut state = match db.load_session(&sender_fp) {
|
messages.lock().unwrap().push(ChatLine {
|
||||||
Ok(Some(s)) => s,
|
sender: "system".into(),
|
||||||
_ => continue,
|
text: "Connection lost, reconnecting...".into(),
|
||||||
};
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
match state.decrypt(&ratchet_message) {
|
});
|
||||||
Ok(plaintext) => {
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
let text = String::from_utf8_lossy(&plaintext).to_string();
|
}
|
||||||
let _ = db.save_session(&sender_fp, &state);
|
Err(_) => {
|
||||||
messages.lock().unwrap().push(ChatLine {
|
// Fallback to HTTP polling
|
||||||
sender: sender_fingerprint[..12].to_string(),
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
text,
|
let raw_msgs = match client.poll_messages(&our_fp).await {
|
||||||
is_system: false,
|
Ok(m) => m,
|
||||||
is_self: false,
|
Err(_) => continue,
|
||||||
});
|
};
|
||||||
}
|
for raw in &raw_msgs {
|
||||||
Err(_) => continue,
|
process_incoming(raw, &identity, &db, &messages);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => continue,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user