AnyBot/src/tunnel.rs

481 lines
20 KiB
Rust
Raw Normal View History

// 長連線: one lasting WebSocket from this AnyBot to the account server. Requests for this AnyBot
// (the phone, a screen of this brain, a brain using this computer's hands) come down it at once;
// this AnyBot's own requests to the account's other AnyBots go up it. Big bodies are compressed.
// A ping every 15 s keeps it (and any proxy on the way) awake; 45 s without a word and it is
// opened again. If the account server is older and has no WebSocket, the old way (asking
// "anything for me?" over and over) is used instead.
use crate::httpd::Req;
use crate::wire;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::{ErrorKind, Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use tungstenite::{Message, WebSocket};
const PING_SECS: u64 = 15;
const DEAD_SECS: u64 = 45;
/// Bodies bigger than this are compressed.
const ZIP_OVER: usize = 1024;
const CUT: &str = "和帳號伺服器的連線斷了,正在重新連線";
/// Another AnyBot's answer to a call.
pub struct Answer {
pub status: u16,
pub body: Vec<u8>,
}
type Calls = Arc<Mutex<HashMap<String, Sender<Result<Answer, String>>>>>;
struct Link {
tx: Sender<Vec<u8>>,
calls: Calls,
serial: u64,
}
fn link() -> &'static Mutex<Option<Link>> {
static L: OnceLock<Mutex<Option<Link>>> = OnceLock::new();
L.get_or_init(|| Mutex::new(None))
}
pub fn zip(b: &[u8]) -> Vec<u8> {
let mut e = flate2::write::DeflateEncoder::new(Vec::with_capacity(b.len() / 3), flate2::Compression::fast());
let _ = e.write_all(b);
e.finish().unwrap_or_default()
}
/// The body to send and whether it is compressed: only when that saves a good part of it
/// (pictures and PDFs are compressed already).
fn maybe_zip(b: Vec<u8>) -> (Vec<u8>, bool) {
// JPEG, PNG, GIF, WebP, PDF, zip/docx, gzip: compressed already, not worth a try (the live
// picture is a JPEG several times a second).
const PACKED: [&[u8]; 7] = [b"\xff\xd8\xff", b"\x89PNG", b"GIF8", b"RIFF", b"%PDF", b"PK\x03\x04", b"\x1f\x8b"];
if b.len() <= ZIP_OVER || PACKED.iter().any(|m| b.starts_with(m)) {
return (b, false);
}
let z = zip(&b);
if z.len() * 10 < b.len() * 9 {
(z, true)
} else {
(b, false)
}
}
pub fn unzip(b: &[u8]) -> Result<Vec<u8>, String> {
let mut out = vec![];
flate2::read::DeflateDecoder::new(b).take(512 * 1024 * 1024).read_to_end(&mut out).map_err(|_| "收到的資料壞掉了".to_string())?;
Ok(out)
}
// ---------- the connection ----------
enum Stream {
Plain(TcpStream),
Tls(Box<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>),
}
impl Stream {
fn tcp(&self) -> &TcpStream {
match self {
Stream::Plain(t) => t,
Stream::Tls(s) => &s.sock,
}
}
}
impl Read for Stream {
fn read(&mut self, b: &mut [u8]) -> std::io::Result<usize> {
match self {
Stream::Plain(t) => t.read(b),
Stream::Tls(s) => s.read(b),
}
}
}
impl Write for Stream {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
match self {
Stream::Plain(t) => t.write(b),
Stream::Tls(s) => s.write(b),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Stream::Plain(t) => t.flush(),
Stream::Tls(s) => s.flush(),
}
}
}
/// The root certificates a connection to the account server accepts: the public ones every
/// browser knows (built in), plus the ones this computer trusts itself. So a server on the local
/// network that signs its own certificate (https://10.0.0.25) works here once this computer
/// trusts it, just as it does in the browser. Read once (the system's list takes a moment).
pub fn trusted_roots() -> &'static [rustls::pki_types::CertificateDer<'static>] {
static R: OnceLock<Vec<rustls::pki_types::CertificateDer<'static>>> = OnceLock::new();
R.get_or_init(|| {
let mut v = webpki_root_certs::TLS_SERVER_ROOT_CERTS.to_vec();
// A computer whose list can't be read still has the public ones.
v.extend(rustls_native_certs::load_native_certs().certs);
v
})
}
fn tls_config() -> Arc<rustls::ClientConfig> {
static C: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
C.get_or_init(|| {
let mut roots = rustls::RootCertStore::empty();
roots.add_parsable_certificates(trusted_roots().iter().cloned());
Arc::new(
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("TLS versions")
.with_root_certificates(roots)
.with_no_client_auth(),
)
})
.clone()
}
fn would_block(e: &tungstenite::Error) -> bool {
matches!(e, tungstenite::Error::Io(io) if matches!(io.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut))
}
/// Why it couldn't connect; `old` = the account server has no WebSocket (use the old way).
struct NoLink {
old: bool,
why: String,
}
fn open(url: &str, token: &str) -> Result<WebSocket<Stream>, NoLink> {
use tungstenite::client::IntoClientRequest;
let fail = |why: &str| NoLink { old: false, why: why.to_string() };
let (tls, rest) = match (url.strip_prefix("https://"), url.strip_prefix("http://")) {
(Some(r), _) => (true, r),
(_, Some(r)) => (false, r),
_ => return Err(fail("帳號伺服器的網址不對")),
};
let hostport = rest.split('/').next().unwrap_or("");
let (host, port) = match hostport.rsplit_once(':') {
Some((h, p)) if p.parse::<u16>().is_ok() => (h, p.parse().unwrap_or(443)),
_ => (hostport, if tls { 443 } else { 80 }),
};
let addrs: Vec<_> = (host, port).to_socket_addrs().map_err(|_| fail("找不到伺服器:這台電腦可能沒連上網路,或伺服器網址打錯了"))?.collect();
// Each address in turn ("localhost" is both ::1 and 127.0.0.1).
let mut last = None;
let mut tcp = None;
for a in &addrs {
match TcpStream::connect_timeout(a, Duration::from_secs(10)) {
Ok(t) => {
tcp = Some(t);
break;
}
Err(e) => last = Some(e.kind()),
}
}
let tcp = tcp.ok_or_else(|| {
fail(match last {
None => "找不到伺服器",
Some(ErrorKind::ConnectionRefused) => "伺服器沒有開(連線被拒絕)",
Some(ErrorKind::TimedOut | ErrorKind::WouldBlock) => "伺服器沒有回應:可能沒開機,或網路把連線擋住了",
_ => "這台電腦沒有連上網路",
})
})?;
let _ = tcp.set_nodelay(true);
let _ = tcp.set_read_timeout(Some(Duration::from_secs(15)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(30)));
let stream = if tls {
let name = rustls::pki_types::ServerName::try_from(host.to_string()).map_err(|_| fail("帳號伺服器的網址不對"))?;
let conn = rustls::ClientConnection::new(tls_config(), name).map_err(|_| fail("和伺服器的安全連線失敗"))?;
Stream::Tls(Box::new(rustls::StreamOwned::new(conn, tcp)))
} else {
Stream::Plain(tcp)
};
let mut req = format!("{}://{hostport}/relay/host/ws", if tls { "wss" } else { "ws" }).into_client_request().map_err(|_| fail("帳號伺服器的網址不對"))?;
if let Ok(v) = format!("Bearer {token}").parse() {
req.headers_mut().insert("Authorization", v);
}
let cfg = tungstenite::protocol::WebSocketConfig::default().max_message_size(Some(512 << 20)).max_frame_size(Some(512 << 20));
match tungstenite::client::client_with_config(req, stream, Some(cfg)) {
Ok((ws, _)) => Ok(ws),
Err(tungstenite::HandshakeError::Failure(tungstenite::Error::Http(r))) => match r.status().as_u16() {
401 | 403 => Err(fail("這台電腦的帳號登入失效了,請到「設定 › 手機與帳號」重新登入")),
400 | 404 | 405 | 426 => Err(NoLink { old: true, why: String::new() }),
_ => Err(fail("帳號伺服器暫時無法使用,稍後會自動再試")),
},
Err(tungstenite::HandshakeError::Failure(e)) if e.to_string().to_lowercase().contains("unknownissuer") => Err(fail("這台電腦還不信任伺服器的憑證(伺服器自己簽的):先在這台電腦安裝並信任它的憑證,再試一次")),
Err(tungstenite::HandshakeError::Failure(e)) if e.to_string().to_lowercase().contains("certificate") || e.to_string().to_lowercase().contains("tls") => Err(fail("和伺服器的安全連線失敗(伺服器的憑證有問題)")),
Err(_) => Err(fail("連線到一半斷掉了")),
}
}
/// Keep the connection for as long as `alive()`. Returns false when the account server has no
/// WebSocket (the caller then uses the old way).
pub fn run(url: &str, token: &str, alive: &dyn Fn() -> bool, status: &dyn Fn(&str)) -> bool {
static SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let mut backoff = 1u64;
while alive() {
match open(url, token) {
Err(NoLink { old: true, .. }) => return false,
Err(e) => {
status(&format!("連不上帳號伺服器:{}", e.why));
// Wait, but notice at once when told to stop.
let until = Instant::now() + Duration::from_secs(backoff);
while Instant::now() < until && alive() {
std::thread::sleep(Duration::from_millis(200));
}
backoff = (backoff * 2).min(30);
}
Ok(ws) => {
backoff = 1;
status("已連線");
let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
keep(ws, serial, alive);
if alive() {
status("連線中斷,正在重新連線…");
}
}
}
}
true
}
fn keep(mut ws: WebSocket<Stream>, serial: u64, alive: &dyn Fn() -> bool) {
let mut pace = wire::Pace::new();
let (tx, rx) = channel::<Vec<u8>>();
let calls: Calls = Arc::default();
*link().lock().unwrap() = Some(Link { tx: tx.clone(), calls: calls.clone(), serial });
let s = crate::remote::hub().settings();
let _ = tx.send(wire::pack(&json!({"t": "hello", "brain": s.brain_id, "hands": s.device_link}), &[]));
let (mut heard, mut pinged) = (Instant::now(), Instant::now());
'live: while alive() {
pace.tune(ws.get_ref().tcp());
match ws.read() {
Ok(Message::Binary(b)) => {
heard = Instant::now();
pace.moved();
if let Some((head, body)) = wire::unpack(&b) {
// The relay says this machine was unregistered (signed out): stop for good.
if head["t"] == "bye" {
break;
}
on_frame(head, body, &tx, &calls);
}
}
Ok(Message::Close(_)) => break,
Ok(_) => heard = Instant::now(),
Err(e) if would_block(&e) => {}
Err(_) => break,
}
while let Ok(f) = rx.try_recv() {
pace.moved();
if ws.write(Message::Binary(f.into())).is_err() {
break 'live;
}
}
if pinged.elapsed().as_secs() >= PING_SECS {
pinged = Instant::now();
if ws.write(Message::Ping(Vec::new().into())).is_err() {
break;
}
}
match ws.flush() {
Err(e) if !would_block(&e) => break,
_ => {}
}
if heard.elapsed().as_secs() >= DEAD_SECS {
break;
}
}
let _ = ws.close(None);
let _ = ws.flush();
let mut l = link().lock().unwrap();
if l.as_ref().map(|l| l.serial) == Some(serial) {
*l = None;
}
drop(l);
People watch and help on the bots' computer through VNC (noVNC), carried by the account server The core: the bot works the computer; a person watches it like a colleague, and takes over when it needs them. VNC is made for that (only what changed, every mouse move and key, touch on phones), so the pictures sent one by one go, and with them their own input path. - vnc.rs: a VNC proxy to the box's x11vnc through one docker exec (a Python bridge that ends with either side). It signs in to the box with the box's own password (its VNC_PASSWORD) and offers the viewer no password, so the password never leaves the brain. The person's clicks and keys make the bot's steps wait until 8 s after they stop; text typed in the viewer's box (any language) is pasted in the box. - Account server: /screen (the viewer page) and /novnc.js (noVNC 1.6.0, MPL-2.0, bundled); /relay/vnc/<host> takes the browser's WebSocket (account session, or a 10-minute ticket the app asks for with POST /relay/screen-ticket; owners and admins) and carries it over that AnyBot's lasting connection (vopen / vdata / vtext / vclose). Both ends of that connection wait 2 ms instead of 40 ms to read while things move: a request and its answer took up to 160 ms more than needed. - Phone page: 看 Bot 的電腦 and 接管 open the viewer page (also on the local network: /api/vnc on the AnyBot's own phone server). This Mac's own screen moved to the ⋯ menu. - Mac app: 看畫面 opens the viewer in the browser (a screen of a brain: the brain's computer by ticket; else this computer's box, noVNC straight from it). The in-app picture viewer, the live JPEG stream, the person's click/move/scroll/key endpoints and the input shell are gone. - Opening a Mac app uses LaunchServices (`open -a`): agent-desktop's launch waited on the window through accessibility and failed without 輔助使用 now and then. Measured on this Mac (made-up account, local relay + headless brain + the real box): viewer signed in in 0.4–0.7 s, a request and its answer through relay + brain + box 25–58 ms median; headless Chrome shows the desktop live in /screen. remote_e2e 28/28 ×2, 149 unit tests, relay 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:05:44 +00:00
streams().lock().unwrap().clear();
// Whoever waits on an answer hears now, not after a timeout.
for (_, w) in calls.lock().unwrap().drain() {
let _ = w.send(Err(CUT.into()));
}
}
People watch and help on the bots' computer through VNC (noVNC), carried by the account server The core: the bot works the computer; a person watches it like a colleague, and takes over when it needs them. VNC is made for that (only what changed, every mouse move and key, touch on phones), so the pictures sent one by one go, and with them their own input path. - vnc.rs: a VNC proxy to the box's x11vnc through one docker exec (a Python bridge that ends with either side). It signs in to the box with the box's own password (its VNC_PASSWORD) and offers the viewer no password, so the password never leaves the brain. The person's clicks and keys make the bot's steps wait until 8 s after they stop; text typed in the viewer's box (any language) is pasted in the box. - Account server: /screen (the viewer page) and /novnc.js (noVNC 1.6.0, MPL-2.0, bundled); /relay/vnc/<host> takes the browser's WebSocket (account session, or a 10-minute ticket the app asks for with POST /relay/screen-ticket; owners and admins) and carries it over that AnyBot's lasting connection (vopen / vdata / vtext / vclose). Both ends of that connection wait 2 ms instead of 40 ms to read while things move: a request and its answer took up to 160 ms more than needed. - Phone page: 看 Bot 的電腦 and 接管 open the viewer page (also on the local network: /api/vnc on the AnyBot's own phone server). This Mac's own screen moved to the ⋯ menu. - Mac app: 看畫面 opens the viewer in the browser (a screen of a brain: the brain's computer by ticket; else this computer's box, noVNC straight from it). The in-app picture viewer, the live JPEG stream, the person's click/move/scroll/key endpoints and the input shell are gone. - Opening a Mac app uses LaunchServices (`open -a`): agent-desktop's launch waited on the window through accessibility and failed without 輔助使用 now and then. Measured on this Mac (made-up account, local relay + headless brain + the real box): viewer signed in in 0.4–0.7 s, a request and its answer through relay + brain + box 25–58 ms median; headless Chrome shows the desktop live in /screen. remote_e2e 28/28 ×2, 149 unit tests, relay 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:05:44 +00:00
/// People watching the bots' computer through the account server: one VNC stream each, by the
/// relay's stream id (vopen / vdata / vtext / vclose).
fn streams() -> &'static Mutex<HashMap<String, crate::vnc::Session>> {
static S: OnceLock<Mutex<HashMap<String, crate::vnc::Session>>> = OnceLock::new();
S.get_or_init(Default::default)
}
fn on_stream(kind: &str, sid: String, body: Vec<u8>, tx: &Sender<Vec<u8>>) {
let send = {
let (tx, sid) = (tx.clone(), sid.clone());
move |b: &[u8]| tx.send(wire::pack(&json!({"t": "vdata", "sid": sid}), b)).is_ok()
};
let close = {
let (tx, sid) = (tx.clone(), sid.clone());
move |why: &str| {
streams().lock().unwrap().remove(&sid);
let _ = tx.send(wire::pack(&json!({"t": "vclose", "sid": sid, "why": why}), &[]));
}
};
match kind {
"vopen" => {
std::thread::spawn(move || {
let out = {
let (send, close) = (send.clone(), close.clone());
move |b: &[u8]| if b.is_empty() { close("這台電腦的畫面關了"); false } else { send(b) }
};
match crate::vnc::open(out) {
Ok(s) => {
streams().lock().unwrap().insert(sid, s);
// Only now: the viewer answers this at once.
if !send(crate::vnc::GREETING) {
close("");
}
}
Err(e) => close(&e),
}
});
}
"vdata" => {
let mut all = streams().lock().unwrap();
let ok = all.get_mut(&sid).map(|s| s.feed(&body, &send)).unwrap_or(false);
drop(all);
if !ok {
close("這台電腦的畫面關了");
}
}
"vtext" => crate::vnc::person_typed(&String::from_utf8_lossy(&body)),
_ => {
streams().lock().unwrap().remove(&sid);
}
}
}
fn on_frame(head: Value, body: Vec<u8>, tx: &Sender<Vec<u8>>, calls: &Calls) {
People watch and help on the bots' computer through VNC (noVNC), carried by the account server The core: the bot works the computer; a person watches it like a colleague, and takes over when it needs them. VNC is made for that (only what changed, every mouse move and key, touch on phones), so the pictures sent one by one go, and with them their own input path. - vnc.rs: a VNC proxy to the box's x11vnc through one docker exec (a Python bridge that ends with either side). It signs in to the box with the box's own password (its VNC_PASSWORD) and offers the viewer no password, so the password never leaves the brain. The person's clicks and keys make the bot's steps wait until 8 s after they stop; text typed in the viewer's box (any language) is pasted in the box. - Account server: /screen (the viewer page) and /novnc.js (noVNC 1.6.0, MPL-2.0, bundled); /relay/vnc/<host> takes the browser's WebSocket (account session, or a 10-minute ticket the app asks for with POST /relay/screen-ticket; owners and admins) and carries it over that AnyBot's lasting connection (vopen / vdata / vtext / vclose). Both ends of that connection wait 2 ms instead of 40 ms to read while things move: a request and its answer took up to 160 ms more than needed. - Phone page: 看 Bot 的電腦 and 接管 open the viewer page (also on the local network: /api/vnc on the AnyBot's own phone server). This Mac's own screen moved to the ⋯ menu. - Mac app: 看畫面 opens the viewer in the browser (a screen of a brain: the brain's computer by ticket; else this computer's box, noVNC straight from it). The in-app picture viewer, the live JPEG stream, the person's click/move/scroll/key endpoints and the input shell are gone. - Opening a Mac app uses LaunchServices (`open -a`): agent-desktop's launch waited on the window through accessibility and failed without 輔助使用 now and then. Measured on this Mac (made-up account, local relay + headless brain + the real box): viewer signed in in 0.4–0.7 s, a request and its answer through relay + brain + box 25–58 ms median; headless Chrome shows the desktop live in /screen. remote_e2e 28/28 ×2, 149 unit tests, relay 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:05:44 +00:00
let t = head["t"].as_str().unwrap_or("");
if matches!(t, "vopen" | "vdata" | "vtext" | "vclose") {
return on_stream(t, head["sid"].as_str().unwrap_or("").to_string(), body, tx);
}
match t {
"job" => {
let tx = tx.clone();
std::thread::spawn(move || {
let s = |k: &str| head[k].as_str().unwrap_or("").to_string();
let id = s("id");
let body = if head["z"].as_bool().unwrap_or(false) { unzip(&body).unwrap_or_default() } else { body };
let req = Req {
method: s("method"),
path: s("path"),
query: head["query"].as_object().map(|o| o.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect()).unwrap_or_default(),
headers: [("x-anybot-user", s("user")), ("x-anybot-role", if head["role"].is_string() { s("role") } else { "owner".into() }), ("x-anybot-from", s("from"))].into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
body,
peer: "relay".into(),
};
let resp = crate::remote::handle(req, true);
let (out, z) = if head["z_ok"].as_bool().unwrap_or(false) { maybe_zip(resp.body) } else { (resp.body, false) };
let _ = tx.send(wire::pack(&json!({"t": "reply", "id": id, "status": resp.status, "ctype": resp.ctype, "headers": resp.headers, "z": z}), &out));
});
}
"ret" => {
if let Some(w) = calls.lock().unwrap().remove(head["id"].as_str().unwrap_or("")) {
let body = if head["z"].as_bool().unwrap_or(false) { unzip(&body) } else { Ok(body) };
People watch and help on the bots' computer through VNC (noVNC), carried by the account server The core: the bot works the computer; a person watches it like a colleague, and takes over when it needs them. VNC is made for that (only what changed, every mouse move and key, touch on phones), so the pictures sent one by one go, and with them their own input path. - vnc.rs: a VNC proxy to the box's x11vnc through one docker exec (a Python bridge that ends with either side). It signs in to the box with the box's own password (its VNC_PASSWORD) and offers the viewer no password, so the password never leaves the brain. The person's clicks and keys make the bot's steps wait until 8 s after they stop; text typed in the viewer's box (any language) is pasted in the box. - Account server: /screen (the viewer page) and /novnc.js (noVNC 1.6.0, MPL-2.0, bundled); /relay/vnc/<host> takes the browser's WebSocket (account session, or a 10-minute ticket the app asks for with POST /relay/screen-ticket; owners and admins) and carries it over that AnyBot's lasting connection (vopen / vdata / vtext / vclose). Both ends of that connection wait 2 ms instead of 40 ms to read while things move: a request and its answer took up to 160 ms more than needed. - Phone page: 看 Bot 的電腦 and 接管 open the viewer page (also on the local network: /api/vnc on the AnyBot's own phone server). This Mac's own screen moved to the ⋯ menu. - Mac app: 看畫面 opens the viewer in the browser (a screen of a brain: the brain's computer by ticket; else this computer's box, noVNC straight from it). The in-app picture viewer, the live JPEG stream, the person's click/move/scroll/key endpoints and the input shell are gone. - Opening a Mac app uses LaunchServices (`open -a`): agent-desktop's launch waited on the window through accessibility and failed without 輔助使用 now and then. Measured on this Mac (made-up account, local relay + headless brain + the real box): viewer signed in in 0.4–0.7 s, a request and its answer through relay + brain + box 25–58 ms median; headless Chrome shows the desktop live in /screen. remote_e2e 28/28 ×2, 149 unit tests, relay 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:05:44 +00:00
let _ = w.send(body.map(|body| Answer { status: head["status"].as_u64().unwrap_or(502) as u16, body }));
}
}
_ => {}
}
}
/// Tell the account server which brain this AnyBot shows (so the phone can go there).
pub fn hello(brain: &str, hands: bool) {
if let Some(l) = link().lock().unwrap().as_ref() {
let _ = l.tx.send(wire::pack(&json!({"t": "hello", "brain": brain, "hands": hands}), &[]));
}
}
/// Ask another AnyBot of this account over the lasting connection. None when it isn't open
/// (the caller then uses an ordinary request).
pub fn call(to: &str, method: &str, path: &str, query: &[(&str, String)], body: &[u8], secs: u64) -> Option<Result<Answer, String>> {
let (tx, calls) = {
let l = link().lock().unwrap();
let l = l.as_ref()?;
(l.tx.clone(), l.calls.clone())
};
let id = crate::httpd::random_hex(8);
let (rtx, rrx) = channel();
calls.lock().unwrap().insert(id.clone(), rtx);
let (out, z) = maybe_zip(body.to_vec());
let q: serde_json::Map<String, Value> = query.iter().map(|(k, v)| (k.to_string(), json!(v))).collect();
if tx.send(wire::pack(&json!({"t": "call", "id": id, "to": to, "method": method, "path": path, "query": q, "z": z}), &out)).is_err() {
calls.lock().unwrap().remove(&id);
return None;
}
Some(match rrx.recv_timeout(Duration::from_secs(secs)) {
Ok(r) => r,
Err(_) => {
calls.lock().unwrap().remove(&id);
Err("等太久沒有回應:網路很慢或斷斷續續".into())
}
})
}
/// An account server on the local network with its own certificate (setup.sh 「區網 IP」, Caddy
/// `tls internal`). Trusted (SSL_CERT_FILE = its root, read like the system's list):
/// ANYBOT_TLS_URL=https://10.0.0.25 SSL_CERT_FILE=root.crt cargo test -- --ignored own_certificate
/// Without SSL_CERT_FILE (and not trusted by this computer) it checks the refusal says what to do.
#[cfg(test)]
mod own_certificate_live {
#[test]
#[ignore]
fn a_server_with_its_own_certificate() {
let url = std::env::var("ANYBOT_TLS_URL").expect("ANYBOT_TLS_URL");
let trusted = std::env::var_os("SSL_CERT_FILE").is_some();
// A sign-up with a wrong invite: refused by the server itself (never makes an account).
let http = crate::remote::relay_post(&format!("{url}/relay/signup"), "", &serde_json::json!({"user": "tls-probe", "password": "tls-probe-123", "invite": "wrong"})).unwrap_err();
let ws = super::open(&url, "not-a-token").err().map(|e| e.why).unwrap_or_default();
if trusted {
assert!(!http.starts_with("連不到帳號伺服器"), "the secure connection should work: {http}");
assert!(ws.contains("登入失效"), "the long connection should reach the server: {ws}");
} else {
assert!(http.contains("不信任伺服器的憑證"), "{http}");
assert!(ws.contains("不信任伺服器的憑證"), "{ws}");
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn the_public_certificates_are_always_trusted() {
// The system's list comes on top; a computer without one still reaches public servers.
assert!(super::trusted_roots().len() >= webpki_root_certs::TLS_SERVER_ROOT_CERTS.len());
}
#[test]
fn compression_round_trips_and_shrinks_text() {
let text = "你好,這是一段會重複的聊天內容。".repeat(400);
let z = super::zip(text.as_bytes());
assert!(z.len() * 5 < text.len());
assert_eq!(super::unzip(&z).unwrap(), text.as_bytes());
// Already-compressed bytes go as they are.
let mut x = 88172645463325252u64;
let noise: Vec<u8> = (0..5000)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x >> 24) as u8
})
.collect();
assert!(!super::maybe_zip(noise).1);
assert!(super::maybe_zip(text.into_bytes()).1);
}
}