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, 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 {
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 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 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
325struct Pool {
330 idle: Mutex<VecDeque<UpstreamConn>>,
331 active: AtomicUsize,
333 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 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
411struct 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 async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<Pool> {
430 {
432 let pools = self.pools.read().await;
433 if let Some(p) = pools.get(uri) {
434 return p.clone();
435 }
436 }
437 let mut pools = self.pools.write().await;
439 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 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 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
483async 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 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
518fn 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
555fn 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
566async 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
587fn 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 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 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 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 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 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
783use 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
897async 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 let mut next_server = Some(first);
968 loop {
969 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 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 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 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 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 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
1101pub 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(); let registry = Registry::new(pool_size);
1136 let share_registry = ShareRegistry::new(pool_size);
1137
1138 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 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 run_listener(registry, share_registry, &sock_path, first).await;
1188 }
1189 });
1190}
1191
1192#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1201 fn proxy_socket_path_default() {
1202 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}