1use std::collections::{HashMap, VecDeque};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering};
8use tokio::io::{AsyncRead, AsyncReadExt, 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
24pub 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
43struct UpstreamConn {
48 reader: BoxRead,
49 writer: BoxWrite,
50}
51
52struct Pool {
57 idle: Mutex<VecDeque<UpstreamConn>>,
58 active: AtomicUsize,
60 last_activity: AtomicI64,
63 pool_size: usize,
64 upstream_uri: String,
65}
66
67impl Pool {
68 fn new(upstream_uri: String, pool_size: usize) -> Arc<Self> {
69 Arc::new(Self {
70 idle: Mutex::new(VecDeque::new()),
71 active: AtomicUsize::new(0),
72 last_activity: AtomicI64::new(now_secs()),
73 pool_size,
74 upstream_uri,
75 })
76 }
77
78 async fn acquire(&self) -> Result<UpstreamConn, String> {
79 {
80 let mut idle = self.idle.lock().await;
81 if let Some(conn) = idle.pop_front() {
82 return Ok(conn);
83 }
84 }
85 self.connect_one().await
86 }
87
88 async fn connect_one(&self) -> Result<UpstreamConn, String> {
89 connect_upstream(&self.upstream_uri).await
90 }
91
92 fn client_connected(&self) {
93 self.active.fetch_add(1, Ordering::Relaxed);
94 self.last_activity.store(i64::MAX, Ordering::Relaxed);
95 }
96
97 fn client_disconnected(&self) {
98 let prev = self.active.fetch_sub(1, Ordering::Relaxed);
99 if prev == 1 {
100 self.last_activity.store(now_secs(), Ordering::Relaxed);
101 }
102 }
103
104 async fn refill_loop(self: Arc<Self>) {
106 loop {
107 let need = {
108 let idle = self.idle.lock().await;
109 self.pool_size.saturating_sub(idle.len())
110 };
111 for _ in 0..need {
112 match self.connect_one().await {
113 Ok(conn) => {
114 self.idle.lock().await.push_back(conn);
115 }
116 Err(e) => {
117 log!(
118 "blit-proxy: [{uri}] upstream connect failed: {e}",
119 uri = self.upstream_uri
120 );
121 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
122 break;
123 }
124 }
125 }
126 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
127 }
128 }
129}
130
131fn now_secs() -> i64 {
132 std::time::SystemTime::now()
133 .duration_since(std::time::UNIX_EPOCH)
134 .unwrap_or_default()
135 .as_secs() as i64
136}
137
138struct Registry {
143 pools: RwLock<HashMap<String, Arc<Pool>>>,
144 pool_size: usize,
145}
146
147impl Registry {
148 fn new(pool_size: usize) -> Arc<Self> {
149 Arc::new(Self {
150 pools: RwLock::new(HashMap::new()),
151 pool_size,
152 })
153 }
154
155 async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<Pool> {
157 {
159 let pools = self.pools.read().await;
160 if let Some(p) = pools.get(uri) {
161 return p.clone();
162 }
163 }
164 let mut pools = self.pools.write().await;
166 if let Some(p) = pools.get(uri) {
168 return p.clone();
169 }
170 let pool = Pool::new(uri.to_string(), self.pool_size);
171 pools.insert(uri.to_string(), pool.clone());
172 drop(pools);
173
174 let pool_seed = pool.clone();
176 tokio::spawn(async move {
177 for _ in 0..pool_seed.pool_size {
178 match pool_seed.connect_one().await {
179 Ok(conn) => pool_seed.idle.lock().await.push_back(conn),
180 Err(e) => {
181 log!(
182 "blit-proxy: [{uri}] initial connect: {e}",
183 uri = pool_seed.upstream_uri
184 );
185 break;
186 }
187 }
188 }
189 pool_seed.refill_loop().await;
190 });
191
192 pool
193 }
194
195 async fn latest_activity(&self) -> i64 {
198 let pools = self.pools.read().await;
199 if pools.is_empty() {
200 return now_secs();
201 }
202 pools
203 .values()
204 .map(|p| p.last_activity.load(Ordering::Relaxed))
205 .max()
206 .unwrap_or_else(now_secs)
207 }
208}
209
210async fn connect_upstream(uri: &str) -> Result<UpstreamConn, String> {
215 if let Some(rest) = uri.strip_prefix("share:") {
216 return connect_share(rest).await;
217 }
218
219 #[cfg(unix)]
220 if let Some(rest) = uri.strip_prefix("ssh:") {
221 return connect_ssh(rest).await;
222 }
223
224 let (base_uri, passphrase, cert_hash) = extract_uri_params(uri);
226
227 if let Some(path) = base_uri.strip_prefix("socket:") {
228 return connect_socket(path).await;
229 }
230 if let Some(addr) = base_uri.strip_prefix("tcp:") {
231 return connect_tcp(addr).await;
232 }
233 if base_uri.starts_with("ws://") || base_uri.starts_with("wss://") {
234 return connect_ws(&base_uri, passphrase.as_deref()).await;
235 }
236 if let Some(rest) = base_uri.strip_prefix("wt://") {
237 let cert_bytes = cert_hash.as_deref().and_then(parse_hex);
238 return connect_wt(rest, passphrase.as_deref(), &cert_bytes).await;
239 }
240 Err(format!(
241 "unknown upstream URI scheme in '{uri}' \
242 (expected socket:, tcp:, ws://, wss://, wt://, share:, or ssh:)"
243 ))
244}
245
246fn parse_share_uri(rest: &str) -> (String, String) {
252 let (passphrase_raw, hub) = if let Some(q_pos) = rest.find('?') {
253 let phrase = &rest[..q_pos];
254 let query = &rest[q_pos + 1..];
255 let hub = query
256 .split('&')
257 .find_map(|kv| kv.strip_prefix("hub=").map(percent_decode))
258 .unwrap_or_else(|| blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string());
259 (phrase.to_string(), hub)
260 } else {
261 (
262 rest.to_string(),
263 blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string(),
264 )
265 };
266 let passphrase = percent_decode(&passphrase_raw);
267 let hub = blit_webrtc_forwarder::normalize_hub(&hub);
268 (passphrase, hub)
269}
270
271async fn connect_share(rest: &str) -> Result<UpstreamConn, String> {
272 let (passphrase, hub) = parse_share_uri(rest);
273 let stream = blit_webrtc_forwarder::client::connect(&passphrase, &hub)
274 .await
275 .map_err(|e| format!("share:{rest}: {e}"))?;
276 let (r, w) = tokio::io::split(stream);
277 Ok(UpstreamConn {
278 reader: Box::new(r),
279 writer: Box::new(w),
280 })
281}
282
283#[cfg(unix)]
289const SSH_SOCK_SEARCH: &str = r#"if [ -n "$BLIT_SOCK" ]; then S="$BLIT_SOCK"; elif [ -n "$TMPDIR" ] && [ -S "$TMPDIR/blit.sock" ]; then S="$TMPDIR/blit.sock"; elif [ -S "/tmp/blit-$(id -un).sock" ]; then S="/tmp/blit-$(id -un).sock"; elif [ -S "/run/blit/$(id -un).sock" ]; then S="/run/blit/$(id -un).sock"; elif [ -n "$XDG_RUNTIME_DIR" ] && [ -S "$XDG_RUNTIME_DIR/blit.sock" ]; then S="$XDG_RUNTIME_DIR/blit.sock"; else S=/tmp/blit.sock; fi"#;
290
291#[cfg(unix)]
293const SSH_AUTOSTART: &str = "if [ -S \"$S\" ]; then if command -v nc >/dev/null 2>&1; then nc -z -U \"$S\" 2>/dev/null || rm -f \"$S\"; elif command -v socat >/dev/null 2>&1; then socat /dev/null \"UNIX-CONNECT:$S\" 2>/dev/null || rm -f \"$S\"; fi; fi; if ! [ -S \"$S\" ]; then if command -v blit >/dev/null 2>&1; then blit server & i=0; while ! [ -S \"$S\" ] && [ $i -lt 50 ]; do sleep 0.1; i=$((i+1)); done; elif command -v blit-server >/dev/null 2>&1; then blit-server & i=0; while ! [ -S \"$S\" ] && [ $i -lt 50 ]; do sleep 0.1; i=$((i+1)); done; fi; fi";
294
295#[cfg(unix)]
300fn dq_escape(s: &str) -> String {
301 let mut out = String::with_capacity(s.len());
302 for ch in s.chars() {
303 match ch {
304 '\\' | '$' | '`' | '"' => {
305 out.push('\\');
306 out.push(ch);
307 }
308 _ => out.push(ch),
309 }
310 }
311 out
312}
313
314#[cfg(unix)]
317fn parse_ssh_target(rest: &str) -> (&str, Option<&str>) {
318 let colon_search_start = rest.find('@').map(|a| a + 1).unwrap_or(0);
319 if let Some(rel) = rest[colon_search_start..].find(':') {
320 let pos = colon_search_start + rel;
321 let path = &rest[pos + 1..];
322 if path.is_empty() {
323 (rest, None)
324 } else {
325 (&rest[..pos], Some(path))
326 }
327 } else {
328 (rest, None)
329 }
330}
331
332#[cfg(unix)]
337async fn connect_ssh(rest: &str) -> Result<UpstreamConn, String> {
338 if rest.is_empty() {
339 return Err("ssh: destination requires a host".into());
340 }
341 let (host, remote_socket) = parse_ssh_target(rest);
342 if host.starts_with('-') {
343 return Err(format!(
344 "invalid ssh host '{host}': must not start with '-'"
345 ));
346 }
347 let resolve = match remote_socket {
348 Some(path) => format!("S=\"{}\"", dq_escape(path)),
349 None => SSH_SOCK_SEARCH.to_string(),
350 };
351 let bridge = format!(
352 "sh -c '{resolve}; {SSH_AUTOSTART}; if command -v nc >/dev/null 2>&1; then exec nc -U \"$S\"; else exec socat - \"UNIX-CONNECT:$S\"; fi'"
353 );
354 let mut child = tokio::process::Command::new("ssh")
355 .arg("-T")
356 .arg("-o")
357 .arg("ControlMaster=auto")
358 .arg("-o")
359 .arg("ControlPath=/tmp/blit-ssh-%r@%h:%p")
360 .arg("-o")
361 .arg("ControlPersist=300")
362 .arg(host)
363 .arg("--")
364 .arg(&bridge)
365 .stdin(std::process::Stdio::piped())
366 .stdout(std::process::Stdio::piped())
367 .stderr(std::process::Stdio::inherit())
368 .spawn()
369 .map_err(|e| format!("ssh: {e}"))?;
370 let stdin = child
371 .stdin
372 .take()
373 .ok_or_else(|| "ssh: could not get stdin".to_string())?;
374 let stdout = child
375 .stdout
376 .take()
377 .ok_or_else(|| "ssh: could not get stdout".to_string())?;
378 tokio::spawn(async move {
380 let _ = child.wait().await;
381 });
382 Ok(UpstreamConn {
383 reader: Box::new(stdout),
384 writer: Box::new(stdin),
385 })
386}
387
388fn extract_uri_params(uri: &str) -> (String, Option<String>, Option<String>) {
391 if !uri.starts_with("ws://") && !uri.starts_with("wss://") && !uri.starts_with("wt://") {
392 return (uri.to_string(), None, None);
393 }
394 let (base, query) = match uri.find('?') {
396 Some(pos) => (&uri[..pos], Some(&uri[pos + 1..])),
397 None => (uri, None),
398 };
399 let mut passphrase = None;
400 let mut cert_hash = None;
401 if let Some(q) = query {
402 for param in q.split('&') {
403 if let Some(v) = param.strip_prefix("passphrase=") {
404 passphrase = Some(percent_decode(v));
405 } else if let Some(v) = param.strip_prefix("certHash=") {
406 cert_hash = Some(v.to_string());
407 }
408 }
409 }
410 (base.to_string(), passphrase, cert_hash)
411}
412
413fn percent_decode(s: &str) -> String {
414 let mut out = String::with_capacity(s.len());
416 let mut chars = s.chars().peekable();
417 while let Some(c) = chars.next() {
418 if c == '%' {
419 let h1 = chars.next().unwrap_or('0');
420 let h2 = chars.next().unwrap_or('0');
421 if let Ok(b) = u8::from_str_radix(&format!("{h1}{h2}"), 16) {
422 out.push(b as char);
423 continue;
424 }
425 }
426 out.push(c);
427 }
428 out
429}
430
431async fn connect_socket(path: &str) -> Result<UpstreamConn, String> {
432 #[cfg(unix)]
433 {
434 let stream = tokio::net::UnixStream::connect(path)
435 .await
436 .map_err(|e| format!("socket:{path}: {e}"))?;
437 let (r, w) = tokio::io::split(stream);
438 Ok(UpstreamConn {
439 reader: Box::new(r),
440 writer: Box::new(w),
441 })
442 }
443 #[cfg(not(unix))]
444 {
445 use tokio::net::windows::named_pipe::ClientOptions;
446 let pipe = ClientOptions::new()
447 .open(path)
448 .map_err(|e| format!("socket:{path}: {e}"))?;
449 let (r, w) = tokio::io::split(pipe);
450 Ok(UpstreamConn {
451 reader: Box::new(r),
452 writer: Box::new(w),
453 })
454 }
455}
456
457async fn connect_tcp(addr: &str) -> Result<UpstreamConn, String> {
458 let stream = tokio::net::TcpStream::connect(addr)
459 .await
460 .map_err(|e| format!("tcp:{addr}: {e}"))?;
461 let _ = stream.set_nodelay(true);
462 let (r, w) = tokio::io::split(stream);
463 Ok(UpstreamConn {
464 reader: Box::new(r),
465 writer: Box::new(w),
466 })
467}
468
469async fn connect_ws(uri: &str, passphrase: Option<&str>) -> Result<UpstreamConn, String> {
470 use futures_util::{SinkExt, StreamExt};
471 use tokio_tungstenite::tungstenite::Message;
472
473 let (mut ws, _) = tokio_tungstenite::connect_async(uri)
474 .await
475 .map_err(|e| format!("{uri}: {e}"))?;
476
477 let pass = passphrase.unwrap_or("");
478 ws.send(Message::Text(pass.into()))
479 .await
480 .map_err(|e| format!("{uri}: auth send: {e}"))?;
481 match ws.next().await {
482 Some(Ok(Message::Text(t))) if t.trim() == "ok" => {}
483 Some(Ok(Message::Text(t))) => {
484 return Err(format!("{uri}: auth rejected: {}", t.trim()));
485 }
486 other => {
487 return Err(format!("{uri}: unexpected auth response: {other:?}"));
488 }
489 }
490
491 let (ws_write, ws_read) = ws.split();
492 Ok(UpstreamConn {
493 reader: Box::new(WsFrameReader {
494 inner: ws_read,
495 buf: bytes::Bytes::new(),
496 }),
497 writer: Box::new(WsFrameWriter { inner: ws_write }),
498 })
499}
500
501async fn connect_wt(
502 rest: &str,
503 passphrase: Option<&str>,
504 cert_hash: &Option<Vec<u8>>,
505) -> Result<UpstreamConn, String> {
506 use web_transport_quinn as wt;
507
508 let (host, port) = parse_wt_host_port(rest)?;
510 let url: url::Url = format!("https://{host}:{port}/")
511 .parse()
512 .map_err(|e| format!("wt: url: {e}"))?;
513
514 let client: wt::Client = if let Some(hash) = cert_hash {
516 wt::ClientBuilder::new()
517 .with_server_certificate_hashes(vec![hash.clone()])
518 .map_err(|e| format!("wt: client build: {e}"))?
519 } else {
520 wt::ClientBuilder::new()
521 .with_system_roots()
522 .map_err(|e| format!("wt: client build: {e}"))?
523 };
524
525 let session = client
526 .connect(url)
527 .await
528 .map_err(|e| format!("wt: connect {host}:{port}: {e}"))?;
529
530 let (mut send, mut recv) = session
531 .open_bi()
532 .await
533 .map_err(|e| format!("wt: open_bi: {e}"))?;
534
535 let pass = passphrase.unwrap_or("").as_bytes();
537 let mut auth_buf = Vec::with_capacity(2 + pass.len());
538 auth_buf.extend_from_slice(&(pass.len() as u16).to_le_bytes());
539 auth_buf.extend_from_slice(pass);
540 send.write_all(&auth_buf)
541 .await
542 .map_err(|e| format!("wt: auth send: {e}"))?;
543
544 let mut resp = [0u8; 1];
545 recv.read_exact(&mut resp)
546 .await
547 .map_err(|e| format!("wt: auth recv: {e}"))?;
548 if resp[0] != 1 {
549 return Err(format!(
550 "wt: auth rejected (response byte {:#04x})",
551 resp[0]
552 ));
553 }
554
555 Ok(UpstreamConn {
556 reader: Box::new(recv),
557 writer: Box::new(send),
558 })
559}
560
561fn parse_wt_host_port(rest: &str) -> Result<(String, u16), String> {
562 let without_path = rest.split('/').next().unwrap_or(rest);
563 if let Some(colon) = without_path.rfind(':') {
564 let host = without_path[..colon].to_string();
565 let port: u16 = without_path[colon + 1..]
566 .parse()
567 .map_err(|_| format!("wt: invalid port in '{rest}'"))?;
568 Ok((host, port))
569 } else {
570 Ok((without_path.to_string(), 443))
571 }
572}
573
574fn parse_hex(s: &str) -> Option<Vec<u8>> {
575 if !s.len().is_multiple_of(2) {
576 return None;
577 }
578 (0..s.len())
579 .step_by(2)
580 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
581 .collect()
582}
583
584use futures_util::{SinkExt, StreamExt};
589use tokio_tungstenite::tungstenite::Message;
590
591type WsSink = futures_util::stream::SplitSink<
592 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
593 Message,
594>;
595type WsStream = futures_util::stream::SplitStream<
596 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
597>;
598
599struct WsFrameReader {
600 inner: WsStream,
601 buf: bytes::Bytes,
602}
603
604impl AsyncRead for WsFrameReader {
605 fn poll_read(
606 mut self: std::pin::Pin<&mut Self>,
607 cx: &mut std::task::Context<'_>,
608 out: &mut tokio::io::ReadBuf<'_>,
609 ) -> std::task::Poll<std::io::Result<()>> {
610 loop {
611 if !self.buf.is_empty() {
612 let n = out.remaining().min(self.buf.len());
613 out.put_slice(&self.buf[..n]);
614 self.buf = self.buf.slice(n..);
615 return std::task::Poll::Ready(Ok(()));
616 }
617 match self.inner.poll_next_unpin(cx) {
618 std::task::Poll::Pending => return std::task::Poll::Pending,
619 std::task::Poll::Ready(None) => return std::task::Poll::Ready(Ok(())),
620 std::task::Poll::Ready(Some(Err(e))) => {
621 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
622 }
623 std::task::Poll::Ready(Some(Ok(msg))) => {
624 let data = match msg {
625 Message::Binary(d) => d,
626 Message::Close(_) => return std::task::Poll::Ready(Ok(())),
627 _ => continue,
628 };
629 let len = data.len() as u32;
630 let mut framed = Vec::with_capacity(4 + data.len());
631 framed.extend_from_slice(&len.to_le_bytes());
632 framed.extend_from_slice(&data);
633 self.buf = bytes::Bytes::from(framed);
634 }
635 }
636 }
637 }
638}
639
640struct WsFrameWriter {
641 inner: WsSink,
642}
643
644impl AsyncWrite for WsFrameWriter {
645 fn poll_write(
646 mut self: std::pin::Pin<&mut Self>,
647 cx: &mut std::task::Context<'_>,
648 buf: &[u8],
649 ) -> std::task::Poll<std::io::Result<usize>> {
650 if buf.len() < 4 {
651 return std::task::Poll::Ready(Err(std::io::Error::new(
652 std::io::ErrorKind::InvalidData,
653 "ws frame writer: buffer too small for length prefix",
654 )));
655 }
656 let len = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize;
657 if buf.len() < 4 + len {
658 return std::task::Poll::Ready(Err(std::io::Error::new(
659 std::io::ErrorKind::InvalidData,
660 format!(
661 "ws frame writer: buffer ({}) too small for frame payload ({})",
662 buf.len(),
663 4 + len
664 ),
665 )));
666 }
667 let payload = buf[4..4 + len].to_vec();
668 match self.inner.poll_ready_unpin(cx) {
669 std::task::Poll::Pending => return std::task::Poll::Pending,
670 std::task::Poll::Ready(Err(e)) => {
671 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
672 }
673 std::task::Poll::Ready(Ok(())) => {}
674 }
675 match self.inner.start_send_unpin(Message::Binary(payload.into())) {
676 Err(e) => std::task::Poll::Ready(Err(std::io::Error::other(e))),
677 Ok(()) => std::task::Poll::Ready(Ok(4 + len)),
678 }
679 }
680 fn poll_flush(
681 mut self: std::pin::Pin<&mut Self>,
682 cx: &mut std::task::Context<'_>,
683 ) -> std::task::Poll<std::io::Result<()>> {
684 self.inner
685 .poll_flush_unpin(cx)
686 .map_err(std::io::Error::other)
687 }
688 fn poll_shutdown(
689 mut self: std::pin::Pin<&mut Self>,
690 cx: &mut std::task::Context<'_>,
691 ) -> std::task::Poll<std::io::Result<()>> {
692 self.inner
693 .poll_close_unpin(cx)
694 .map_err(std::io::Error::other)
695 }
696}
697
698#[cfg(unix)]
704async fn read_line(stream: &mut tokio::net::UnixStream) -> Option<String> {
705 let mut buf = Vec::with_capacity(128);
706 let mut byte = [0u8; 1];
707 loop {
708 match stream.read(&mut byte).await {
709 Ok(0) | Err(_) => return None,
710 Ok(_) => {
711 if byte[0] == b'\n' {
712 break;
713 }
714 buf.push(byte[0]);
715 if buf.len() > 4096 {
716 return None;
717 }
718 }
719 }
720 }
721 Some(
722 String::from_utf8_lossy(&buf)
723 .trim_end_matches('\r')
724 .to_string(),
725 )
726}
727
728#[cfg(unix)]
729async fn run_listener(registry: Arc<Registry>, sock_path: &str) {
730 let _ = std::fs::remove_file(sock_path);
731 let listener = tokio::net::UnixListener::bind(sock_path).unwrap_or_else(|e| {
732 eprintln!("blit-proxy: cannot bind to {sock_path}: {e}");
733 std::process::exit(1);
734 });
735 log!("blit-proxy: listening on {sock_path}");
736 loop {
737 match listener.accept().await {
738 Ok((stream, _)) => {
739 let registry = registry.clone();
740 tokio::spawn(async move {
741 handle_downstream(registry, stream).await;
742 });
743 }
744 Err(e) => log!("blit-proxy: accept error: {e}"),
745 }
746 }
747}
748
749#[cfg(unix)]
750async fn handle_downstream(registry: Arc<Registry>, mut downstream: tokio::net::UnixStream) {
751 let line = match read_line(&mut downstream).await {
753 Some(l) => l,
754 None => return,
755 };
756 if line == "shutdown" {
757 let _ = downstream.write_all(b"ok\n").await;
758 log!("blit-proxy: shutdown requested, exiting");
759 std::process::exit(0);
760 }
761 let uri = match line.strip_prefix("target ") {
762 Some(u) if !u.is_empty() => u.to_string(),
763 _ => {
764 let _ = downstream.write_all(b"error invalid handshake\n").await;
765 return;
766 }
767 };
768
769 let pool = registry.get_or_create(&uri).await;
770
771 let upstream = match pool.acquire().await {
772 Ok(u) => u,
773 Err(e) => {
774 log!("blit-proxy: [{uri}] upstream unavailable: {e}");
775 let msg = format!("error {e}\n");
776 let _ = downstream.write_all(msg.as_bytes()).await;
777 return;
778 }
779 };
780
781 if downstream.write_all(b"ok\n").await.is_err() {
782 return;
783 }
784
785 pool.client_connected();
786
787 let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
788 let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
789
790 let mut d2u = tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
791 let mut u2d = tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
792
793 tokio::select! { _ = &mut d2u => { u2d.abort(); }, _ = &mut u2d => { d2u.abort(); } }
794
795 pool.client_disconnected();
796}
797
798pub fn run(verbose: bool) {
808 if verbose {
809 VERBOSE.store(true, Ordering::Relaxed);
810 }
811
812 let sock_path = proxy_socket_path();
813
814 let pool_size: usize = std::env::var("BLIT_PROXY_POOL")
815 .ok()
816 .and_then(|v| v.parse().ok())
817 .unwrap_or(4);
818
819 let idle_secs: Option<u64> = std::env::var("BLIT_PROXY_IDLE")
820 .ok()
821 .and_then(|v| v.parse().ok());
822
823 tokio::runtime::Builder::new_multi_thread()
824 .enable_all()
825 .build()
826 .expect("blit-proxy: tokio runtime")
827 .block_on(async move {
828 rustls::crypto::ring::default_provider()
829 .install_default()
830 .ok(); let registry = Registry::new(pool_size);
833
834 if let Some(idle) = idle_secs {
836 let reg = registry.clone();
837 let sock = sock_path.clone();
838 tokio::spawn(async move {
839 loop {
840 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
841 let last = reg.latest_activity().await;
842 if last == i64::MAX {
843 continue;
844 }
845 let elapsed = now_secs().saturating_sub(last) as u64;
846 if elapsed >= idle {
847 log!("blit-proxy: idle for {elapsed}s (limit {idle}s), exiting");
848 let _ = std::fs::remove_file(&sock);
849 std::process::exit(0);
850 }
851 }
852 });
853 }
854
855 #[cfg(unix)]
856 {
857 let sock_cleanup = sock_path.clone();
858 tokio::spawn(async move {
859 let _ = tokio::signal::ctrl_c().await;
860 let _ = std::fs::remove_file(&sock_cleanup);
861 std::process::exit(0);
862 });
863 run_listener(registry, &sock_path).await;
864 }
865 #[cfg(not(unix))]
866 {
867 eprintln!("blit-proxy: Windows not yet supported");
868 std::process::exit(1);
869 }
870 });
871}
872
873#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
882 fn proxy_socket_path_default() {
883 let p = proxy_socket_path();
885 assert!(!p.is_empty());
886 assert!(p.contains("blit-proxy"));
887 }
888
889 #[test]
890 fn parse_hex_valid() {
891 assert_eq!(parse_hex("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
892 assert_eq!(parse_hex(""), Some(vec![]));
893 }
894
895 #[test]
896 fn parse_hex_odd_length() {
897 assert_eq!(parse_hex("abc"), None);
898 }
899
900 #[test]
901 fn parse_hex_invalid_char() {
902 assert_eq!(parse_hex("zz"), None);
903 }
904
905 #[test]
906 fn extract_uri_params_no_query() {
907 let (base, pass, cert) = extract_uri_params("wss://host:3264/");
908 assert_eq!(base, "wss://host:3264/");
909 assert_eq!(pass, None);
910 assert_eq!(cert, None);
911 }
912
913 #[test]
914 fn extract_uri_params_passphrase() {
915 let (base, pass, cert) = extract_uri_params("wss://host:3264/?passphrase=secret");
916 assert_eq!(base, "wss://host:3264/");
917 assert_eq!(pass, Some("secret".into()));
918 assert_eq!(cert, None);
919 }
920
921 #[test]
922 fn extract_uri_params_both() {
923 let (base, pass, cert) =
924 extract_uri_params("wt://host:4433/?passphrase=abc&certHash=deadbeef");
925 assert_eq!(base, "wt://host:4433/");
926 assert_eq!(pass, Some("abc".into()));
927 assert_eq!(cert, Some("deadbeef".into()));
928 }
929
930 #[test]
931 fn extract_uri_params_socket_unchanged() {
932 let (base, pass, cert) = extract_uri_params("socket:/tmp/blit.sock");
933 assert_eq!(base, "socket:/tmp/blit.sock");
934 assert_eq!(pass, None);
935 assert_eq!(cert, None);
936 }
937
938 #[test]
939 fn parse_wt_host_port_with_port() {
940 assert_eq!(parse_wt_host_port("host:4433"), Ok(("host".into(), 4433)));
941 }
942
943 #[test]
944 fn parse_wt_host_port_default() {
945 assert_eq!(parse_wt_host_port("host"), Ok(("host".into(), 443)));
946 }
947
948 #[test]
949 fn percent_decode_basic() {
950 assert_eq!(percent_decode("hello%20world"), "hello world");
951 assert_eq!(percent_decode("plain"), "plain");
952 }
953
954 #[test]
955 fn parse_share_uri_no_hub() {
956 let (pass, hub) = parse_share_uri("myphrase");
957 assert_eq!(pass, "myphrase");
958 assert_eq!(
959 hub,
960 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
961 );
962 }
963
964 #[test]
965 fn parse_share_uri_with_hub() {
966 let (pass, hub) = parse_share_uri("myphrase?hub=wss://custom.example.com");
967 assert_eq!(pass, "myphrase");
968 assert_eq!(hub, "wss://custom.example.com");
969 }
970
971 #[test]
972 fn parse_share_uri_hub_normalized() {
973 let (pass, hub) = parse_share_uri("myphrase?hub=custom.example.com");
974 assert_eq!(pass, "myphrase");
975 assert_eq!(hub, "wss://custom.example.com");
976 }
977
978 #[test]
979 fn parse_share_uri_percent_encoded_passphrase() {
980 let (pass, hub) = parse_share_uri("my%3Fphrase");
981 assert_eq!(pass, "my?phrase");
982 assert_eq!(
983 hub,
984 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
985 );
986 }
987}