Skip to main content

blit_proxy/
lib.rs

1/// blit-proxy library — all proxy logic, usable in-process or as a binary.
2///
3/// Call [`proxy_socket_path`] to find the socket, then [`run`] to start the
4/// proxy on the current thread (blocking, runs its own tokio runtime).
5use std::collections::{HashMap, VecDeque};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering};
8use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
9use tokio::sync::{Mutex, RwLock};
10
11static VERBOSE: AtomicBool = AtomicBool::new(false);
12
13macro_rules! log {
14    ($($arg:tt)*) => {
15        if VERBOSE.load(Ordering::Relaxed) {
16            eprintln!($($arg)*);
17        }
18    };
19}
20
21type BoxRead = Box<dyn AsyncRead + Unpin + Send>;
22type BoxWrite = Box<dyn AsyncWrite + Unpin + Send>;
23
24// ---------------------------------------------------------------------------
25// Proxy socket path (single stable path for the whole process)
26// ---------------------------------------------------------------------------
27
28pub fn proxy_socket_path() -> String {
29    if let Ok(p) = std::env::var("BLIT_PROXY_SOCK") {
30        return p;
31    }
32    #[cfg(unix)]
33    {
34        let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into());
35        format!("{dir}/blit-proxy.sock")
36    }
37    #[cfg(windows)]
38    {
39        r"\\.\pipe\blit-proxy".into()
40    }
41}
42
43// ---------------------------------------------------------------------------
44// Auto-start helpers (shared by blit-cli and blit-gateway)
45// ---------------------------------------------------------------------------
46
47/// Returns true if a proxy is already listening at `path`.
48pub async fn proxy_alive(path: &str) -> bool {
49    #[cfg(unix)]
50    {
51        std::path::Path::new(path).exists() && tokio::net::UnixStream::connect(path).await.is_ok()
52    }
53    #[cfg(windows)]
54    {
55        use tokio::net::windows::named_pipe::ClientOptions;
56        ClientOptions::new().open(path).is_ok()
57    }
58    #[cfg(not(any(unix, windows)))]
59    {
60        let _ = path;
61        false
62    }
63}
64
65/// Ensure a blit-proxy daemon is running, spawning one if necessary.
66///
67/// `proxy_bin` is the path to the binary that accepts a `proxy-daemon`
68/// subcommand (typically `std::env::current_exe()`).  When the binary is
69/// the standalone `blit-proxy` it should be invoked without arguments;
70/// when it is the `blit` CLI it needs the `proxy-daemon` subcommand.
71///
72/// Returns the socket path on success.
73pub async fn ensure_proxy(
74    proxy_bin: &std::path::Path,
75    use_subcommand: bool,
76) -> Result<String, String> {
77    let sock = proxy_socket_path();
78
79    if proxy_alive(&sock).await {
80        return Ok(sock);
81    }
82
83    #[cfg(unix)]
84    let _ = std::fs::remove_file(&sock);
85
86    #[cfg(unix)]
87    {
88        use std::os::unix::process::CommandExt;
89        let mut cmd = std::process::Command::new(proxy_bin);
90        if use_subcommand {
91            cmd.arg("proxy-daemon");
92        }
93        // SAFETY: pre_exec runs in the child between fork and exec.
94        // setsid() is async-signal-safe per POSIX.
95        unsafe {
96            cmd.env("BLIT_PROXY_IDLE", "300")
97                .stdin(std::process::Stdio::null())
98                .stdout(std::process::Stdio::null())
99                .stderr(std::process::Stdio::null())
100                .pre_exec(|| {
101                    libc::setsid();
102                    Ok(())
103                })
104                .spawn()
105                .map_err(|e| format!("blit-proxy: spawn failed: {e}"))?;
106        }
107    }
108
109    #[cfg(windows)]
110    {
111        use std::os::windows::process::CommandExt;
112        const DETACHED_PROCESS: u32 = 0x0000_0008;
113        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
114        let mut cmd = std::process::Command::new(proxy_bin);
115        if use_subcommand {
116            cmd.arg("proxy-daemon");
117        }
118        cmd.env("BLIT_PROXY_IDLE", "300")
119            .stdin(std::process::Stdio::null())
120            .stdout(std::process::Stdio::null())
121            .stderr(std::process::Stdio::null())
122            .creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)
123            .spawn()
124            .map_err(|e| format!("blit-proxy: spawn failed: {e}"))?;
125    }
126
127    #[cfg(not(any(unix, windows)))]
128    return Err("blit-proxy auto-start is not supported on this platform".into());
129
130    // Wait up to 5 s for the socket/pipe to appear.
131    for _ in 0..100 {
132        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
133        if proxy_alive(&sock).await {
134            return Ok(sock);
135        }
136    }
137    Err(format!("blit-proxy did not become ready at {sock} in time"))
138}
139
140// ---------------------------------------------------------------------------
141// Upstream connection
142// ---------------------------------------------------------------------------
143
144struct UpstreamConn {
145    reader: BoxRead,
146    writer: BoxWrite,
147}
148
149// ---------------------------------------------------------------------------
150// Share session pool — pools WebRTC Sessions, opens/closes channels per req
151// ---------------------------------------------------------------------------
152
153struct SharePool {
154    idle: Mutex<VecDeque<blit_webrtc_forwarder::client::Session>>,
155    passphrase: String,
156    hub: String,
157    pool_size: usize,
158    active: AtomicUsize,
159    last_activity: AtomicI64,
160}
161
162impl SharePool {
163    fn new(passphrase: String, hub: String, pool_size: usize) -> Arc<Self> {
164        Arc::new(Self {
165            idle: Mutex::new(VecDeque::new()),
166            passphrase,
167            hub,
168            pool_size,
169            active: AtomicUsize::new(0),
170            last_activity: AtomicI64::new(now_secs()),
171        })
172    }
173
174    async fn acquire(
175        &self,
176    ) -> Result<
177        (
178            blit_webrtc_forwarder::client::Session,
179            blit_webrtc_forwarder::client::ChannelHandle,
180            UpstreamConn,
181        ),
182        String,
183    > {
184        loop {
185            let session = {
186                let mut idle = self.idle.lock().await;
187                idle.pop_front()
188            };
189            match session {
190                Some(s) => match s.open_channel().await {
191                    Ok((handle, stream)) => {
192                        let (r, w) = tokio::io::split(stream);
193                        return Ok((
194                            s,
195                            handle,
196                            UpstreamConn {
197                                reader: Box::new(r),
198                                writer: Box::new(w),
199                            },
200                        ));
201                    }
202                    Err(e) => {
203                        log!("blit-proxy: share: idle session stale ({e}), discarding");
204                        continue;
205                    }
206                },
207                None => break,
208            }
209        }
210        // No idle session — establish a new one.
211        let (session, handle, stream) =
212            blit_webrtc_forwarder::client::Session::establish(&self.passphrase, &self.hub)
213                .await
214                .map_err(|e| format!("{e}"))?;
215        let (r, w) = tokio::io::split(stream);
216        Ok((
217            session,
218            handle,
219            UpstreamConn {
220                reader: Box::new(r),
221                writer: Box::new(w),
222            },
223        ))
224    }
225
226    async fn release(&self, session: blit_webrtc_forwarder::client::Session) {
227        let mut idle = self.idle.lock().await;
228        if idle.len() < self.pool_size {
229            idle.push_back(session);
230        }
231    }
232
233    fn client_connected(&self) {
234        self.active.fetch_add(1, Ordering::Relaxed);
235        self.last_activity.store(i64::MAX, Ordering::Relaxed);
236    }
237
238    fn client_disconnected(&self) {
239        let prev = self.active.fetch_sub(1, Ordering::Relaxed);
240        if prev == 1 {
241            self.last_activity.store(now_secs(), Ordering::Relaxed);
242        }
243    }
244
245    async fn refill_loop(self: Arc<Self>) {
246        loop {
247            let need = {
248                let idle = self.idle.lock().await;
249                self.pool_size.saturating_sub(idle.len())
250            };
251            for _ in 0..need {
252                match blit_webrtc_forwarder::client::Session::establish(&self.passphrase, &self.hub)
253                    .await
254                {
255                    Ok((session, handle, _stream)) => {
256                        // Close the initial channel; keep only the warm session.
257                        session.close_channel(handle);
258                        let mut idle = self.idle.lock().await;
259                        if idle.len() < self.pool_size {
260                            idle.push_back(session);
261                        }
262                    }
263                    Err(e) => {
264                        log!("blit-proxy: share: pre-warm failed: {e}");
265                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
266                        break;
267                    }
268                }
269            }
270            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
271        }
272    }
273}
274
275struct ShareRegistry {
276    pools: RwLock<HashMap<String, Arc<SharePool>>>,
277    pool_size: usize,
278}
279
280impl ShareRegistry {
281    fn new(pool_size: usize) -> Arc<Self> {
282        Arc::new(Self {
283            pools: RwLock::new(HashMap::new()),
284            pool_size,
285        })
286    }
287
288    async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<SharePool> {
289        {
290            let pools = self.pools.read().await;
291            if let Some(p) = pools.get(uri) {
292                return p.clone();
293            }
294        }
295        let mut pools = self.pools.write().await;
296        if let Some(p) = pools.get(uri) {
297            return p.clone();
298        }
299        let (passphrase, hub) = parse_share_uri(uri.strip_prefix("share:").unwrap_or(uri));
300        let pool = SharePool::new(passphrase, hub, self.pool_size);
301        pools.insert(uri.to_string(), pool.clone());
302        drop(pools);
303
304        let pool_seed = pool.clone();
305        tokio::spawn(async move {
306            pool_seed.refill_loop().await;
307        });
308
309        pool
310    }
311
312    async fn latest_activity(&self) -> i64 {
313        let pools = self.pools.read().await;
314        if pools.is_empty() {
315            return now_secs();
316        }
317        pools
318            .values()
319            .map(|p| p.last_activity.load(Ordering::Relaxed))
320            .max()
321            .unwrap_or_else(now_secs)
322    }
323}
324
325// ---------------------------------------------------------------------------
326// Per-target pool
327// ---------------------------------------------------------------------------
328
329struct Pool {
330    idle: Mutex<VecDeque<UpstreamConn>>,
331    /// Number of currently active (proxied) downstream clients.
332    active: AtomicUsize,
333    /// Unix seconds of last client connect or disconnect.
334    /// `i64::MAX` while any client is active.
335    last_activity: AtomicI64,
336    pool_size: usize,
337    upstream_uri: String,
338}
339
340impl Pool {
341    fn new(upstream_uri: String, pool_size: usize) -> Arc<Self> {
342        Arc::new(Self {
343            idle: Mutex::new(VecDeque::new()),
344            active: AtomicUsize::new(0),
345            last_activity: AtomicI64::new(now_secs()),
346            pool_size,
347            upstream_uri,
348        })
349    }
350
351    async fn acquire(&self) -> Result<UpstreamConn, String> {
352        {
353            let mut idle = self.idle.lock().await;
354            if let Some(conn) = idle.pop_front() {
355                return Ok(conn);
356            }
357        }
358        self.connect_one().await
359    }
360
361    async fn connect_one(&self) -> Result<UpstreamConn, String> {
362        connect_upstream(&self.upstream_uri).await
363    }
364
365    fn client_connected(&self) {
366        self.active.fetch_add(1, Ordering::Relaxed);
367        self.last_activity.store(i64::MAX, Ordering::Relaxed);
368    }
369
370    fn client_disconnected(&self) {
371        let prev = self.active.fetch_sub(1, Ordering::Relaxed);
372        if prev == 1 {
373            self.last_activity.store(now_secs(), Ordering::Relaxed);
374        }
375    }
376
377    /// Background task: keep idle slots full.
378    async fn refill_loop(self: Arc<Self>) {
379        loop {
380            let need = {
381                let idle = self.idle.lock().await;
382                self.pool_size.saturating_sub(idle.len())
383            };
384            for _ in 0..need {
385                match self.connect_one().await {
386                    Ok(conn) => {
387                        self.idle.lock().await.push_back(conn);
388                    }
389                    Err(e) => {
390                        log!(
391                            "blit-proxy: [{uri}] upstream connect failed: {e}",
392                            uri = self.upstream_uri
393                        );
394                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
395                        break;
396                    }
397                }
398            }
399            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
400        }
401    }
402}
403
404fn now_secs() -> i64 {
405    std::time::SystemTime::now()
406        .duration_since(std::time::UNIX_EPOCH)
407        .unwrap_or_default()
408        .as_secs() as i64
409}
410
411// ---------------------------------------------------------------------------
412// Pool registry: one pool per distinct upstream URI
413// ---------------------------------------------------------------------------
414
415struct Registry {
416    pools: RwLock<HashMap<String, Arc<Pool>>>,
417    pool_size: usize,
418}
419
420impl Registry {
421    fn new(pool_size: usize) -> Arc<Self> {
422        Arc::new(Self {
423            pools: RwLock::new(HashMap::new()),
424            pool_size,
425        })
426    }
427
428    /// Get or create the pool for `uri`, spawning its refill task if new.
429    async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<Pool> {
430        // Fast path: pool already exists.
431        {
432            let pools = self.pools.read().await;
433            if let Some(p) = pools.get(uri) {
434                return p.clone();
435            }
436        }
437        // Slow path: create and seed.
438        let mut pools = self.pools.write().await;
439        // Re-check under write lock.
440        if let Some(p) = pools.get(uri) {
441            return p.clone();
442        }
443        let pool = Pool::new(uri.to_string(), self.pool_size);
444        pools.insert(uri.to_string(), pool.clone());
445        drop(pools);
446
447        // Seed eagerly in a background task.
448        let pool_seed = pool.clone();
449        tokio::spawn(async move {
450            for _ in 0..pool_seed.pool_size {
451                match pool_seed.connect_one().await {
452                    Ok(conn) => pool_seed.idle.lock().await.push_back(conn),
453                    Err(e) => {
454                        log!(
455                            "blit-proxy: [{uri}] initial connect: {e}",
456                            uri = pool_seed.upstream_uri
457                        );
458                        break;
459                    }
460                }
461            }
462            pool_seed.refill_loop().await;
463        });
464
465        pool
466    }
467
468    /// Returns the most-recent `last_activity` across all pools.
469    /// Returns `i64::MAX` if any pool has an active client.
470    async fn latest_activity(&self) -> i64 {
471        let pools = self.pools.read().await;
472        if pools.is_empty() {
473            return now_secs();
474        }
475        pools
476            .values()
477            .map(|p| p.last_activity.load(Ordering::Relaxed))
478            .max()
479            .unwrap_or_else(now_secs)
480    }
481}
482
483// ---------------------------------------------------------------------------
484// Upstream transport implementations
485// ---------------------------------------------------------------------------
486
487async fn connect_upstream(uri: &str) -> Result<UpstreamConn, String> {
488    if let Some(rest) = uri.strip_prefix("share:") {
489        return connect_share(rest).await;
490    }
491
492    if let Some(rest) = uri.strip_prefix("ssh:") {
493        return connect_ssh(rest).await;
494    }
495
496    // Extract query parameters from URIs that support them.
497    let (base_uri, passphrase, cert_hash) = extract_uri_params(uri);
498
499    if let Some(path) = base_uri.strip_prefix("socket:") {
500        return connect_socket(path).await;
501    }
502    if let Some(addr) = base_uri.strip_prefix("tcp:") {
503        return connect_tcp(addr).await;
504    }
505    if base_uri.starts_with("ws://") || base_uri.starts_with("wss://") {
506        return connect_ws(&base_uri, passphrase.as_deref()).await;
507    }
508    if let Some(rest) = base_uri.strip_prefix("wt://") {
509        let cert_bytes = cert_hash.as_deref().and_then(parse_hex);
510        return connect_wt(rest, passphrase.as_deref(), &cert_bytes).await;
511    }
512    Err(format!(
513        "unknown upstream URI scheme in '{uri}' \
514         (expected socket:, tcp:, ws://, wss://, wt://, share:, or ssh:)"
515    ))
516}
517
518/// Split `share:` URI rest into (passphrase, hub_url).
519///
520/// Accepted forms:
521///   `myphrase`                       — use default hub
522///   `myphrase?hub=wss://custom.hub`  — use specific hub
523fn parse_share_uri(rest: &str) -> (String, String) {
524    let (passphrase_raw, hub) = if let Some(q_pos) = rest.find('?') {
525        let phrase = &rest[..q_pos];
526        let query = &rest[q_pos + 1..];
527        let hub = query
528            .split('&')
529            .find_map(|kv| kv.strip_prefix("hub=").map(percent_decode))
530            .unwrap_or_else(|| blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string());
531        (phrase.to_string(), hub)
532    } else {
533        (
534            rest.to_string(),
535            blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string(),
536        )
537    };
538    let passphrase = percent_decode(&passphrase_raw);
539    let hub = blit_webrtc_forwarder::normalize_hub(&hub);
540    (passphrase, hub)
541}
542
543async fn connect_share(rest: &str) -> Result<UpstreamConn, String> {
544    let (passphrase, hub) = parse_share_uri(rest);
545    let stream = blit_webrtc_forwarder::client::connect(&passphrase, &hub)
546        .await
547        .map_err(|e| format!("share:{rest}: {e}"))?;
548    let (r, w) = tokio::io::split(stream);
549    Ok(UpstreamConn {
550        reader: Box::new(r),
551        writer: Box::new(w),
552    })
553}
554
555// ---------------------------------------------------------------------------
556// SSH via embedded client (cross-platform)
557// ---------------------------------------------------------------------------
558
559/// Shared SSH connection pool for the proxy.  Connections are multiplexed
560/// over a single TCP+SSH session per host, matching the gateway's behaviour.
561fn ssh_pool() -> &'static blit_ssh::SshPool {
562    static POOL: std::sync::OnceLock<blit_ssh::SshPool> = std::sync::OnceLock::new();
563    POOL.get_or_init(blit_ssh::SshPool::new)
564}
565
566/// Connect to a remote blit-server via the embedded SSH client.
567///
568/// Uses `direct-streamlocal` channel forwarding (no external `ssh`, `nc`, or
569/// `socat` required).  The connection is multiplexed and pooled so subsequent
570/// calls to the same host reuse the TCP+SSH session.
571async fn connect_ssh(rest: &str) -> Result<UpstreamConn, String> {
572    if rest.is_empty() {
573        return Err("ssh: destination requires a host".into());
574    }
575    let (user, host, socket) = blit_ssh::parse_ssh_uri(rest);
576    let stream = ssh_pool()
577        .connect(&host, user.as_deref(), socket.as_deref())
578        .await
579        .map_err(|e| format!("ssh:{rest}: {e}"))?;
580    let (r, w) = tokio::io::split(stream);
581    Ok(UpstreamConn {
582        reader: Box::new(r),
583        writer: Box::new(w),
584    })
585}
586
587/// Split a URI into (base, passphrase, certHash) by parsing query params.
588/// Only applies to ws://, wss://, wt:// — socket: and tcp: are returned as-is.
589fn extract_uri_params(uri: &str) -> (String, Option<String>, Option<String>) {
590    if !uri.starts_with("ws://") && !uri.starts_with("wss://") && !uri.starts_with("wt://") {
591        return (uri.to_string(), None, None);
592    }
593    // Find '?'.
594    let (base, query) = match uri.find('?') {
595        Some(pos) => (&uri[..pos], Some(&uri[pos + 1..])),
596        None => (uri, None),
597    };
598    let mut passphrase = None;
599    let mut cert_hash = None;
600    if let Some(q) = query {
601        for param in q.split('&') {
602            if let Some(v) = param.strip_prefix("passphrase=") {
603                passphrase = Some(percent_decode(v));
604            } else if let Some(v) = param.strip_prefix("certHash=") {
605                cert_hash = Some(v.to_string());
606            }
607        }
608    }
609    (base.to_string(), passphrase, cert_hash)
610}
611
612fn percent_decode(s: &str) -> String {
613    // Minimal %XX decoder sufficient for passphrases.
614    let mut out = String::with_capacity(s.len());
615    let mut chars = s.chars().peekable();
616    while let Some(c) = chars.next() {
617        if c == '%' {
618            let h1 = chars.next().unwrap_or('0');
619            let h2 = chars.next().unwrap_or('0');
620            if let Ok(b) = u8::from_str_radix(&format!("{h1}{h2}"), 16) {
621                out.push(b as char);
622                continue;
623            }
624        }
625        out.push(c);
626    }
627    out
628}
629
630async fn connect_socket(path: &str) -> Result<UpstreamConn, String> {
631    #[cfg(unix)]
632    {
633        let stream = tokio::net::UnixStream::connect(path)
634            .await
635            .map_err(|e| format!("socket:{path}: {e}"))?;
636        let (r, w) = tokio::io::split(stream);
637        Ok(UpstreamConn {
638            reader: Box::new(r),
639            writer: Box::new(w),
640        })
641    }
642    #[cfg(not(unix))]
643    {
644        use tokio::net::windows::named_pipe::ClientOptions;
645        let pipe = ClientOptions::new()
646            .open(path)
647            .map_err(|e| format!("socket:{path}: {e}"))?;
648        let (r, w) = tokio::io::split(pipe);
649        Ok(UpstreamConn {
650            reader: Box::new(r),
651            writer: Box::new(w),
652        })
653    }
654}
655
656async fn connect_tcp(addr: &str) -> Result<UpstreamConn, String> {
657    let stream = tokio::net::TcpStream::connect(addr)
658        .await
659        .map_err(|e| format!("tcp:{addr}: {e}"))?;
660    let _ = stream.set_nodelay(true);
661    let (r, w) = tokio::io::split(stream);
662    Ok(UpstreamConn {
663        reader: Box::new(r),
664        writer: Box::new(w),
665    })
666}
667
668async fn connect_ws(uri: &str, passphrase: Option<&str>) -> Result<UpstreamConn, String> {
669    use futures_util::{SinkExt, StreamExt};
670    use tokio_tungstenite::tungstenite::Message;
671
672    let (mut ws, _) = tokio_tungstenite::connect_async(uri)
673        .await
674        .map_err(|e| format!("{uri}: {e}"))?;
675
676    let pass = passphrase.unwrap_or("");
677    ws.send(Message::Text(pass.into()))
678        .await
679        .map_err(|e| format!("{uri}: auth send: {e}"))?;
680    match ws.next().await {
681        Some(Ok(Message::Text(t))) if t.trim() == "ok" => {}
682        Some(Ok(Message::Text(t))) => {
683            return Err(format!("{uri}: auth rejected: {}", t.trim()));
684        }
685        other => {
686            return Err(format!("{uri}: unexpected auth response: {other:?}"));
687        }
688    }
689
690    let (ws_write, ws_read) = ws.split();
691    Ok(UpstreamConn {
692        reader: Box::new(WsFrameReader {
693            inner: ws_read,
694            buf: bytes::Bytes::new(),
695        }),
696        writer: Box::new(WsFrameWriter { inner: ws_write }),
697    })
698}
699
700async fn connect_wt(
701    rest: &str,
702    passphrase: Option<&str>,
703    cert_hash: &Option<Vec<u8>>,
704) -> Result<UpstreamConn, String> {
705    use web_transport_quinn as wt;
706
707    // Build the URL for the WT session (must use https: scheme).
708    let (host, port) = parse_wt_host_port(rest)?;
709    let url: url::Url = format!("https://{host}:{port}/")
710        .parse()
711        .map_err(|e| format!("wt: url: {e}"))?;
712
713    // Build the client with appropriate certificate verification.
714    let client: wt::Client = if let Some(hash) = cert_hash {
715        wt::ClientBuilder::new()
716            .with_server_certificate_hashes(vec![hash.clone()])
717            .map_err(|e| format!("wt: client build: {e}"))?
718    } else {
719        wt::ClientBuilder::new()
720            .with_system_roots()
721            .map_err(|e| format!("wt: client build: {e}"))?
722    };
723
724    let session = client
725        .connect(url)
726        .await
727        .map_err(|e| format!("wt: connect {host}:{port}: {e}"))?;
728
729    let (mut send, mut recv) = session
730        .open_bi()
731        .await
732        .map_err(|e| format!("wt: open_bi: {e}"))?;
733
734    // Auth: 2-byte-LE passphrase length + passphrase bytes, then read 1-byte response.
735    let pass = passphrase.unwrap_or("").as_bytes();
736    let mut auth_buf = Vec::with_capacity(2 + pass.len());
737    auth_buf.extend_from_slice(&(pass.len() as u16).to_le_bytes());
738    auth_buf.extend_from_slice(pass);
739    send.write_all(&auth_buf)
740        .await
741        .map_err(|e| format!("wt: auth send: {e}"))?;
742
743    let mut resp = [0u8; 1];
744    recv.read_exact(&mut resp)
745        .await
746        .map_err(|e| format!("wt: auth recv: {e}"))?;
747    if resp[0] != 1 {
748        return Err(format!(
749            "wt: auth rejected (response byte {:#04x})",
750            resp[0]
751        ));
752    }
753
754    Ok(UpstreamConn {
755        reader: Box::new(recv),
756        writer: Box::new(send),
757    })
758}
759
760fn parse_wt_host_port(rest: &str) -> Result<(String, u16), String> {
761    let without_path = rest.split('/').next().unwrap_or(rest);
762    if let Some(colon) = without_path.rfind(':') {
763        let host = without_path[..colon].to_string();
764        let port: u16 = without_path[colon + 1..]
765            .parse()
766            .map_err(|_| format!("wt: invalid port in '{rest}'"))?;
767        Ok((host, port))
768    } else {
769        Ok((without_path.to_string(), 443))
770    }
771}
772
773fn parse_hex(s: &str) -> Option<Vec<u8>> {
774    if !s.len().is_multiple_of(2) {
775        return None;
776    }
777    (0..s.len())
778        .step_by(2)
779        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
780        .collect()
781}
782
783// ---------------------------------------------------------------------------
784// WS ↔ raw blit-frame adapters
785// ---------------------------------------------------------------------------
786
787use futures_util::{SinkExt, StreamExt};
788use tokio_tungstenite::tungstenite::Message;
789
790type WsSink = futures_util::stream::SplitSink<
791    tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
792    Message,
793>;
794type WsStream = futures_util::stream::SplitStream<
795    tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
796>;
797
798struct WsFrameReader {
799    inner: WsStream,
800    buf: bytes::Bytes,
801}
802
803impl AsyncRead for WsFrameReader {
804    fn poll_read(
805        mut self: std::pin::Pin<&mut Self>,
806        cx: &mut std::task::Context<'_>,
807        out: &mut tokio::io::ReadBuf<'_>,
808    ) -> std::task::Poll<std::io::Result<()>> {
809        loop {
810            if !self.buf.is_empty() {
811                let n = out.remaining().min(self.buf.len());
812                out.put_slice(&self.buf[..n]);
813                self.buf = self.buf.slice(n..);
814                return std::task::Poll::Ready(Ok(()));
815            }
816            match self.inner.poll_next_unpin(cx) {
817                std::task::Poll::Pending => return std::task::Poll::Pending,
818                std::task::Poll::Ready(None) => return std::task::Poll::Ready(Ok(())),
819                std::task::Poll::Ready(Some(Err(e))) => {
820                    return std::task::Poll::Ready(Err(std::io::Error::other(e)));
821                }
822                std::task::Poll::Ready(Some(Ok(msg))) => {
823                    let data = match msg {
824                        Message::Binary(d) => d,
825                        Message::Close(_) => return std::task::Poll::Ready(Ok(())),
826                        _ => continue,
827                    };
828                    let len = data.len() as u32;
829                    let mut framed = Vec::with_capacity(4 + data.len());
830                    framed.extend_from_slice(&len.to_le_bytes());
831                    framed.extend_from_slice(&data);
832                    self.buf = bytes::Bytes::from(framed);
833                }
834            }
835        }
836    }
837}
838
839struct WsFrameWriter {
840    inner: WsSink,
841}
842
843impl AsyncWrite for WsFrameWriter {
844    fn poll_write(
845        mut self: std::pin::Pin<&mut Self>,
846        cx: &mut std::task::Context<'_>,
847        buf: &[u8],
848    ) -> std::task::Poll<std::io::Result<usize>> {
849        if buf.len() < 4 {
850            return std::task::Poll::Ready(Err(std::io::Error::new(
851                std::io::ErrorKind::InvalidData,
852                "ws frame writer: buffer too small for length prefix",
853            )));
854        }
855        let len = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize;
856        if buf.len() < 4 + len {
857            return std::task::Poll::Ready(Err(std::io::Error::new(
858                std::io::ErrorKind::InvalidData,
859                format!(
860                    "ws frame writer: buffer ({}) too small for frame payload ({})",
861                    buf.len(),
862                    4 + len
863                ),
864            )));
865        }
866        let payload = buf[4..4 + len].to_vec();
867        match self.inner.poll_ready_unpin(cx) {
868            std::task::Poll::Pending => return std::task::Poll::Pending,
869            std::task::Poll::Ready(Err(e)) => {
870                return std::task::Poll::Ready(Err(std::io::Error::other(e)));
871            }
872            std::task::Poll::Ready(Ok(())) => {}
873        }
874        match self.inner.start_send_unpin(Message::Binary(payload.into())) {
875            Err(e) => std::task::Poll::Ready(Err(std::io::Error::other(e))),
876            Ok(()) => std::task::Poll::Ready(Ok(4 + len)),
877        }
878    }
879    fn poll_flush(
880        mut self: std::pin::Pin<&mut Self>,
881        cx: &mut std::task::Context<'_>,
882    ) -> std::task::Poll<std::io::Result<()>> {
883        self.inner
884            .poll_flush_unpin(cx)
885            .map_err(std::io::Error::other)
886    }
887    fn poll_shutdown(
888        mut self: std::pin::Pin<&mut Self>,
889        cx: &mut std::task::Context<'_>,
890    ) -> std::task::Poll<std::io::Result<()>> {
891        self.inner
892            .poll_close_unpin(cx)
893            .map_err(std::io::Error::other)
894    }
895}
896
897// ---------------------------------------------------------------------------
898// Downstream listener
899// ---------------------------------------------------------------------------
900
901/// Read one line from the downstream socket (up to 4 KiB).
902async fn read_line<S>(stream: &mut S) -> Option<String>
903where
904    S: tokio::io::AsyncRead + Unpin,
905{
906    use tokio::io::AsyncReadExt;
907    let mut buf = Vec::with_capacity(128);
908    let mut byte = [0u8; 1];
909    loop {
910        match stream.read(&mut byte).await {
911            Ok(0) | Err(_) => return None,
912            Ok(_) => {
913                if byte[0] == b'\n' {
914                    break;
915                }
916                buf.push(byte[0]);
917                if buf.len() > 4096 {
918                    return None;
919                }
920            }
921        }
922    }
923    Some(
924        String::from_utf8_lossy(&buf)
925            .trim_end_matches('\r')
926            .to_string(),
927    )
928}
929
930#[cfg(unix)]
931async fn run_listener(
932    registry: Arc<Registry>,
933    share_registry: Arc<ShareRegistry>,
934    sock_path: &str,
935) {
936    let _ = std::fs::remove_file(sock_path);
937    let listener = tokio::net::UnixListener::bind(sock_path).unwrap_or_else(|e| {
938        eprintln!("blit-proxy: cannot bind to {sock_path}: {e}");
939        std::process::exit(1);
940    });
941    log!("blit-proxy: listening on {sock_path}");
942    loop {
943        match listener.accept().await {
944            Ok((stream, _)) => {
945                let registry = registry.clone();
946                let share_registry = share_registry.clone();
947                tokio::spawn(async move {
948                    handle_downstream(registry, share_registry, stream).await;
949                });
950            }
951            Err(e) => log!("blit-proxy: accept error: {e}"),
952        }
953    }
954}
955
956#[cfg(windows)]
957async fn run_listener(
958    registry: Arc<Registry>,
959    share_registry: Arc<ShareRegistry>,
960    pipe_path: &str,
961    first: tokio::net::windows::named_pipe::NamedPipeServer,
962) {
963    use tokio::net::windows::named_pipe::ServerOptions;
964    log!("blit-proxy: listening on {pipe_path}");
965    // Use the pre-created first instance (created with first_pipe_instance(true)
966    // by the caller) to avoid a race window where the pipe name is unowned.
967    let mut next_server = Some(first);
968    loop {
969        // Prepare the next server instance before awaiting the current connection,
970        // so there is always a waiting server end after handoff.
971        let server = match next_server.take() {
972            Some(s) => s,
973            None => match ServerOptions::new()
974                .first_pipe_instance(false)
975                .create(pipe_path)
976            {
977                Ok(s) => s,
978                Err(e) => {
979                    log!("blit-proxy: create pipe instance: {e}");
980                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
981                    continue;
982                }
983            },
984        };
985        // Create the *next* instance before connecting, so the pipe name is
986        // never unowned between connections.
987        let upcoming = ServerOptions::new()
988            .first_pipe_instance(false)
989            .create(pipe_path);
990        if let Err(e) = server.connect().await {
991            log!("blit-proxy: pipe connect: {e}");
992            // Put the upcoming server back for the next iteration.
993            if let Ok(u) = upcoming {
994                next_server = Some(u);
995            }
996            continue;
997        }
998        if let Ok(u) = upcoming {
999            next_server = Some(u);
1000        }
1001        let registry = registry.clone();
1002        let share_registry = share_registry.clone();
1003        tokio::spawn(async move {
1004            handle_downstream(registry, share_registry, server).await;
1005        });
1006    }
1007}
1008
1009async fn handle_downstream<S>(
1010    registry: Arc<Registry>,
1011    share_registry: Arc<ShareRegistry>,
1012    mut downstream: S,
1013) where
1014    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1015{
1016    // Handshake: read "target <uri>\n" or "shutdown\n"
1017    let line = match read_line(&mut downstream).await {
1018        Some(l) => l,
1019        None => return,
1020    };
1021    if line == "shutdown" {
1022        let _ = downstream.write_all(b"ok\n").await;
1023        log!("blit-proxy: shutdown requested, exiting");
1024        std::process::exit(0);
1025    }
1026    let uri = match line.strip_prefix("target ") {
1027        Some(u) if !u.is_empty() => u.to_string(),
1028        _ => {
1029            let _ = downstream.write_all(b"error invalid handshake\n").await;
1030            return;
1031        }
1032    };
1033
1034    if uri.starts_with("share:") {
1035        // Route through session-reuse pool.
1036        let pool = share_registry.get_or_create(&uri).await;
1037
1038        let (session, channel_handle, upstream) = match pool.acquire().await {
1039            Ok(t) => t,
1040            Err(e) => {
1041                log!("blit-proxy: [{uri}] share upstream unavailable: {e}");
1042                let msg = format!("error {e}\n");
1043                let _ = downstream.write_all(msg.as_bytes()).await;
1044                return;
1045            }
1046        };
1047
1048        if downstream.write_all(b"ok\n").await.is_err() {
1049            session.close_channel(channel_handle);
1050            return;
1051        }
1052
1053        pool.client_connected();
1054
1055        let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1056        let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1057
1058        let mut d2u =
1059            tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1060        let mut u2d =
1061            tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1062
1063        tokio::select! { _ = &mut d2u => { u2d.abort(); }, _ = &mut u2d => { d2u.abort(); } }
1064
1065        // Close the DataChannel but keep the WebRTC session alive for reuse.
1066        session.close_channel(channel_handle);
1067        pool.release(session).await;
1068        pool.client_disconnected();
1069        return;
1070    }
1071
1072    let pool = registry.get_or_create(&uri).await;
1073
1074    let upstream = match pool.acquire().await {
1075        Ok(u) => u,
1076        Err(e) => {
1077            log!("blit-proxy: [{uri}] upstream unavailable: {e}");
1078            let msg = format!("error {e}\n");
1079            let _ = downstream.write_all(msg.as_bytes()).await;
1080            return;
1081        }
1082    };
1083
1084    if downstream.write_all(b"ok\n").await.is_err() {
1085        return;
1086    }
1087
1088    pool.client_connected();
1089
1090    let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1091    let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1092
1093    let mut d2u = tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1094    let mut u2d = tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1095
1096    tokio::select! { _ = &mut d2u => { u2d.abort(); }, _ = &mut u2d => { d2u.abort(); } }
1097
1098    pool.client_disconnected();
1099}
1100
1101// ---------------------------------------------------------------------------
1102// Public entry point
1103// ---------------------------------------------------------------------------
1104
1105/// Run the proxy on the current thread (blocks until the process exits).
1106///
1107/// Reads `BLIT_PROXY_SOCK`, `BLIT_PROXY_POOL`, and `BLIT_PROXY_IDLE` from the
1108/// environment. When called from within the `blit` binary instead of the
1109/// standalone `blit-proxy` binary, `verbose` should be `false`.
1110pub fn run(verbose: bool) {
1111    if verbose {
1112        VERBOSE.store(true, Ordering::Relaxed);
1113    }
1114
1115    let sock_path = proxy_socket_path();
1116
1117    let pool_size: usize = std::env::var("BLIT_PROXY_POOL")
1118        .ok()
1119        .and_then(|v| v.parse().ok())
1120        .unwrap_or(4);
1121
1122    let idle_secs: Option<u64> = std::env::var("BLIT_PROXY_IDLE")
1123        .ok()
1124        .and_then(|v| v.parse().ok());
1125
1126    tokio::runtime::Builder::new_multi_thread()
1127        .enable_all()
1128        .build()
1129        .expect("blit-proxy: tokio runtime")
1130        .block_on(async move {
1131            rustls::crypto::ring::default_provider()
1132                .install_default()
1133                .ok(); // may already be installed by the CLI's runtime
1134
1135            let registry = Registry::new(pool_size);
1136            let share_registry = ShareRegistry::new(pool_size);
1137
1138            // Idle-timeout watcher.
1139            if let Some(idle) = idle_secs {
1140                let reg = registry.clone();
1141                let sreg = share_registry.clone();
1142                let sock = sock_path.clone();
1143                tokio::spawn(async move {
1144                    loop {
1145                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1146                        let last = reg
1147                            .latest_activity()
1148                            .await
1149                            .min(sreg.latest_activity().await);
1150                        if last == i64::MAX {
1151                            continue;
1152                        }
1153                        let elapsed = now_secs().saturating_sub(last) as u64;
1154                        if elapsed >= idle {
1155                            log!("blit-proxy: idle for {elapsed}s (limit {idle}s), exiting");
1156                            let _ = std::fs::remove_file(&sock);
1157                            std::process::exit(0);
1158                        }
1159                    }
1160                });
1161            }
1162
1163            #[cfg(unix)]
1164            {
1165                let sock_cleanup = sock_path.clone();
1166                tokio::spawn(async move {
1167                    let _ = tokio::signal::ctrl_c().await;
1168                    let _ = std::fs::remove_file(&sock_cleanup);
1169                    std::process::exit(0);
1170                });
1171                run_listener(registry, share_registry, &sock_path).await;
1172            }
1173            #[cfg(windows)]
1174            {
1175                // Create the first pipe instance before signalling readiness
1176                // so that clients polling the pipe path see it immediately.
1177                use tokio::net::windows::named_pipe::ServerOptions;
1178                let first = ServerOptions::new()
1179                    .first_pipe_instance(true)
1180                    .create(&sock_path)
1181                    .unwrap_or_else(|e| {
1182                        eprintln!("blit-proxy: cannot create pipe {sock_path}: {e}");
1183                        std::process::exit(1);
1184                    });
1185                // Pass `first` into run_listener so the pipe name is never
1186                // unowned between creation and the first client connection.
1187                run_listener(registry, share_registry, &sock_path, first).await;
1188            }
1189        });
1190}
1191
1192// ---------------------------------------------------------------------------
1193// Tests
1194// ---------------------------------------------------------------------------
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    #[test]
1201    fn proxy_socket_path_default() {
1202        // Just verify it produces a non-empty string without panicking.
1203        let p = proxy_socket_path();
1204        assert!(!p.is_empty());
1205        assert!(p.contains("blit-proxy"));
1206    }
1207
1208    #[test]
1209    fn parse_hex_valid() {
1210        assert_eq!(parse_hex("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
1211        assert_eq!(parse_hex(""), Some(vec![]));
1212    }
1213
1214    #[test]
1215    fn parse_hex_odd_length() {
1216        assert_eq!(parse_hex("abc"), None);
1217    }
1218
1219    #[test]
1220    fn parse_hex_invalid_char() {
1221        assert_eq!(parse_hex("zz"), None);
1222    }
1223
1224    #[test]
1225    fn extract_uri_params_no_query() {
1226        let (base, pass, cert) = extract_uri_params("wss://host:3264/");
1227        assert_eq!(base, "wss://host:3264/");
1228        assert_eq!(pass, None);
1229        assert_eq!(cert, None);
1230    }
1231
1232    #[test]
1233    fn extract_uri_params_passphrase() {
1234        let (base, pass, cert) = extract_uri_params("wss://host:3264/?passphrase=secret");
1235        assert_eq!(base, "wss://host:3264/");
1236        assert_eq!(pass, Some("secret".into()));
1237        assert_eq!(cert, None);
1238    }
1239
1240    #[test]
1241    fn extract_uri_params_both() {
1242        let (base, pass, cert) =
1243            extract_uri_params("wt://host:4433/?passphrase=abc&certHash=deadbeef");
1244        assert_eq!(base, "wt://host:4433/");
1245        assert_eq!(pass, Some("abc".into()));
1246        assert_eq!(cert, Some("deadbeef".into()));
1247    }
1248
1249    #[test]
1250    fn extract_uri_params_socket_unchanged() {
1251        let (base, pass, cert) = extract_uri_params("socket:/tmp/blit.sock");
1252        assert_eq!(base, "socket:/tmp/blit.sock");
1253        assert_eq!(pass, None);
1254        assert_eq!(cert, None);
1255    }
1256
1257    #[test]
1258    fn parse_wt_host_port_with_port() {
1259        assert_eq!(parse_wt_host_port("host:4433"), Ok(("host".into(), 4433)));
1260    }
1261
1262    #[test]
1263    fn parse_wt_host_port_default() {
1264        assert_eq!(parse_wt_host_port("host"), Ok(("host".into(), 443)));
1265    }
1266
1267    #[test]
1268    fn percent_decode_basic() {
1269        assert_eq!(percent_decode("hello%20world"), "hello world");
1270        assert_eq!(percent_decode("plain"), "plain");
1271    }
1272
1273    #[test]
1274    fn parse_share_uri_no_hub() {
1275        let (pass, hub) = parse_share_uri("myphrase");
1276        assert_eq!(pass, "myphrase");
1277        assert_eq!(
1278            hub,
1279            blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1280        );
1281    }
1282
1283    #[test]
1284    fn parse_share_uri_with_hub() {
1285        let (pass, hub) = parse_share_uri("myphrase?hub=wss://custom.example.com");
1286        assert_eq!(pass, "myphrase");
1287        assert_eq!(hub, "wss://custom.example.com");
1288    }
1289
1290    #[test]
1291    fn parse_share_uri_hub_normalized() {
1292        let (pass, hub) = parse_share_uri("myphrase?hub=custom.example.com");
1293        assert_eq!(pass, "myphrase");
1294        assert_eq!(hub, "wss://custom.example.com");
1295    }
1296
1297    #[test]
1298    fn parse_share_uri_percent_encoded_passphrase() {
1299        let (pass, hub) = parse_share_uri("my%3Fphrase");
1300        assert_eq!(pass, "my?phrase");
1301        assert_eq!(
1302            hub,
1303            blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1304        );
1305    }
1306}