use crate::proto::{read_ndjson, send_msg, Out, Peer};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::BufReader;
use std::net::{TcpListener, TcpStream};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
struct Config {
token: Option<String>,
name: String,
}
fn config() -> &'static Mutex<Config> {
static C: OnceLock<Mutex<Config>> = OnceLock::new();
C.get_or_init(|| {
Mutex::new(Config {
token: std::env::var("ZWIRE_HOST_TOKEN")
.ok()
.filter(|t| !t.is_empty()),
name: hostname(),
})
})
}
fn hostname() -> String {
crate::osops::hostname().unwrap_or_else(|| "host".to_string())
}
pub fn configure(token: Option<String>, name: Option<String>) {
let mut c = config().lock().unwrap();
if let Some(t) = token.filter(|t| !t.is_empty()) {
c.token = Some(t);
}
if let Some(n) = name {
c.name = n;
}
}
fn token() -> Option<String> {
config().lock().unwrap().token.clone()
}
pub fn local_name() -> String {
config().lock().unwrap().name.clone()
}
pub fn auth_required() -> bool {
token().is_some()
}
pub fn token_ok(presented: Option<&str>) -> bool {
match token() {
None => true,
Some(tok) => presented == Some(tok.as_str()),
}
}
struct Link {
out: Out,
name: String,
}
fn links() -> &'static Mutex<(u64, HashMap<u64, Link>)> {
static L: OnceLock<Mutex<(u64, HashMap<u64, Link>)>> = OnceLock::new();
L.get_or_init(|| Mutex::new((0, HashMap::new())))
}
pub fn register_link(out: &Out, name: &str) -> u64 {
let mut g = links().lock().unwrap();
let stale: Vec<u64> =
g.1.iter()
.filter(|(_, l)| l.name == name)
.map(|(id, _)| *id)
.collect();
for id in stale {
g.1.remove(&id);
}
g.0 += 1;
let id = g.0;
g.1.insert(
id,
Link {
out: out.clone(),
name: name.to_string(),
},
);
id
}
pub fn unregister_link(id: u64) {
links().lock().unwrap().1.remove(&id);
}
pub fn peer_names() -> Vec<String> {
let mut names: Vec<String> = links()
.lock()
.unwrap()
.1
.values()
.map(|l| l.name.clone())
.collect();
names.sort();
names
}
pub fn broadcast(topic: &str, data: &Value) -> usize {
let outs: Vec<Out> = {
links()
.lock()
.unwrap()
.1
.values()
.map(|l| l.out.clone())
.collect()
};
let frame = json!({ "cmd": "peer_pub", "topic": topic, "data": data });
outs.iter().filter(|o| send_msg(o, &frame).is_ok()).count()
}
pub fn listen_tcp(addr: String) {
std::thread::spawn(move || {
let listener = match TcpListener::bind(&addr) {
Ok(l) => l,
Err(e) => {
eprintln!("zwire-host: tcp bind {addr}: {e}");
return;
}
};
eprintln!("zwire-host: peering on tcp {addr}");
for conn in listener.incoming().flatten() {
std::thread::spawn(move || handle_inbound(conn));
}
});
}
fn handle_inbound(stream: TcpStream) {
let Ok(rclone) = stream.try_clone() else {
return;
};
let out = Peer::ndjson(Box::new(stream));
let sess = crate::session::Session::guarded(auth_required());
crate::transport::serve_conn(BufReader::new(rclone), out, sess);
}
pub fn dial(addr: String) {
std::thread::spawn(move || loop {
if let Err(e) = try_dial(&addr) {
eprintln!("zwire-host: peer {addr}: {e}");
}
std::thread::sleep(Duration::from_secs(5));
});
}
fn try_dial(addr: &str) -> Result<(), String> {
let stream = TcpStream::connect(addr).map_err(|e| e.to_string())?;
let rclone = stream.try_clone().map_err(|e| e.to_string())?;
let out = Peer::ndjson(Box::new(stream));
let mut reader = BufReader::new(rclone);
send_msg(
&out,
&json!({ "cmd": "peer_hello", "token": token(), "name": local_name() }),
)
.map_err(|e| e.to_string())?;
let reply = read_ndjson(&mut reader).ok_or("peer closed during handshake")?;
if reply["ok"] != json!(true) {
return Err(reply["err"]
.as_str()
.unwrap_or("handshake rejected")
.to_string());
}
let peer_name = reply["name"].as_str().unwrap_or(addr).to_string();
let link = register_link(&out, &peer_name);
eprintln!("zwire-host: peered with {peer_name} ({addr})");
let mut sess = crate::session::Session::guarded(false);
while let Some(msg) = read_ndjson(&mut reader) {
sess.handle(&out, &msg);
}
unregister_link(link);
Ok(())
}
pub fn remote(addr: &str, request: &Value) -> Result<Value, String> {
let stream = TcpStream::connect(addr).map_err(|e| format!("connect {addr}: {e}"))?;
let _ = stream.set_read_timeout(Some(Duration::from_secs(15)));
let rclone = stream.try_clone().map_err(|e| e.to_string())?;
let out = Peer::ndjson(Box::new(stream));
let mut reader = BufReader::new(rclone);
if let Some(tok) = token() {
send_msg(&out, &json!({ "cmd": "auth", "token": tok })).map_err(|e| e.to_string())?;
let _ = read_ndjson(&mut reader); }
send_msg(&out, request).map_err(|e| e.to_string())?;
read_ndjson(&mut reader).ok_or_else(|| "no reply from remote".to_string())
}