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