secure chat addon

This commit is contained in:
ariska committed 2026-07-07 16:35:55 +07:00
1 parent 0715f5d66d
commit a6a4375281
4 files changed
+92 -25

No files matched your search

+15 -7
View File
@@ -26,6 +26,8 @@ use crate::serviceclient::ServiceClient;
const WINDOW_W: c_int = 960; const WINDOW_W: c_int = 960;
const WINDOW_H: c_int = 640; const WINDOW_H: c_int = 640;
const DEFAULT_WEBHUB_PORT: u16 = 9994;
const DEFAULT_BACKONE_PORT: u16 = 9993;
struct SecureChatCtx { struct SecureChatCtx {
client: ServiceClient, client: ServiceClient,
@@ -226,7 +228,7 @@ unsafe fn update_core_status() {
if c.session.hub_connected { if c.session.hub_connected {
"WebHub connected" "WebHub connected"
} else { } else {
"WebHub disconnected — messages may not send" "WebHub disconnected — incoming sync may pause"
} }
), ),
); );
@@ -296,10 +298,7 @@ unsafe fn update_send_enabled() {
if send_button.is_null() || compose_entry.is_null() { if send_button.is_null() || compose_entry.is_null() {
return; return;
} }
let c = ctx(); let ok = selected_network_id().is_some() && selected_contact().is_some();
let ok = c.session.hub_connected
&& selected_network_id().is_some()
&& selected_contact().is_some();
if ok { if ok {
libui::uiControlEnable(send_button.cast()); libui::uiControlEnable(send_button.cast());
libui::uiControlEnable(compose_entry.cast()); libui::uiControlEnable(compose_entry.cast());
@@ -780,9 +779,16 @@ unsafe extern "C" fn on_send_clicked(_: *mut libui::uiButton, _: *mut c_void) {
let name = self_display_name(&c.client); let name = self_display_name(&c.client);
let sender_id = c.client.self_address(); let sender_id = c.client.self_address();
let now = crate::serviceclient::ms_since_epoch(); let now = crate::serviceclient::ms_since_epoch();
let mut ports = vec![cv.contact.port];
if !ports.contains(&DEFAULT_WEBHUB_PORT) {
ports.push(DEFAULT_WEBHUB_PORT);
}
if !ports.contains(&DEFAULT_BACKONE_PORT) {
ports.push(DEFAULT_BACKONE_PORT);
}
let _ = c.hub_cmd.send(HubCommand::SendChatRemote { let _ = c.hub_cmd.send(HubCommand::SendChatRemote {
host: cv.contact.ip.clone(), host: cv.contact.ip.clone(),
port: cv.contact.port, ports,
network_id: network_id.to_lowercase(), network_id: network_id.to_lowercase(),
sender_id: sender_id.to_lowercase(), sender_id: sender_id.to_lowercase(),
target_id: cv.contact.peer_id.to_lowercase(), target_id: cv.contact.peer_id.to_lowercase(),
@@ -887,6 +893,7 @@ unsafe extern "C" fn on_timer(_: *mut c_void) -> c_int {
&c.client.self_address(), &c.client.self_address(),
&name, &name,
&cv.contact.peer_id, &cv.contact.peer_id,
&cv.contact.ip,
&body, &body,
); );
let now = crate::serviceclient::ms_since_epoch(); let now = crate::serviceclient::ms_since_epoch();
@@ -1066,8 +1073,9 @@ pub fn secure_chat_main() {
} }
let auth_token = client.auth_token().to_string(); let auth_token = client.auth_token().to_string();
let hub_host = client.webhub_host();
let hub_port = client.webhub_port(); let hub_port = client.webhub_port();
let (hub_cmd, hub_events) = spawn_webhub(auth_token, hub_port); let (hub_cmd, hub_events) = spawn_webhub(auth_token, hub_host, hub_port);
let ctx_box = Box::new(SecureChatCtx { let ctx_box = Box::new(SecureChatCtx {
client, client,
+20
View File
@@ -2,6 +2,8 @@ use serde_json::json;
use crate::serviceclient::ServiceClient; use crate::serviceclient::ServiceClient;
use super::state::load_member_ip_for_peer;
fn peer_path_hosts(client: &ServiceClient, target_id: &str) -> Vec<String> { fn peer_path_hosts(client: &ServiceClient, target_id: &str) -> Vec<String> {
let target = target_id.to_lowercase(); let target = target_id.to_lowercase();
let Some(peers) = client.http_get_json("/peer").and_then(|v| v.as_array().cloned()) else { let Some(peers) = client.http_get_json("/peer").and_then(|v| v.as_array().cloned()) else {
@@ -35,6 +37,14 @@ fn peer_path_hosts(client: &ServiceClient, target_id: &str) -> Vec<String> {
hosts hosts
} }
fn push_unique_host(hosts: &mut Vec<String>, host: &str) {
let host = host.split('/').next().unwrap_or(host).trim();
if host.is_empty() || hosts.iter().any(|candidate| candidate == host) {
return;
}
hosts.push(host.to_string());
}
fn post_relay_url( fn post_relay_url(
host: &str, host: &str,
port: u16, port: u16,
@@ -69,6 +79,7 @@ pub fn relay_chat_to_peer_paths(
sender_id: &str, sender_id: &str,
sender_name: &str, sender_name: &str,
target_id: &str, target_id: &str,
preferred_host: &str,
body: &str, body: &str,
) -> bool { ) -> bool {
let port = client.http_port(); let port = client.http_port();
@@ -85,8 +96,17 @@ pub fn relay_chat_to_peer_paths(
}); });
let payload_str = payload.to_string(); let payload_str = payload.to_string();
let token = client.auth_token(); let token = client.auth_token();
let mut hosts = Vec::new();
push_unique_host(&mut hosts, preferred_host);
if let Some(member_ip) = load_member_ip_for_peer(client, network_id, target_id) {
push_unique_host(&mut hosts, &member_ip);
}
for host in peer_path_hosts(client, target_id) { for host in peer_path_hosts(client, target_id) {
push_unique_host(&mut hosts, &host);
}
for host in hosts {
for use_auth in [true, false] { for use_auth in [true, false] {
let auth = if use_auth { Some(token) } else { None }; let auth = if use_auth { Some(token) } else { None };
let (status, resp_body) = post_relay_url(&host, port, auth, &payload_str); let (status, resp_body) = post_relay_url(&host, port, auth, &payload_str);
+28 -18
View File
@@ -37,7 +37,7 @@ pub enum HubCommand {
}, },
SendChatRemote { SendChatRemote {
host: String, host: String,
port: u16, ports: Vec<u16>,
network_id: String, network_id: String,
sender_id: String, sender_id: String,
target_id: String, target_id: String,
@@ -65,13 +65,23 @@ fn is_loopback_host(host: &str) -> bool {
fn send_chat_remote( fn send_chat_remote(
auth_token: &str, auth_token: &str,
host: &str, host: &str,
port: u16, ports: &[u16],
network_id: &str, network_id: &str,
sender_id: &str, sender_id: &str,
target_id: &str, target_id: &str,
sender_name: &str, sender_name: &str,
body: &str, body: &str,
) -> bool { ) -> bool {
let payload = json!({
"type": "chat_message",
"networkId": network_id.to_lowercase(),
"senderId": sender_id.to_lowercase(),
"targetId": target_id.to_lowercase(),
"senderName": sender_name,
"body": body,
});
for port in ports {
let url = if is_loopback_host(host) && !auth_token.is_empty() { let url = if is_loopback_host(host) && !auth_token.is_empty() {
format!( format!(
"ws://{}:{}/?auth={}", "ws://{}:{}/?auth={}",
@@ -84,7 +94,7 @@ fn send_chat_remote(
}; };
let Ok((mut socket, _)) = connect(&url) else { let Ok((mut socket, _)) = connect(&url) else {
return false; continue;
}; };
if let MaybeTlsStream::Plain(tcp) = socket.get_ref() { if let MaybeTlsStream::Plain(tcp) = socket.get_ref() {
let _ = tcp.set_read_timeout(Some(Duration::from_millis(3000))); let _ = tcp.set_read_timeout(Some(Duration::from_millis(3000)));
@@ -103,16 +113,8 @@ fn send_chat_remote(
)); ));
} }
let payload = json!({
"type": "chat_message",
"networkId": network_id.to_lowercase(),
"senderId": sender_id.to_lowercase(),
"targetId": target_id.to_lowercase(),
"senderName": sender_name,
"body": body,
});
if socket.send(Message::Text(payload.to_string())).is_err() { if socket.send(Message::Text(payload.to_string())).is_err() {
return false; continue;
} }
let deadline = std::time::Instant::now() + Duration::from_secs(3); let deadline = std::time::Instant::now() + Duration::from_secs(3);
@@ -121,17 +123,22 @@ fn send_chat_remote(
Ok(Message::Text(text)) => { Ok(Message::Text(text)) => {
if let Ok(msg) = serde_json::from_str::<Value>(&text) { if let Ok(msg) = serde_json::from_str::<Value>(&text) {
if msg.get("type").and_then(|t| t.as_str()) == Some("chat_delivery") { if msg.get("type").and_then(|t| t.as_str()) == Some("chat_delivery") {
return msg if msg
.get("delivered") .get("delivered")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(false)
{
return true;
}
break;
} }
} }
} }
Ok(Message::Close(_)) => break, Ok(Message::Close(_)) => break,
Err(e) => match e { Err(e) => match e {
tungstenite::Error::Io(ref io) tungstenite::Error::Io(ref io)
if io.kind() == ErrorKind::WouldBlock || io.kind() == ErrorKind::TimedOut => if io.kind() == ErrorKind::WouldBlock
|| io.kind() == ErrorKind::TimedOut =>
{ {
thread::sleep(Duration::from_millis(50)); thread::sleep(Duration::from_millis(50));
continue; continue;
@@ -141,11 +148,13 @@ fn send_chat_remote(
Ok(_) => {} Ok(_) => {}
} }
} }
}
false false
} }
pub fn spawn_webhub( pub fn spawn_webhub(
auth_token: String, auth_token: String,
host: String,
port: u16, port: u16,
) -> (Sender<HubCommand>, Receiver<HubEvent>) { ) -> (Sender<HubCommand>, Receiver<HubEvent>) {
let (cmd_tx, cmd_rx) = mpsc::channel::<HubCommand>(); let (cmd_tx, cmd_rx) = mpsc::channel::<HubCommand>();
@@ -159,7 +168,8 @@ pub fn spawn_webhub(
} }
let url = format!( let url = format!(
"ws://127.0.0.1:{}/?auth={}", "ws://{}:{}/?auth={}",
host,
port, port,
encode_query_component(auth_token.as_str()) encode_query_component(auth_token.as_str())
); );
@@ -204,7 +214,7 @@ pub fn spawn_webhub(
} }
HubCommand::SendChatRemote { HubCommand::SendChatRemote {
host, host,
port: remote_port, ports,
network_id, network_id,
sender_id, sender_id,
target_id, target_id,
@@ -217,7 +227,7 @@ pub fn spawn_webhub(
let delivered = send_chat_remote( let delivered = send_chat_remote(
&auth, &auth,
&host, &host,
remote_port, &ports,
&network_id, &network_id,
&sender_id, &sender_id,
&target_id, &target_id,
+29
View File
@@ -432,6 +432,10 @@ impl ServiceClient {
Self::webhub_port_from_local_conf().unwrap_or(9994) Self::webhub_port_from_local_conf().unwrap_or(9994)
} }
pub fn webhub_host(&self) -> String {
Self::webhub_host_from_local_conf().unwrap_or_else(|| String::from("127.0.0.1"))
}
fn webhub_port_from_local_conf() -> Option<u16> { fn webhub_port_from_local_conf() -> Option<u16> {
for path in [crate::GLOBAL_SERVICE_HOME_V2, crate::GLOBAL_SERVICE_HOME_V1] { for path in [crate::GLOBAL_SERVICE_HOME_V2, crate::GLOBAL_SERVICE_HOME_V1] {
let conf_path = Path::new(path).join("local.conf"); let conf_path = Path::new(path).join("local.conf");
@@ -452,6 +456,31 @@ impl ServiceClient {
None None
} }
fn webhub_host_from_local_conf() -> Option<String> {
for path in [crate::GLOBAL_SERVICE_HOME_V2, crate::GLOBAL_SERVICE_HOME_V1] {
let conf_path = Path::new(path).join("local.conf");
let Ok(body) = std::fs::read_to_string(conf_path) else {
continue;
};
let Ok(cfg) = serde_json::from_str::<Value>(&body) else {
continue;
};
let Some(bind) = cfg
.get("settings")
.and_then(|s| s.get("webhubBind"))
.and_then(|b| b.as_str())
.map(str::trim)
else {
continue;
};
if bind.is_empty() || bind.eq_ignore_ascii_case("overlay") {
continue;
}
return Some(bind.to_string());
}
None
}
pub fn self_address(&self) -> String { pub fn self_address(&self) -> String {
self.get_str(&["status", "address"]).to_lowercase() self.get_str(&["status", "address"]).to_lowercase()
} }