use crate::identity::Identity;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const ANNOUNCE_PORT: u16 = 7789;
pub const QUERY_PORT: u16 = 7790;
pub const LISTEN_PORT: u16 = 7791;
fn env_u16(k: &str, default: u16) -> u16 {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn announce_bind() -> u16 {
env_u16("CTX_LAN_ANNOUNCE_BIND", ANNOUNCE_PORT)
}
fn announce_dest() -> u16 {
env_u16("CTX_LAN_ANNOUNCE_DEST", announce_bind())
}
fn query_bind() -> u16 {
env_u16("CTX_LAN_QUERY_BIND", QUERY_PORT)
}
fn query_dest() -> u16 {
env_u16("CTX_LAN_QUERY_DEST", query_bind())
}
fn local_ip() -> Option<std::net::IpAddr> {
let s = UdpSocket::bind(("0.0.0.0", 0)).ok()?;
s.connect(("1.1.1.1", 9)).ok()?;
Some(s.local_addr().ok()?.ip())
}
pub fn bcast_targets() -> Vec<String> {
if let Ok(v) = std::env::var("CTX_LAN_BCAST") {
return vec![v];
}
let mut out = vec!["255.255.255.255".to_string()];
if let Some(std::net::IpAddr::V4(ip)) = local_ip() {
let o = ip.octets();
if o[0] != 127 {
out.push(format!("{}.{}.{}.255", o[0], o[1], o[2]));
}
}
out
}
const MAX_PACKET: usize = 2048;
const MAX_SKEW: u64 = 120;
#[derive(Serialize, Deserialize, Clone)]
pub struct Packet {
pub t: String,
#[serde(default)]
pub handle: String,
pub sign: String,
#[serde(default)]
pub boxpub: String,
#[serde(default)]
pub port: u16,
pub at: u64,
#[serde(default)]
pub want: String,
#[serde(default)]
pub nonce: String,
pub sig: String,
}
fn signed_bytes(p: &Packet) -> Vec<u8> {
let mut out = b"gage-lan-1".to_vec();
for f in [
p.t.as_str(),
p.handle.as_str(),
p.sign.as_str(),
p.boxpub.as_str(),
&p.port.to_string(),
&p.at.to_string(),
p.want.as_str(),
p.nonce.as_str(),
] {
out.extend_from_slice(f.len().to_string().as_bytes());
out.push(b':');
out.extend_from_slice(f.as_bytes());
}
out
}
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn want_tag(sign_key: &str, nonce: &str) -> String {
crate::bundle::sha256_hex(format!("gage-lan-want|{}|{}", sign_key.trim(), nonce).as_bytes())
}
pub fn new_nonce() -> Option<String> {
use base64::{Engine, engine::general_purpose::STANDARD as B64};
let mut buf = [0u8; 18];
let mut f = std::fs::File::open("/dev/urandom").ok()?;
f.read_exact(&mut buf).ok()?;
Some(B64.encode(buf))
}
fn sign_packet(id: &Identity, mut p: Packet) -> Packet {
p.sig = id.sign_b64(&signed_bytes(&p));
p
}
pub fn parse(raw: &[u8]) -> Option<Packet> {
if raw.len() > MAX_PACKET {
return None;
}
let p: Packet = serde_json::from_slice(raw).ok()?;
if !matches!(p.t.as_str(), "announce" | "query" | "here") {
return None;
}
if !crate::bundle::b64_decode_ok(&p.sign) {
return None;
}
let n = now();
if p.at.abs_diff(n) > MAX_SKEW {
return None;
}
if !crate::identity::verify(&p.sign, &signed_bytes(&p), &p.sig) {
return None;
}
Some(p)
}
pub struct Seen {
pub handle: String,
pub sign: String,
pub boxpub: String,
pub addr: SocketAddr,
pub port: u16,
}
impl Seen {
pub fn token(&self) -> String {
if self.boxpub.is_empty() {
self.sign.clone()
} else {
format!("{}.{}", self.sign, self.boxpub)
}
}
}
pub fn nearby(
id: Option<&Identity>,
listen_port: u16,
secs: u64,
announce: bool,
) -> std::io::Result<Vec<Seen>> {
let ap = announce_bind();
let sock = UdpSocket::bind(("0.0.0.0", ap)).map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"could not bind udp {ap} ({e}). Another gage nearby is probably \
already running -- discovery wants one process per machine."
),
)
})?;
sock.set_broadcast(true)?;
sock.set_read_timeout(Some(Duration::from_millis(500)))?;
let mut out: Vec<Seen> = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(secs);
let mut last_shout = std::time::Instant::now() - Duration::from_secs(10);
while std::time::Instant::now() < deadline {
if announce
&& let Some(id) = id
&& last_shout.elapsed() >= Duration::from_secs(2)
{
let p = sign_packet(
id,
Packet {
t: "announce".into(),
handle: id.handle.clone(),
sign: id.public_b64(),
boxpub: id.box_public_b64(),
port: listen_port,
at: now(),
want: String::new(),
nonce: String::new(),
sig: String::new(),
},
);
let body = serde_json::to_vec(&p).unwrap_or_default();
for d in bcast_targets() {
let _ = sock.send_to(&body, (d.as_str(), announce_dest()));
}
last_shout = std::time::Instant::now();
}
let mut buf = [0u8; MAX_PACKET];
let Ok((n, from)) = sock.recv_from(&mut buf) else {
continue;
};
let Some(p) = parse(&buf[..n]) else { continue };
if p.t != "announce" {
continue;
}
if let Some(id) = id
&& p.sign == id.public_b64()
{
continue; }
let seen = Seen {
handle: p.handle,
sign: p.sign,
boxpub: p.boxpub,
addr: from,
port: p.port,
};
match out.iter().position(|s| s.sign == seen.sign) {
Some(i) => out[i] = seen,
None => out.push(seen),
}
}
Ok(out)
}
pub fn resolve(id: &Identity, want_sign: &str, secs: u64) -> std::io::Result<Option<Seen>> {
let nonce = new_nonce().ok_or_else(|| {
std::io::Error::other("no randomness available, so a query cannot be made safely")
})?;
let sock = UdpSocket::bind(("0.0.0.0", 0))?;
sock.set_broadcast(true)?;
sock.set_read_timeout(Some(Duration::from_millis(500)))?;
let p = sign_packet(
id,
Packet {
t: "query".into(),
handle: id.handle.clone(),
sign: id.public_b64(),
boxpub: String::new(),
port: 0,
at: now(),
want: want_tag(want_sign, &nonce),
nonce: nonce.clone(),
sig: String::new(),
},
);
let body = serde_json::to_vec(&p).unwrap_or_default();
let deadline = std::time::Instant::now() + Duration::from_secs(secs);
let mut last = std::time::Instant::now() - Duration::from_secs(10);
while std::time::Instant::now() < deadline {
if last.elapsed() >= Duration::from_secs(1) {
let mut sent = false;
let mut last_err = None;
for d in bcast_targets() {
match sock.send_to(&body, (d.as_str(), query_dest())) {
Ok(_) => sent = true,
Err(e) => last_err = Some(e),
}
}
if !sent {
return Err(last_err.unwrap_or_else(|| {
std::io::Error::other("no broadcast address could be used")
}));
}
last = std::time::Instant::now();
}
let mut buf = [0u8; MAX_PACKET];
let Ok((n, from)) = sock.recv_from(&mut buf) else {
continue;
};
let Some(a) = parse(&buf[..n]) else { continue };
if a.t != "here" || a.sign != want_sign.trim() || a.nonce != nonce {
continue;
}
return Ok(Some(Seen {
handle: a.handle,
sign: a.sign,
boxpub: a.boxpub,
addr: from,
port: a.port,
}));
}
Ok(None)
}
fn answer_queries(listen_port: u16) -> std::io::Result<()> {
let id = Identity::load()?;
let sock = UdpSocket::bind(("0.0.0.0", query_bind()))?;
let me = id.public_b64();
loop {
let mut buf = [0u8; MAX_PACKET];
let Ok((n, from)) = sock.recv_from(&mut buf) else {
continue;
};
let Some(q) = parse(&buf[..n]) else { continue };
if q.t != "query" || q.nonce.is_empty() {
continue;
}
if q.want != want_tag(&me, &q.nonce) {
continue;
}
if !crate::trust::Contacts::load().knows(&q.sign) {
eprintln!(
" ignored a query from a key not on your list ({}...)",
q.sign.chars().take(12).collect::<String>()
);
continue;
}
let a = sign_packet(
&id,
Packet {
t: "here".into(),
handle: id.handle.clone(),
sign: me.clone(),
boxpub: id.box_public_b64(),
port: listen_port,
at: now(),
want: String::new(),
nonce: q.nonce.clone(),
sig: String::new(),
},
);
let body = serde_json::to_vec(&a).unwrap_or_default();
let _ = sock.send_to(&body, from);
}
}
pub fn listen(bind: &str, port: u16) -> std::io::Result<()> {
let id = Identity::load()?;
let l = TcpListener::bind((bind, port))?;
println!(
"gage listen on {bind}:{port} (tcp) and 0.0.0.0:{} (udp queries)",
query_bind()
);
println!(" it answers a query only when the query is signed by a key on your list");
println!(" an envelope that arrives lands in inbox/pending -- arrival is not consent");
std::thread::spawn(move || {
if let Err(e) = answer_queries(port) {
eprintln!("gage listen: query responder stopped: {e}");
}
});
for s in l.incoming() {
match s {
Ok(s) => {
if let Err(e) = handle_inbound(s, &id) {
eprintln!("gage listen: {e}");
}
}
Err(e) => eprintln!("gage listen: accept: {e}"),
}
}
Ok(())
}
fn http_reply(mut s: TcpStream, status: &str, body: &str) -> std::io::Result<()> {
let h = format!(
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
s.write_all(h.as_bytes())?;
s.write_all(body.as_bytes())?;
s.flush()
}
fn handle_inbound(stream: TcpStream, id: &Identity) -> std::io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(60)))?;
let mut r = BufReader::new(stream.try_clone()?);
let mut line = String::new();
r.read_line(&mut line)?;
let mut it = line.split_whitespace();
let method = it.next().unwrap_or("").to_string();
let target = it.next().unwrap_or("/").to_string();
let mut len = 0usize;
loop {
let mut h = String::new();
if r.read_line(&mut h)? == 0 || h.trim().is_empty() {
break;
}
if let Some(v) = h.to_ascii_lowercase().strip_prefix("content-length:") {
len = v.trim().parse().unwrap_or(0);
}
}
if len > crate::relay::MAX_BODY {
return http_reply(stream, "413 Payload Too Large", r#"{"error":"too large"}"#);
}
let mut body = vec![0u8; len];
if len > 0 {
r.read_exact(&mut body)?;
}
match (method.as_str(), target.as_str()) {
("GET", "/health") => http_reply(stream, "200 OK", r#"{"ok":true}"#),
("POST", "/lan/send") => match accept_envelope(&body, id) {
Ok(msg) => {
println!(" {msg}");
http_reply(stream, "200 OK", r#"{"ok":true}"#)
}
Err(e) => {
eprintln!(" refused an envelope: {e}");
http_reply(stream, "400 Bad Request", r#"{"error":"refused"}"#)
}
},
_ => http_reply(stream, "404 Not Found", r#"{"error":"no such route"}"#),
}
}
fn accept_envelope(body: &[u8], id: &Identity) -> Result<String, String> {
let v: serde_json::Value = serde_json::from_slice(body).map_err(|e| e.to_string())?;
let raw = v
.get("envelope")
.and_then(|e| e.as_str())
.ok_or("no envelope")?;
let env: crate::seal::Envelope =
serde_json::from_str(raw).map_err(|e| format!("unreadable envelope: {e}"))?;
if env.to_box.trim() != id.box_public_b64() {
return Err("sealed to a different key than this machine holds".into());
}
let tmp = crate::identity::home().join(format!("tmp-lan-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let land = |tmp: &std::path::Path| -> Result<String, String> {
crate::seal::unseal_to_dir(&env, id, tmp).map_err(|e| e.to_string())?;
let loaded = crate::bundle::load(tmp).map_err(|e| e.to_string())?;
if !crate::manifest::id_is_safe(&loaded.manifest.id) {
return Err("a bundle whose id is not an id -- it would have become a path".into());
}
if !loaded.tampered.is_empty() {
return Err(loaded.tampered.join(", "));
}
let dest = crate::routes::pending_dir(&crate::identity::home()).join(&loaded.manifest.id);
if dest.exists() {
return Ok(format!(
"already have {} -- left in place",
loaded.manifest.id
));
}
std::fs::create_dir_all(dest.parent().unwrap()).map_err(|e| e.to_string())?;
crate::routes::deliver(tmp, &dest).map_err(|e| e.to_string())?;
let verdict = crate::bundle::load(&dest)
.map(|l| l.trust.label().to_string())
.unwrap_or_else(|e| format!("could not be read back: {e}"));
Ok(format!(
"{} {} ({verdict}) -- gage inbox, then accept or reject",
loaded.manifest.id, loaded.manifest.subject
))
};
let r = land(&tmp);
let _ = std::fs::remove_dir_all(&tmp);
r
}
pub fn deliver_to(addr: SocketAddr, body: &str) -> std::io::Result<()> {
let mut s = TcpStream::connect_timeout(&addr, Duration::from_secs(10))?;
s.set_read_timeout(Some(Duration::from_secs(60)))?;
s.set_write_timeout(Some(Duration::from_secs(60)))?;
let req = format!(
"POST /lan/send HTTP/1.1\r\nHost: {addr}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
s.write_all(req.as_bytes())?;
s.write_all(body.as_bytes())?;
s.flush()?;
let mut raw = Vec::new();
s.read_to_end(&mut raw)?;
let text = String::from_utf8_lossy(&raw).to_string();
let status = text.lines().next().unwrap_or("").to_string();
if !status.contains(" 200") {
return Err(std::io::Error::other(format!(
"the other machine refused it: {}",
status.trim()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn p(t: &str) -> Packet {
Packet {
t: t.into(),
handle: "laptop".into(),
sign: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".into(),
boxpub: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=".into(),
port: 7791,
at: 1_700_000_000,
want: String::new(),
nonce: "nnn".into(),
sig: String::new(),
}
}
#[test]
fn the_signing_input_is_unambiguous_across_handles() {
let mut a = p("announce");
a.handle = "a|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".into();
let mut b = p("announce");
b.handle = "a".into();
assert_ne!(signed_bytes(&a), signed_bytes(&b));
}
#[test]
fn the_kind_of_packet_is_inside_the_signature() {
assert_ne!(signed_bytes(&p("query")), signed_bytes(&p("here")));
}
#[test]
fn a_wanted_key_is_not_on_the_wire() {
let k = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
let tag = want_tag(k, "nonce-one");
assert!(!tag.contains("yomSmC"));
assert_eq!(tag, want_tag(k, "nonce-one"));
assert_ne!(tag, want_tag(k, "nonce-two"));
assert_ne!(
tag,
want_tag("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "nonce-one")
);
}
#[test]
fn a_pasted_key_with_whitespace_tags_the_same() {
let k = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
assert_eq!(want_tag(k, "n"), want_tag(&format!(" {k}\n"), "n"));
}
#[test]
fn nothing_unsigned_or_stale_parses() {
let mut q = p("announce");
q.at = now();
assert!(parse(&serde_json::to_vec(&q).unwrap()).is_none());
let mut odd = p("delete-everything");
odd.at = now();
assert!(parse(&serde_json::to_vec(&odd).unwrap()).is_none());
let mut big = p("announce");
big.at = now();
big.handle = "x".repeat(MAX_PACKET);
assert!(parse(&serde_json::to_vec(&big).unwrap()).is_none());
assert!(parse(b"hello").is_none());
}
#[test]
fn a_stale_packet_does_not_parse_even_correctly_signed() {
let q = p("announce"); assert!(parse(&serde_json::to_vec(&q).unwrap()).is_none());
}
#[test]
fn a_seen_peer_renders_the_token_ctx_add_takes() {
let s = Seen {
handle: "laptop".into(),
sign: "SIGN".into(),
boxpub: "BOX".into(),
addr: "10.0.0.2:7789".parse().unwrap(),
port: 7791,
};
assert_eq!(s.token(), "SIGN.BOX");
let bare = Seen {
boxpub: String::new(),
..s
};
assert_eq!(bare.token(), "SIGN");
}
}
pub fn nearby_path() -> std::path::PathBuf {
crate::identity::home().join("nearby.json")
}
#[derive(Serialize, Deserialize)]
pub struct NearbyEntry {
pub label: String,
pub handle: String,
pub sign: String,
pub boxpub: String,
pub addr: String,
pub at: u64,
}
pub fn read_nearby() -> Vec<NearbyEntry> {
std::fs::read_to_string(nearby_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn lookup_nearby(name: &str) -> Option<NearbyEntry> {
let want = name.trim().to_ascii_lowercase().replace('-', " ");
let seen = read_nearby();
seen.iter()
.position(|e| e.label == want || e.handle.to_ascii_lowercase() == want)
.or_else(|| {
let hits: Vec<usize> = seen
.iter()
.enumerate()
.filter(|(_, e)| e.label.starts_with(&want))
.map(|(i, _)| i)
.collect();
if hits.len() == 1 { Some(hits[0]) } else { None }
})
.map(|i| seen.into_iter().nth(i).unwrap())
}
pub fn run(port: u16) -> std::io::Result<()> {
let id = match Identity::load() {
Ok(i) => i,
Err(_) => {
let i = Identity::create_named(None)?;
println!("new identity, named after its own key: {}", i.handle);
println!(
" {}",
crate::words::fingerprint_of(&i.public_b64(), Some(&i.box_public_b64()))
);
println!();
i
}
};
let me = id.public_b64();
let my_label = crate::words::label(&me);
let tcp = TcpListener::bind(("0.0.0.0", port))?;
println!("gage lan -- you are \"{my_label}\" on this network");
println!(" visible and reachable while this runs, silent the moment you stop it");
println!(" anything sent to you lands unread in inbox/pending");
println!(" broadcasting to: {}", bcast_targets().join(", "));
println!();
let rid = Identity::load()?;
std::thread::spawn(move || {
for s in tcp.incoming() {
match s {
Ok(s) => {
if let Err(e) = handle_inbound(s, &rid) {
eprintln!("gage lan: {e}");
}
}
Err(e) => eprintln!("gage lan: accept: {e}"),
}
}
});
std::thread::spawn(move || {
if let Err(e) = answer_queries(port) {
eprintln!("gage lan: query responder stopped: {e}");
}
});
let _ = std::fs::remove_file(nearby_path());
let out = nearby_path();
let cleanup = out.clone();
let _ = ctrlc_remove(cleanup);
let sock = UdpSocket::bind(("0.0.0.0", announce_bind())).map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"could not bind udp {} ({e}). Another gage lan is already running on \
this machine.",
announce_bind()
),
)
})?;
sock.set_broadcast(true)?;
sock.set_read_timeout(Some(Duration::from_millis(500)))?;
let mut seen: Vec<NearbyEntry> = Vec::new();
let mut last_shout = std::time::Instant::now() - Duration::from_secs(10);
let mut shown = String::new();
loop {
if last_shout.elapsed() >= Duration::from_secs(2) {
let p = sign_packet(
&id,
Packet {
t: "announce".into(),
handle: id.handle.clone(),
sign: me.clone(),
boxpub: id.box_public_b64(),
port,
at: now(),
want: String::new(),
nonce: String::new(),
sig: String::new(),
},
);
let body = serde_json::to_vec(&p).unwrap_or_default();
for d in bcast_targets() {
let _ = sock.send_to(&body, (d.as_str(), announce_dest()));
}
last_shout = std::time::Instant::now();
seen.retain(|e| now().saturating_sub(e.at) <= 15);
}
let mut buf = [0u8; MAX_PACKET];
if let Ok((n, from)) = sock.recv_from(&mut buf)
&& let Some(p) = parse(&buf[..n])
&& p.t == "announce"
&& p.sign != me
{
let e = NearbyEntry {
label: crate::words::label(&p.sign),
handle: p.handle.clone(),
sign: p.sign.clone(),
boxpub: p.boxpub.clone(),
addr: format!("{}:{}", from.ip(), p.port),
at: now(),
};
match seen.iter().position(|x| x.sign == e.sign) {
Some(i) => seen[i] = e,
None => seen.push(e),
}
}
if let Ok(j) = serde_json::to_vec_pretty(&seen) {
let _ = std::fs::write(&out, j);
}
let c = crate::trust::Contacts::load();
let mut lines = String::new();
for e in &seen {
let known = match c.name_for(&e.sign) {
Some(n) => format!("paired, on your list as {n}"),
None => "not paired -- gage pair <name> to confirm its key".to_string(),
};
lines.push_str(&format!(" {} at {} ({known})\n", e.label, e.addr));
lines.push_str(&format!(
" says : {}\n",
crate::words::fingerprint_of(&e.sign, Some(&e.boxpub))
));
}
if lines != shown {
if lines.is_empty() {
println!("nobody else here yet. Run `gage lan` on the other machine.");
} else {
print!("{lines}");
}
let _ = std::io::stdout().flush();
shown = lines;
}
}
}
#[cfg(unix)]
fn ctrlc_remove(path: std::path::PathBuf) -> std::io::Result<()> {
use std::sync::OnceLock;
static PATH: OnceLock<std::path::PathBuf> = OnceLock::new();
let _ = PATH.set(path);
extern "C" fn on_int(_: i32) {
if let Some(p) = PATH.get() {
let _ = std::fs::remove_file(p);
}
std::process::exit(130);
}
crate::sig::install(crate::sig::SIGINT, on_int);
Ok(())
}
#[cfg(not(unix))]
fn ctrlc_remove(_path: std::path::PathBuf) -> std::io::Result<()> {
Ok(())
}
pub fn hand_over(dir: &std::path::Path, who: &str) -> Result<Vec<String>, String> {
let id = Identity::load().map_err(|e| e.to_string())?;
let c = crate::trust::Contacts::load();
let on_list = lookup_nearby(who).or_else(|| {
let k = c.key_for_name(who)?;
read_nearby().into_iter().find(|e| e.sign == k)
});
let (boxpub, addr, desc) = match on_list {
Some(e) => {
let age = now().saturating_sub(e.at);
if age > 60 {
return Err(format!(
"the entry for {who} is {age}s old, so it is not an address any more. \
Is `gage lan` still running on both machines?"
));
}
if e.boxpub.is_empty() {
return Err(format!(
"{who} announced no encryption key, so nothing can be sealed to it"
));
}
let known = match c.name_for(&e.sign) {
Some(n) => format!("paired, on your list as {n}"),
None => "NOT PAIRED -- it will arrive as SENDER NOT ON YOUR LIST".to_string(),
};
(
e.boxpub.clone(),
e.addr.clone(),
format!("{} ({known})", e.label),
)
}
None => {
let sign = c.key_for_name(who).ok_or_else(|| {
format!(
"no machine called {who} on this network, and no contact by that name.\n \
Run `gage lan` on both machines and use the word-pair it shows."
)
})?;
let boxpub = c
.box_keys
.get(&sign)
.ok_or(format!("{who} has no encryption key on file; re-add them"))?
.clone();
let seen = resolve(&id, &sign, 8)
.map_err(|e| e.to_string())?
.ok_or_else(|| {
format!(
"no answer from {who} on this network. They need `gage lan` or `gage \
listen` running, and you need to be on their list -- a query from \
a key they do not know is ignored, deliberately and silently."
)
})?;
(
boxpub,
format!("{}:{}", seen.addr.ip(), seen.port),
who.to_string(),
)
}
};
let env = crate::seal::seal_dir(dir, &boxpub).map_err(|e| e.to_string())?;
let body = serde_json::json!({
"to_box": boxpub,
"envelope": serde_json::to_string(&env).map_err(|e| e.to_string())? })
.to_string();
let sa: SocketAddr = addr
.parse()
.map_err(|e| format!("{who} announced an address that is not one ({addr}): {e}"))?;
deliver_to(sa, &body).map_err(|e| e.to_string())?;
Ok(vec![
format!(" handed to {desc} at {addr}, over this network"),
" it is in their inbox/pending -- not accepted".to_string(),
])
}