1use 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, Notify, 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
43pub 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
65pub 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 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 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
140struct UpstreamConn {
145 reader: BoxRead,
146 writer: BoxWrite,
147}
148
149struct SharePool {
158 mux: Mutex<Option<blit_webrtc_forwarder::client::MuxSession>>,
159 passphrase: String,
160 hub: String,
161 active: AtomicUsize,
162 last_activity: AtomicI64,
163}
164
165impl SharePool {
166 fn new(passphrase: String, hub: String) -> Arc<Self> {
167 Arc::new(Self {
168 mux: Mutex::new(None),
169 passphrase,
170 hub,
171 active: AtomicUsize::new(0),
172 last_activity: AtomicI64::new(now_secs()),
173 })
174 }
175
176 async fn acquire(&self) -> Result<(u16, UpstreamConn), String> {
182 {
184 let mux = self.mux.lock().await;
185 if let Some(ref m) = *mux
186 && m.is_alive()
187 {
188 let (id, stream) = m.open_stream()?;
189 let (r, w) = tokio::io::split(stream);
190 return Ok((
191 id,
192 UpstreamConn {
193 reader: Box::new(r),
194 writer: Box::new(w),
195 },
196 ));
197 }
198 }
199
200 let hub_url = blit_webrtc_forwarder::normalize_hub(&self.hub);
202 let new_mux =
203 blit_webrtc_forwarder::client::MuxSession::establish(&self.passphrase, &hub_url)
204 .await
205 .map_err(|e| format!("{e}"))?;
206
207 let (id, stream) = new_mux.open_stream()?;
208 *self.mux.lock().await = Some(new_mux);
209
210 let (r, w) = tokio::io::split(stream);
211 Ok((
212 id,
213 UpstreamConn {
214 reader: Box::new(r),
215 writer: Box::new(w),
216 },
217 ))
218 }
219
220 fn recycle(&self, stream_id: u16) {
222 if let Ok(mux) = self.mux.try_lock()
223 && let Some(ref m) = *mux
224 {
225 m.close_stream(stream_id);
226 }
227 }
228
229 fn client_connected(&self) {
230 self.active.fetch_add(1, Ordering::Relaxed);
231 self.last_activity.store(i64::MAX, Ordering::Relaxed);
232 }
233
234 fn client_disconnected(&self) {
235 let prev = self.active.fetch_sub(1, Ordering::Relaxed);
236 if prev == 1 {
237 self.last_activity.store(now_secs(), Ordering::Relaxed);
238 }
239 }
240}
241
242struct ShareRegistry {
243 pools: RwLock<HashMap<String, Arc<SharePool>>>,
244}
245
246impl ShareRegistry {
247 fn new() -> Arc<Self> {
248 Arc::new(Self {
249 pools: RwLock::new(HashMap::new()),
250 })
251 }
252
253 async fn get_or_create(&self, uri: &str) -> Arc<SharePool> {
254 {
255 let pools = self.pools.read().await;
256 if let Some(p) = pools.get(uri) {
257 return p.clone();
258 }
259 }
260 let mut pools = self.pools.write().await;
261 if let Some(p) = pools.get(uri) {
262 return p.clone();
263 }
264 let (passphrase, hub) = parse_share_uri(uri.strip_prefix("share:").unwrap_or(uri));
265 let pool = SharePool::new(passphrase, hub);
266 pools.insert(uri.to_string(), pool.clone());
267 pool
268 }
269
270 async fn latest_activity(&self) -> i64 {
271 let pools = self.pools.read().await;
272 if pools.is_empty() {
273 return now_secs();
274 }
275 pools
276 .values()
277 .map(|p| p.last_activity.load(Ordering::Relaxed))
278 .max()
279 .unwrap_or_else(now_secs)
280 }
281}
282
283struct Pool {
288 idle: Mutex<VecDeque<UpstreamConn>>,
289 active: AtomicUsize,
291 last_activity: AtomicI64,
294 pool_size: usize,
295 upstream_uri: String,
296 refill_notify: Notify,
298}
299
300impl Pool {
301 fn new(upstream_uri: String, pool_size: usize) -> Arc<Self> {
302 Arc::new(Self {
303 idle: Mutex::new(VecDeque::new()),
304 active: AtomicUsize::new(0),
305 last_activity: AtomicI64::new(now_secs()),
306 pool_size,
307 upstream_uri,
308 refill_notify: Notify::new(),
309 })
310 }
311
312 async fn acquire(&self) -> Result<UpstreamConn, String> {
313 {
314 let mut idle = self.idle.lock().await;
315 if let Some(conn) = idle.pop_front() {
316 self.refill_notify.notify_one();
317 return Ok(conn);
318 }
319 }
320 self.connect_one().await
321 }
322
323 async fn connect_one(&self) -> Result<UpstreamConn, String> {
324 connect_upstream(&self.upstream_uri).await
325 }
326
327 fn client_connected(&self) {
328 self.active.fetch_add(1, Ordering::Relaxed);
329 self.last_activity.store(i64::MAX, Ordering::Relaxed);
330 }
331
332 fn client_disconnected(&self) {
333 let prev = self.active.fetch_sub(1, Ordering::Relaxed);
334 if prev == 1 {
335 self.last_activity.store(now_secs(), Ordering::Relaxed);
336 }
337 }
338
339 async fn refill_loop(self: Arc<Self>) {
345 loop {
346 let need = {
347 let idle = self.idle.lock().await;
348 self.pool_size.saturating_sub(idle.len())
349 };
350 if need > 0 {
351 let mut tasks = Vec::with_capacity(need);
352 for _ in 0..need {
353 let pool = self.clone();
354 tasks.push(tokio::spawn(async move { pool.connect_one().await }));
355 }
356 let mut any_failed = false;
357 for task in tasks {
358 match task.await {
359 Ok(Ok(conn)) => {
360 self.idle.lock().await.push_back(conn);
361 }
362 Ok(Err(e)) => {
363 log!(
364 "blit-proxy: [{uri}] upstream connect failed: {e}",
365 uri = self.upstream_uri
366 );
367 any_failed = true;
368 }
369 Err(_) => {
370 any_failed = true;
371 }
372 }
373 }
374 if any_failed {
375 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
376 }
377 }
378 tokio::select! {
380 _ = self.refill_notify.notified() => {}
381 _ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {}
382 }
383 }
384 }
385}
386
387fn now_secs() -> i64 {
388 std::time::SystemTime::now()
389 .duration_since(std::time::UNIX_EPOCH)
390 .unwrap_or_default()
391 .as_secs() as i64
392}
393
394struct Registry {
399 pools: RwLock<HashMap<String, Arc<Pool>>>,
400 pool_size: usize,
401}
402
403impl Registry {
404 fn new(pool_size: usize) -> Arc<Self> {
405 Arc::new(Self {
406 pools: RwLock::new(HashMap::new()),
407 pool_size,
408 })
409 }
410
411 async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<Pool> {
413 {
415 let pools = self.pools.read().await;
416 if let Some(p) = pools.get(uri) {
417 return p.clone();
418 }
419 }
420 let mut pools = self.pools.write().await;
422 if let Some(p) = pools.get(uri) {
424 return p.clone();
425 }
426
427 let effective_pool_size = if uri.starts_with("ssh:") {
432 0
433 } else {
434 self.pool_size
435 };
436
437 let pool = Pool::new(uri.to_string(), effective_pool_size);
438 pools.insert(uri.to_string(), pool.clone());
439 drop(pools);
440
441 if effective_pool_size > 0 {
443 let pool_seed = pool.clone();
444 tokio::spawn(async move {
445 let mut tasks = Vec::with_capacity(pool_seed.pool_size);
446 for _ in 0..pool_seed.pool_size {
447 let p = pool_seed.clone();
448 tasks.push(tokio::spawn(async move { p.connect_one().await }));
449 }
450 for task in tasks {
451 match task.await {
452 Ok(Ok(conn)) => pool_seed.idle.lock().await.push_back(conn),
453 Ok(Err(e)) => {
454 log!(
455 "blit-proxy: [{uri}] initial connect: {e}",
456 uri = pool_seed.upstream_uri
457 );
458 }
459 Err(_) => {}
460 }
461 }
462 pool_seed.refill_loop().await;
463 });
464 }
465
466 pool
467 }
468
469 async fn latest_activity(&self) -> i64 {
472 let pools = self.pools.read().await;
473 if pools.is_empty() {
474 return now_secs();
475 }
476 pools
477 .values()
478 .map(|p| p.last_activity.load(Ordering::Relaxed))
479 .max()
480 .unwrap_or_else(now_secs)
481 }
482}
483
484async fn connect_upstream(uri: &str) -> Result<UpstreamConn, String> {
489 if let Some(rest) = uri.strip_prefix("share:") {
490 return connect_share(rest).await;
491 }
492
493 if let Some(rest) = uri.strip_prefix("ssh:") {
494 return connect_ssh(rest).await;
495 }
496
497 let (base_uri, passphrase, cert_hash) = extract_uri_params(uri);
499
500 if let Some(path) = base_uri.strip_prefix("socket:") {
501 return connect_socket(path).await;
502 }
503 if let Some(addr) = base_uri.strip_prefix("tcp:") {
504 return connect_tcp(addr).await;
505 }
506 if base_uri.starts_with("ws://") || base_uri.starts_with("wss://") {
507 return connect_ws(&base_uri, passphrase.as_deref()).await;
508 }
509 if let Some(rest) = base_uri.strip_prefix("wt://") {
510 let cert_bytes = cert_hash.as_deref().and_then(parse_hex);
511 return connect_wt(rest, passphrase.as_deref(), &cert_bytes).await;
512 }
513 Err(format!(
514 "unknown upstream URI scheme in '{uri}' \
515 (expected socket:, tcp:, ws://, wss://, wt://, share:, or ssh:)"
516 ))
517}
518
519fn parse_share_uri(rest: &str) -> (String, String) {
525 let (passphrase_raw, hub) = if let Some(q_pos) = rest.find('?') {
526 let phrase = &rest[..q_pos];
527 let query = &rest[q_pos + 1..];
528 let hub = query
529 .split('&')
530 .find_map(|kv| kv.strip_prefix("hub=").map(percent_decode))
531 .unwrap_or_else(|| blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string());
532 (phrase.to_string(), hub)
533 } else {
534 (
535 rest.to_string(),
536 blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string(),
537 )
538 };
539 let passphrase = percent_decode(&passphrase_raw);
540 let hub = blit_webrtc_forwarder::normalize_hub(&hub);
541 (passphrase, hub)
542}
543
544async fn connect_share(rest: &str) -> Result<UpstreamConn, String> {
545 let (passphrase, hub) = parse_share_uri(rest);
546 let stream = blit_webrtc_forwarder::client::connect(&passphrase, &hub)
547 .await
548 .map_err(|e| format!("share:{rest}: {e}"))?;
549 let (r, w) = tokio::io::split(stream);
550 Ok(UpstreamConn {
551 reader: Box::new(r),
552 writer: Box::new(w),
553 })
554}
555
556fn ssh_pool() -> &'static blit_ssh::SshPool {
563 static POOL: std::sync::OnceLock<blit_ssh::SshPool> = std::sync::OnceLock::new();
564 POOL.get_or_init(blit_ssh::SshPool::new)
565}
566
567async fn connect_ssh(rest: &str) -> Result<UpstreamConn, String> {
573 if rest.is_empty() {
574 return Err("ssh: destination requires a host".into());
575 }
576 let (user, host, socket) = blit_ssh::parse_ssh_uri(rest);
577 let stream = ssh_pool()
578 .connect(&host, user.as_deref(), socket.as_deref())
579 .await
580 .map_err(|e| format!("ssh:{rest}: {e}"))?;
581 let (r, w) = tokio::io::split(stream);
582 Ok(UpstreamConn {
583 reader: Box::new(r),
584 writer: Box::new(w),
585 })
586}
587
588fn extract_uri_params(uri: &str) -> (String, Option<String>, Option<String>) {
591 if !uri.starts_with("ws://") && !uri.starts_with("wss://") && !uri.starts_with("wt://") {
592 return (uri.to_string(), None, None);
593 }
594 let (base, query) = match uri.find('?') {
596 Some(pos) => (&uri[..pos], Some(&uri[pos + 1..])),
597 None => (uri, None),
598 };
599 let mut passphrase = None;
600 let mut cert_hash = None;
601 if let Some(q) = query {
602 for param in q.split('&') {
603 if let Some(v) = param.strip_prefix("passphrase=") {
604 passphrase = Some(percent_decode(v));
605 } else if let Some(v) = param.strip_prefix("certHash=") {
606 cert_hash = Some(v.to_string());
607 }
608 }
609 }
610 (base.to_string(), passphrase, cert_hash)
611}
612
613fn percent_decode(s: &str) -> String {
614 let mut out = String::with_capacity(s.len());
616 let mut chars = s.chars().peekable();
617 while let Some(c) = chars.next() {
618 if c == '%' {
619 let h1 = chars.next().unwrap_or('0');
620 let h2 = chars.next().unwrap_or('0');
621 if let Ok(b) = u8::from_str_radix(&format!("{h1}{h2}"), 16) {
622 out.push(b as char);
623 continue;
624 }
625 }
626 out.push(c);
627 }
628 out
629}
630
631async fn connect_socket(path: &str) -> Result<UpstreamConn, String> {
632 #[cfg(unix)]
633 {
634 let stream = tokio::net::UnixStream::connect(path)
635 .await
636 .map_err(|e| format!("socket:{path}: {e}"))?;
637 let (r, w) = tokio::io::split(stream);
638 Ok(UpstreamConn {
639 reader: Box::new(r),
640 writer: Box::new(w),
641 })
642 }
643 #[cfg(not(unix))]
644 {
645 use tokio::net::windows::named_pipe::ClientOptions;
646 let pipe = ClientOptions::new()
647 .open(path)
648 .map_err(|e| format!("socket:{path}: {e}"))?;
649 let (r, w) = tokio::io::split(pipe);
650 Ok(UpstreamConn {
651 reader: Box::new(r),
652 writer: Box::new(w),
653 })
654 }
655}
656
657async fn connect_tcp(addr: &str) -> Result<UpstreamConn, String> {
658 let stream = tokio::net::TcpStream::connect(addr)
659 .await
660 .map_err(|e| format!("tcp:{addr}: {e}"))?;
661 let _ = stream.set_nodelay(true);
662 let (r, w) = tokio::io::split(stream);
663 Ok(UpstreamConn {
664 reader: Box::new(r),
665 writer: Box::new(w),
666 })
667}
668
669async fn connect_ws(uri: &str, passphrase: Option<&str>) -> Result<UpstreamConn, String> {
670 use futures_util::{SinkExt, StreamExt};
671 use tokio_tungstenite::tungstenite::Message;
672
673 let (mut ws, _) = tokio_tungstenite::connect_async(uri)
674 .await
675 .map_err(|e| format!("{uri}: {e}"))?;
676
677 let pass = passphrase.unwrap_or("");
678 ws.send(Message::Text(pass.into()))
679 .await
680 .map_err(|e| format!("{uri}: auth send: {e}"))?;
681 match ws.next().await {
682 Some(Ok(Message::Text(t))) if t.trim() == "ok" => {}
683 Some(Ok(Message::Text(t))) => {
684 return Err(format!("{uri}: auth rejected: {}", t.trim()));
685 }
686 other => {
687 return Err(format!("{uri}: unexpected auth response: {other:?}"));
688 }
689 }
690
691 let (ws_write, ws_read) = ws.split();
692 Ok(UpstreamConn {
693 reader: Box::new(WsFrameReader {
694 inner: ws_read,
695 buf: bytes::Bytes::new(),
696 }),
697 writer: Box::new(WsFrameWriter { inner: ws_write }),
698 })
699}
700
701async fn connect_wt(
702 rest: &str,
703 passphrase: Option<&str>,
704 cert_hash: &Option<Vec<u8>>,
705) -> Result<UpstreamConn, String> {
706 use web_transport_quinn as wt;
707
708 let (host, port) = parse_wt_host_port(rest)?;
710 let url: url::Url = format!("https://{host}:{port}/")
711 .parse()
712 .map_err(|e| format!("wt: url: {e}"))?;
713
714 let client: wt::Client = if let Some(hash) = cert_hash {
716 wt::ClientBuilder::new()
717 .with_server_certificate_hashes(vec![hash.clone()])
718 .map_err(|e| format!("wt: client build: {e}"))?
719 } else {
720 wt::ClientBuilder::new()
721 .with_system_roots()
722 .map_err(|e| format!("wt: client build: {e}"))?
723 };
724
725 let session = client
726 .connect(url)
727 .await
728 .map_err(|e| format!("wt: connect {host}:{port}: {e}"))?;
729
730 let (mut send, mut recv) = session
731 .open_bi()
732 .await
733 .map_err(|e| format!("wt: open_bi: {e}"))?;
734
735 let pass = passphrase.unwrap_or("").as_bytes();
737 let mut auth_buf = Vec::with_capacity(2 + pass.len());
738 auth_buf.extend_from_slice(&(pass.len() as u16).to_le_bytes());
739 auth_buf.extend_from_slice(pass);
740 send.write_all(&auth_buf)
741 .await
742 .map_err(|e| format!("wt: auth send: {e}"))?;
743
744 let mut resp = [0u8; 1];
745 recv.read_exact(&mut resp)
746 .await
747 .map_err(|e| format!("wt: auth recv: {e}"))?;
748 if resp[0] != 1 {
749 return Err(format!(
750 "wt: auth rejected (response byte {:#04x})",
751 resp[0]
752 ));
753 }
754
755 Ok(UpstreamConn {
756 reader: Box::new(recv),
757 writer: Box::new(send),
758 })
759}
760
761fn parse_wt_host_port(rest: &str) -> Result<(String, u16), String> {
762 let without_path = rest.split('/').next().unwrap_or(rest);
763 if let Some(colon) = without_path.rfind(':') {
764 let host = without_path[..colon].to_string();
765 let port: u16 = without_path[colon + 1..]
766 .parse()
767 .map_err(|_| format!("wt: invalid port in '{rest}'"))?;
768 Ok((host, port))
769 } else {
770 Ok((without_path.to_string(), 443))
771 }
772}
773
774fn parse_hex(s: &str) -> Option<Vec<u8>> {
775 if !s.len().is_multiple_of(2) {
776 return None;
777 }
778 (0..s.len())
779 .step_by(2)
780 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
781 .collect()
782}
783
784use futures_util::{SinkExt, StreamExt};
789use tokio_tungstenite::tungstenite::Message;
790
791type WsSink = futures_util::stream::SplitSink<
792 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
793 Message,
794>;
795type WsStream = futures_util::stream::SplitStream<
796 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
797>;
798
799struct WsFrameReader {
800 inner: WsStream,
801 buf: bytes::Bytes,
802}
803
804impl AsyncRead for WsFrameReader {
805 fn poll_read(
806 mut self: std::pin::Pin<&mut Self>,
807 cx: &mut std::task::Context<'_>,
808 out: &mut tokio::io::ReadBuf<'_>,
809 ) -> std::task::Poll<std::io::Result<()>> {
810 loop {
811 if !self.buf.is_empty() {
812 let n = out.remaining().min(self.buf.len());
813 out.put_slice(&self.buf[..n]);
814 self.buf = self.buf.slice(n..);
815 return std::task::Poll::Ready(Ok(()));
816 }
817 match self.inner.poll_next_unpin(cx) {
818 std::task::Poll::Pending => return std::task::Poll::Pending,
819 std::task::Poll::Ready(None) => return std::task::Poll::Ready(Ok(())),
820 std::task::Poll::Ready(Some(Err(e))) => {
821 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
822 }
823 std::task::Poll::Ready(Some(Ok(msg))) => {
824 let data = match msg {
825 Message::Binary(d) => d,
826 Message::Close(_) => return std::task::Poll::Ready(Ok(())),
827 _ => continue,
828 };
829 let len = data.len() as u32;
830 let mut framed = Vec::with_capacity(4 + data.len());
831 framed.extend_from_slice(&len.to_le_bytes());
832 framed.extend_from_slice(&data);
833 self.buf = bytes::Bytes::from(framed);
834 }
835 }
836 }
837 }
838}
839
840struct WsFrameWriter {
841 inner: WsSink,
842}
843
844impl AsyncWrite for WsFrameWriter {
845 fn poll_write(
846 mut self: std::pin::Pin<&mut Self>,
847 cx: &mut std::task::Context<'_>,
848 buf: &[u8],
849 ) -> std::task::Poll<std::io::Result<usize>> {
850 if buf.len() < 4 {
851 return std::task::Poll::Ready(Err(std::io::Error::new(
852 std::io::ErrorKind::InvalidData,
853 "ws frame writer: buffer too small for length prefix",
854 )));
855 }
856 let len = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize;
857 if buf.len() < 4 + len {
858 return std::task::Poll::Ready(Err(std::io::Error::new(
859 std::io::ErrorKind::InvalidData,
860 format!(
861 "ws frame writer: buffer ({}) too small for frame payload ({})",
862 buf.len(),
863 4 + len
864 ),
865 )));
866 }
867 let payload = buf[4..4 + len].to_vec();
868 match self.inner.poll_ready_unpin(cx) {
869 std::task::Poll::Pending => return std::task::Poll::Pending,
870 std::task::Poll::Ready(Err(e)) => {
871 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
872 }
873 std::task::Poll::Ready(Ok(())) => {}
874 }
875 match self.inner.start_send_unpin(Message::Binary(payload.into())) {
876 Err(e) => std::task::Poll::Ready(Err(std::io::Error::other(e))),
877 Ok(()) => std::task::Poll::Ready(Ok(4 + len)),
878 }
879 }
880 fn poll_flush(
881 mut self: std::pin::Pin<&mut Self>,
882 cx: &mut std::task::Context<'_>,
883 ) -> std::task::Poll<std::io::Result<()>> {
884 self.inner
885 .poll_flush_unpin(cx)
886 .map_err(std::io::Error::other)
887 }
888 fn poll_shutdown(
889 mut self: std::pin::Pin<&mut Self>,
890 cx: &mut std::task::Context<'_>,
891 ) -> std::task::Poll<std::io::Result<()>> {
892 self.inner
893 .poll_close_unpin(cx)
894 .map_err(std::io::Error::other)
895 }
896}
897
898async fn read_line<S>(stream: &mut S) -> Option<String>
904where
905 S: tokio::io::AsyncRead + Unpin,
906{
907 use tokio::io::AsyncReadExt;
908 let mut buf = Vec::with_capacity(128);
909 let mut byte = [0u8; 1];
910 loop {
911 match stream.read(&mut byte).await {
912 Ok(0) | Err(_) => return None,
913 Ok(_) => {
914 if byte[0] == b'\n' {
915 break;
916 }
917 buf.push(byte[0]);
918 if buf.len() > 4096 {
919 return None;
920 }
921 }
922 }
923 }
924 Some(
925 String::from_utf8_lossy(&buf)
926 .trim_end_matches('\r')
927 .to_string(),
928 )
929}
930
931#[cfg(unix)]
932async fn run_listener(
933 registry: Arc<Registry>,
934 share_registry: Arc<ShareRegistry>,
935 sock_path: &str,
936) {
937 let _ = std::fs::remove_file(sock_path);
938 let listener = tokio::net::UnixListener::bind(sock_path).unwrap_or_else(|e| {
939 eprintln!("blit-proxy: cannot bind to {sock_path}: {e}");
940 std::process::exit(1);
941 });
942 log!("blit-proxy: listening on {sock_path}");
943 loop {
944 match listener.accept().await {
945 Ok((stream, _)) => {
946 let registry = registry.clone();
947 let share_registry = share_registry.clone();
948 tokio::spawn(async move {
949 handle_downstream(registry, share_registry, stream).await;
950 });
951 }
952 Err(e) => log!("blit-proxy: accept error: {e}"),
953 }
954 }
955}
956
957#[cfg(windows)]
958async fn run_listener(
959 registry: Arc<Registry>,
960 share_registry: Arc<ShareRegistry>,
961 pipe_path: &str,
962 first: tokio::net::windows::named_pipe::NamedPipeServer,
963) {
964 use tokio::net::windows::named_pipe::ServerOptions;
965 log!("blit-proxy: listening on {pipe_path}");
966 let mut next_server = Some(first);
969 loop {
970 let server = match next_server.take() {
973 Some(s) => s,
974 None => match ServerOptions::new()
975 .first_pipe_instance(false)
976 .create(pipe_path)
977 {
978 Ok(s) => s,
979 Err(e) => {
980 log!("blit-proxy: create pipe instance: {e}");
981 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
982 continue;
983 }
984 },
985 };
986 let upcoming = ServerOptions::new()
989 .first_pipe_instance(false)
990 .create(pipe_path);
991 if let Err(e) = server.connect().await {
992 log!("blit-proxy: pipe connect: {e}");
993 if let Ok(u) = upcoming {
995 next_server = Some(u);
996 }
997 continue;
998 }
999 if let Ok(u) = upcoming {
1000 next_server = Some(u);
1001 }
1002 let registry = registry.clone();
1003 let share_registry = share_registry.clone();
1004 tokio::spawn(async move {
1005 handle_downstream(registry, share_registry, server).await;
1006 });
1007 }
1008}
1009
1010async fn handle_downstream<S>(
1011 registry: Arc<Registry>,
1012 share_registry: Arc<ShareRegistry>,
1013 mut downstream: S,
1014) where
1015 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1016{
1017 let line = match read_line(&mut downstream).await {
1019 Some(l) => l,
1020 None => return,
1021 };
1022 if line == "shutdown" {
1023 let _ = downstream.write_all(b"ok\n").await;
1024 log!("blit-proxy: shutdown requested, exiting");
1025 std::process::exit(0);
1026 }
1027 let uri = match line.strip_prefix("target ") {
1028 Some(u) if !u.is_empty() => u.to_string(),
1029 _ => {
1030 let _ = downstream.write_all(b"error invalid handshake\n").await;
1031 return;
1032 }
1033 };
1034
1035 if uri.starts_with("share:") {
1036 let pool = share_registry.get_or_create(&uri).await;
1038
1039 let (stream_id, upstream) = match pool.acquire().await {
1040 Ok(t) => t,
1041 Err(e) => {
1042 log!("blit-proxy: [{uri}] share upstream unavailable: {e}");
1043 let msg = format!("error {e}\n");
1044 let _ = downstream.write_all(msg.as_bytes()).await;
1045 return;
1046 }
1047 };
1048
1049 if downstream.write_all(b"ok\n").await.is_err() {
1050 pool.recycle(stream_id);
1051 return;
1052 }
1053
1054 pool.client_connected();
1055
1056 let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1057 let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1058
1059 let mut d2u =
1060 tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1061 let mut u2d =
1062 tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1063
1064 tokio::select! {
1070 _ = &mut d2u => { u2d.abort(); let _ = u2d.await; },
1071 _ = &mut u2d => { d2u.abort(); let _ = d2u.await; },
1072 }
1073
1074 pool.recycle(stream_id);
1075 pool.client_disconnected();
1076 return;
1077 }
1078
1079 let pool = registry.get_or_create(&uri).await;
1080
1081 let upstream = match pool.acquire().await {
1082 Ok(u) => u,
1083 Err(e) => {
1084 log!("blit-proxy: [{uri}] upstream unavailable: {e}");
1085 let msg = format!("error {e}\n");
1086 let _ = downstream.write_all(msg.as_bytes()).await;
1087 return;
1088 }
1089 };
1090
1091 if downstream.write_all(b"ok\n").await.is_err() {
1092 return;
1093 }
1094
1095 pool.client_connected();
1096
1097 let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1098 let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1099
1100 let mut d2u = tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1101 let mut u2d = tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1102
1103 tokio::select! {
1106 _ = &mut d2u => { u2d.abort(); let _ = u2d.await; },
1107 _ = &mut u2d => { d2u.abort(); let _ = d2u.await; },
1108 }
1109
1110 pool.client_disconnected();
1111}
1112
1113pub fn run(verbose: bool) {
1123 if verbose {
1124 VERBOSE.store(true, Ordering::Relaxed);
1125 }
1126
1127 let sock_path = proxy_socket_path();
1128
1129 let pool_size: usize = std::env::var("BLIT_PROXY_POOL")
1130 .ok()
1131 .and_then(|v| v.parse().ok())
1132 .unwrap_or(4);
1133
1134 let idle_secs: Option<u64> = std::env::var("BLIT_PROXY_IDLE")
1135 .ok()
1136 .and_then(|v| v.parse().ok());
1137
1138 tokio::runtime::Builder::new_multi_thread()
1139 .enable_all()
1140 .build()
1141 .expect("blit-proxy: tokio runtime")
1142 .block_on(async move {
1143 rustls::crypto::ring::default_provider()
1144 .install_default()
1145 .ok(); let registry = Registry::new(pool_size);
1148 let share_registry = ShareRegistry::new();
1149
1150 if let Some(idle) = idle_secs {
1152 let reg = registry.clone();
1153 let sreg = share_registry.clone();
1154 let sock = sock_path.clone();
1155 tokio::spawn(async move {
1156 loop {
1157 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1158 let last = reg
1159 .latest_activity()
1160 .await
1161 .min(sreg.latest_activity().await);
1162 if last == i64::MAX {
1163 continue;
1164 }
1165 let elapsed = now_secs().saturating_sub(last) as u64;
1166 if elapsed >= idle {
1167 log!("blit-proxy: idle for {elapsed}s (limit {idle}s), exiting");
1168 let _ = std::fs::remove_file(&sock);
1169 std::process::exit(0);
1170 }
1171 }
1172 });
1173 }
1174
1175 #[cfg(unix)]
1176 {
1177 let sock_cleanup = sock_path.clone();
1178 tokio::spawn(async move {
1179 let _ = tokio::signal::ctrl_c().await;
1180 let _ = std::fs::remove_file(&sock_cleanup);
1181 std::process::exit(0);
1182 });
1183 run_listener(registry, share_registry, &sock_path).await;
1184 }
1185 #[cfg(windows)]
1186 {
1187 use tokio::net::windows::named_pipe::ServerOptions;
1190 let first = ServerOptions::new()
1191 .first_pipe_instance(true)
1192 .create(&sock_path)
1193 .unwrap_or_else(|e| {
1194 eprintln!("blit-proxy: cannot create pipe {sock_path}: {e}");
1195 std::process::exit(1);
1196 });
1197 run_listener(registry, share_registry, &sock_path, first).await;
1200 }
1201 });
1202}
1203
1204#[cfg(test)]
1209mod tests {
1210 use super::*;
1211
1212 #[test]
1213 fn proxy_socket_path_default() {
1214 let p = proxy_socket_path();
1216 assert!(!p.is_empty());
1217 assert!(p.contains("blit-proxy"));
1218 }
1219
1220 #[test]
1221 fn parse_hex_valid() {
1222 assert_eq!(parse_hex("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
1223 assert_eq!(parse_hex(""), Some(vec![]));
1224 }
1225
1226 #[test]
1227 fn parse_hex_odd_length() {
1228 assert_eq!(parse_hex("abc"), None);
1229 }
1230
1231 #[test]
1232 fn parse_hex_invalid_char() {
1233 assert_eq!(parse_hex("zz"), None);
1234 }
1235
1236 #[test]
1237 fn extract_uri_params_no_query() {
1238 let (base, pass, cert) = extract_uri_params("wss://host:3264/");
1239 assert_eq!(base, "wss://host:3264/");
1240 assert_eq!(pass, None);
1241 assert_eq!(cert, None);
1242 }
1243
1244 #[test]
1245 fn extract_uri_params_passphrase() {
1246 let (base, pass, cert) = extract_uri_params("wss://host:3264/?passphrase=secret");
1247 assert_eq!(base, "wss://host:3264/");
1248 assert_eq!(pass, Some("secret".into()));
1249 assert_eq!(cert, None);
1250 }
1251
1252 #[test]
1253 fn extract_uri_params_both() {
1254 let (base, pass, cert) =
1255 extract_uri_params("wt://host:4433/?passphrase=abc&certHash=deadbeef");
1256 assert_eq!(base, "wt://host:4433/");
1257 assert_eq!(pass, Some("abc".into()));
1258 assert_eq!(cert, Some("deadbeef".into()));
1259 }
1260
1261 #[test]
1262 fn extract_uri_params_socket_unchanged() {
1263 let (base, pass, cert) = extract_uri_params("socket:/tmp/blit.sock");
1264 assert_eq!(base, "socket:/tmp/blit.sock");
1265 assert_eq!(pass, None);
1266 assert_eq!(cert, None);
1267 }
1268
1269 #[test]
1270 fn parse_wt_host_port_with_port() {
1271 assert_eq!(parse_wt_host_port("host:4433"), Ok(("host".into(), 4433)));
1272 }
1273
1274 #[test]
1275 fn parse_wt_host_port_default() {
1276 assert_eq!(parse_wt_host_port("host"), Ok(("host".into(), 443)));
1277 }
1278
1279 #[test]
1280 fn percent_decode_basic() {
1281 assert_eq!(percent_decode("hello%20world"), "hello world");
1282 assert_eq!(percent_decode("plain"), "plain");
1283 }
1284
1285 #[test]
1286 fn parse_share_uri_no_hub() {
1287 let (pass, hub) = parse_share_uri("myphrase");
1288 assert_eq!(pass, "myphrase");
1289 assert_eq!(
1290 hub,
1291 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1292 );
1293 }
1294
1295 #[test]
1296 fn parse_share_uri_with_hub() {
1297 let (pass, hub) = parse_share_uri("myphrase?hub=wss://custom.example.com");
1298 assert_eq!(pass, "myphrase");
1299 assert_eq!(hub, "wss://custom.example.com");
1300 }
1301
1302 #[test]
1303 fn parse_share_uri_hub_normalized() {
1304 let (pass, hub) = parse_share_uri("myphrase?hub=custom.example.com");
1305 assert_eq!(pass, "myphrase");
1306 assert_eq!(hub, "wss://custom.example.com");
1307 }
1308
1309 #[test]
1310 fn parse_share_uri_percent_encoded_passphrase() {
1311 let (pass, hub) = parse_share_uri("my%3Fphrase");
1312 assert_eq!(pass, "my?phrase");
1313 assert_eq!(
1314 hub,
1315 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1316 );
1317 }
1318}