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
43struct UpstreamConn {
48 reader: BoxRead,
49 writer: BoxWrite,
50}
51
52struct SharePool {
57 idle: Mutex<VecDeque<blit_webrtc_forwarder::client::Session>>,
58 passphrase: String,
59 hub: String,
60 pool_size: usize,
61 active: AtomicUsize,
62 last_activity: AtomicI64,
63}
64
65impl SharePool {
66 fn new(passphrase: String, hub: String, pool_size: usize) -> Arc<Self> {
67 Arc::new(Self {
68 idle: Mutex::new(VecDeque::new()),
69 passphrase,
70 hub,
71 pool_size,
72 active: AtomicUsize::new(0),
73 last_activity: AtomicI64::new(now_secs()),
74 })
75 }
76
77 async fn acquire(
78 &self,
79 ) -> Result<
80 (
81 blit_webrtc_forwarder::client::Session,
82 blit_webrtc_forwarder::client::ChannelHandle,
83 UpstreamConn,
84 ),
85 String,
86 > {
87 loop {
88 let session = {
89 let mut idle = self.idle.lock().await;
90 idle.pop_front()
91 };
92 match session {
93 Some(s) => match s.open_channel().await {
94 Ok((handle, stream)) => {
95 let (r, w) = tokio::io::split(stream);
96 return Ok((
97 s,
98 handle,
99 UpstreamConn {
100 reader: Box::new(r),
101 writer: Box::new(w),
102 },
103 ));
104 }
105 Err(e) => {
106 log!("blit-proxy: share: idle session stale ({e}), discarding");
107 continue;
108 }
109 },
110 None => break,
111 }
112 }
113 let (session, handle, stream) =
115 blit_webrtc_forwarder::client::Session::establish(&self.passphrase, &self.hub)
116 .await
117 .map_err(|e| format!("{e}"))?;
118 let (r, w) = tokio::io::split(stream);
119 Ok((
120 session,
121 handle,
122 UpstreamConn {
123 reader: Box::new(r),
124 writer: Box::new(w),
125 },
126 ))
127 }
128
129 async fn release(&self, session: blit_webrtc_forwarder::client::Session) {
130 let mut idle = self.idle.lock().await;
131 if idle.len() < self.pool_size {
132 idle.push_back(session);
133 }
134 }
135
136 fn client_connected(&self) {
137 self.active.fetch_add(1, Ordering::Relaxed);
138 self.last_activity.store(i64::MAX, Ordering::Relaxed);
139 }
140
141 fn client_disconnected(&self) {
142 let prev = self.active.fetch_sub(1, Ordering::Relaxed);
143 if prev == 1 {
144 self.last_activity.store(now_secs(), Ordering::Relaxed);
145 }
146 }
147
148 async fn refill_loop(self: Arc<Self>) {
149 loop {
150 let need = {
151 let idle = self.idle.lock().await;
152 self.pool_size.saturating_sub(idle.len())
153 };
154 for _ in 0..need {
155 if self.active.load(Ordering::Relaxed) > 0 {
156 break;
157 }
158 match blit_webrtc_forwarder::client::Session::establish(&self.passphrase, &self.hub)
159 .await
160 {
161 Ok((session, handle, _stream)) => {
162 session.close_channel(handle);
164 let mut idle = self.idle.lock().await;
165 if idle.len() < self.pool_size {
166 idle.push_back(session);
167 }
168 }
169 Err(e) => {
170 log!("blit-proxy: share: pre-warm failed: {e}");
171 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
172 break;
173 }
174 }
175 }
176 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
177 }
178 }
179}
180
181struct ShareRegistry {
182 pools: RwLock<HashMap<String, Arc<SharePool>>>,
183 pool_size: usize,
184}
185
186impl ShareRegistry {
187 fn new(pool_size: usize) -> Arc<Self> {
188 Arc::new(Self {
189 pools: RwLock::new(HashMap::new()),
190 pool_size,
191 })
192 }
193
194 async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<SharePool> {
195 {
196 let pools = self.pools.read().await;
197 if let Some(p) = pools.get(uri) {
198 return p.clone();
199 }
200 }
201 let mut pools = self.pools.write().await;
202 if let Some(p) = pools.get(uri) {
203 return p.clone();
204 }
205 let (passphrase, hub) = parse_share_uri(uri.strip_prefix("share:").unwrap_or(uri));
206 let pool = SharePool::new(passphrase, hub, self.pool_size);
207 pools.insert(uri.to_string(), pool.clone());
208 drop(pools);
209
210 let pool_seed = pool.clone();
211 tokio::spawn(async move {
212 pool_seed.refill_loop().await;
213 });
214
215 pool
216 }
217
218 async fn latest_activity(&self) -> i64 {
219 let pools = self.pools.read().await;
220 if pools.is_empty() {
221 return now_secs();
222 }
223 pools
224 .values()
225 .map(|p| p.last_activity.load(Ordering::Relaxed))
226 .max()
227 .unwrap_or_else(now_secs)
228 }
229}
230
231struct Pool {
236 idle: Mutex<VecDeque<UpstreamConn>>,
237 active: AtomicUsize,
239 last_activity: AtomicI64,
242 pool_size: usize,
243 upstream_uri: String,
244}
245
246impl Pool {
247 fn new(upstream_uri: String, pool_size: usize) -> Arc<Self> {
248 Arc::new(Self {
249 idle: Mutex::new(VecDeque::new()),
250 active: AtomicUsize::new(0),
251 last_activity: AtomicI64::new(now_secs()),
252 pool_size,
253 upstream_uri,
254 })
255 }
256
257 async fn acquire(&self) -> Result<UpstreamConn, String> {
258 {
259 let mut idle = self.idle.lock().await;
260 if let Some(conn) = idle.pop_front() {
261 return Ok(conn);
262 }
263 }
264 self.connect_one().await
265 }
266
267 async fn connect_one(&self) -> Result<UpstreamConn, String> {
268 connect_upstream(&self.upstream_uri).await
269 }
270
271 fn client_connected(&self) {
272 self.active.fetch_add(1, Ordering::Relaxed);
273 self.last_activity.store(i64::MAX, Ordering::Relaxed);
274 }
275
276 fn client_disconnected(&self) {
277 let prev = self.active.fetch_sub(1, Ordering::Relaxed);
278 if prev == 1 {
279 self.last_activity.store(now_secs(), Ordering::Relaxed);
280 }
281 }
282
283 async fn refill_loop(self: Arc<Self>) {
285 loop {
286 let need = {
287 let idle = self.idle.lock().await;
288 self.pool_size.saturating_sub(idle.len())
289 };
290 for _ in 0..need {
291 match self.connect_one().await {
292 Ok(conn) => {
293 self.idle.lock().await.push_back(conn);
294 }
295 Err(e) => {
296 log!(
297 "blit-proxy: [{uri}] upstream connect failed: {e}",
298 uri = self.upstream_uri
299 );
300 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
301 break;
302 }
303 }
304 }
305 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
306 }
307 }
308}
309
310fn now_secs() -> i64 {
311 std::time::SystemTime::now()
312 .duration_since(std::time::UNIX_EPOCH)
313 .unwrap_or_default()
314 .as_secs() as i64
315}
316
317struct Registry {
322 pools: RwLock<HashMap<String, Arc<Pool>>>,
323 pool_size: usize,
324}
325
326impl Registry {
327 fn new(pool_size: usize) -> Arc<Self> {
328 Arc::new(Self {
329 pools: RwLock::new(HashMap::new()),
330 pool_size,
331 })
332 }
333
334 async fn get_or_create(self: &Arc<Self>, uri: &str) -> Arc<Pool> {
336 {
338 let pools = self.pools.read().await;
339 if let Some(p) = pools.get(uri) {
340 return p.clone();
341 }
342 }
343 let mut pools = self.pools.write().await;
345 if let Some(p) = pools.get(uri) {
347 return p.clone();
348 }
349 let pool = Pool::new(uri.to_string(), self.pool_size);
350 pools.insert(uri.to_string(), pool.clone());
351 drop(pools);
352
353 let pool_seed = pool.clone();
355 tokio::spawn(async move {
356 for _ in 0..pool_seed.pool_size {
357 match pool_seed.connect_one().await {
358 Ok(conn) => pool_seed.idle.lock().await.push_back(conn),
359 Err(e) => {
360 log!(
361 "blit-proxy: [{uri}] initial connect: {e}",
362 uri = pool_seed.upstream_uri
363 );
364 break;
365 }
366 }
367 }
368 pool_seed.refill_loop().await;
369 });
370
371 pool
372 }
373
374 async fn latest_activity(&self) -> i64 {
377 let pools = self.pools.read().await;
378 if pools.is_empty() {
379 return now_secs();
380 }
381 pools
382 .values()
383 .map(|p| p.last_activity.load(Ordering::Relaxed))
384 .max()
385 .unwrap_or_else(now_secs)
386 }
387}
388
389async fn connect_upstream(uri: &str) -> Result<UpstreamConn, String> {
394 if let Some(rest) = uri.strip_prefix("share:") {
395 return connect_share(rest).await;
396 }
397
398 #[cfg(unix)]
399 if let Some(rest) = uri.strip_prefix("ssh:") {
400 return connect_ssh(rest).await;
401 }
402
403 let (base_uri, passphrase, cert_hash) = extract_uri_params(uri);
405
406 if let Some(path) = base_uri.strip_prefix("socket:") {
407 return connect_socket(path).await;
408 }
409 if let Some(addr) = base_uri.strip_prefix("tcp:") {
410 return connect_tcp(addr).await;
411 }
412 if base_uri.starts_with("ws://") || base_uri.starts_with("wss://") {
413 return connect_ws(&base_uri, passphrase.as_deref()).await;
414 }
415 if let Some(rest) = base_uri.strip_prefix("wt://") {
416 let cert_bytes = cert_hash.as_deref().and_then(parse_hex);
417 return connect_wt(rest, passphrase.as_deref(), &cert_bytes).await;
418 }
419 Err(format!(
420 "unknown upstream URI scheme in '{uri}' \
421 (expected socket:, tcp:, ws://, wss://, wt://, share:, or ssh:)"
422 ))
423}
424
425fn parse_share_uri(rest: &str) -> (String, String) {
431 let (passphrase_raw, hub) = if let Some(q_pos) = rest.find('?') {
432 let phrase = &rest[..q_pos];
433 let query = &rest[q_pos + 1..];
434 let hub = query
435 .split('&')
436 .find_map(|kv| kv.strip_prefix("hub=").map(percent_decode))
437 .unwrap_or_else(|| blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string());
438 (phrase.to_string(), hub)
439 } else {
440 (
441 rest.to_string(),
442 blit_webrtc_forwarder::DEFAULT_HUB_URL.to_string(),
443 )
444 };
445 let passphrase = percent_decode(&passphrase_raw);
446 let hub = blit_webrtc_forwarder::normalize_hub(&hub);
447 (passphrase, hub)
448}
449
450async fn connect_share(rest: &str) -> Result<UpstreamConn, String> {
451 let (passphrase, hub) = parse_share_uri(rest);
452 let stream = blit_webrtc_forwarder::client::connect(&passphrase, &hub)
453 .await
454 .map_err(|e| format!("share:{rest}: {e}"))?;
455 let (r, w) = tokio::io::split(stream);
456 Ok(UpstreamConn {
457 reader: Box::new(r),
458 writer: Box::new(w),
459 })
460}
461
462#[cfg(unix)]
468const SSH_SOCK_SEARCH: &str = r#"if [ -n "$BLIT_SOCK" ]; then S="$BLIT_SOCK"; elif [ -n "$TMPDIR" ] && [ -S "$TMPDIR/blit.sock" ]; then S="$TMPDIR/blit.sock"; elif [ -S "/tmp/blit-$(id -un).sock" ]; then S="/tmp/blit-$(id -un).sock"; elif [ -S "/run/blit/$(id -un).sock" ]; then S="/run/blit/$(id -un).sock"; elif [ -n "$XDG_RUNTIME_DIR" ] && [ -S "$XDG_RUNTIME_DIR/blit.sock" ]; then S="$XDG_RUNTIME_DIR/blit.sock"; else S=/tmp/blit.sock; fi"#;
469
470#[cfg(unix)]
472const SSH_AUTOSTART: &str = "if [ -S \"$S\" ]; then if command -v nc >/dev/null 2>&1; then nc -z -U \"$S\" 2>/dev/null || rm -f \"$S\"; elif command -v socat >/dev/null 2>&1; then socat /dev/null \"UNIX-CONNECT:$S\" 2>/dev/null || rm -f \"$S\"; fi; fi; if ! [ -S \"$S\" ]; then if command -v blit >/dev/null 2>&1; then blit server & i=0; while ! [ -S \"$S\" ] && [ $i -lt 50 ]; do sleep 0.1; i=$((i+1)); done; elif command -v blit-server >/dev/null 2>&1; then blit-server & i=0; while ! [ -S \"$S\" ] && [ $i -lt 50 ]; do sleep 0.1; i=$((i+1)); done; fi; fi";
473
474#[cfg(unix)]
479fn dq_escape(s: &str) -> String {
480 let mut out = String::with_capacity(s.len());
481 for ch in s.chars() {
482 match ch {
483 '\\' | '$' | '`' | '"' => {
484 out.push('\\');
485 out.push(ch);
486 }
487 _ => out.push(ch),
488 }
489 }
490 out
491}
492
493#[cfg(unix)]
496fn parse_ssh_target(rest: &str) -> (&str, Option<&str>) {
497 let colon_search_start = rest.find('@').map(|a| a + 1).unwrap_or(0);
498 if let Some(rel) = rest[colon_search_start..].find(':') {
499 let pos = colon_search_start + rel;
500 let path = &rest[pos + 1..];
501 if path.is_empty() {
502 (rest, None)
503 } else {
504 (&rest[..pos], Some(path))
505 }
506 } else {
507 (rest, None)
508 }
509}
510
511#[cfg(unix)]
516async fn connect_ssh(rest: &str) -> Result<UpstreamConn, String> {
517 if rest.is_empty() {
518 return Err("ssh: destination requires a host".into());
519 }
520 let (host, remote_socket) = parse_ssh_target(rest);
521 if host.starts_with('-') {
522 return Err(format!(
523 "invalid ssh host '{host}': must not start with '-'"
524 ));
525 }
526 let resolve = match remote_socket {
527 Some(path) => format!("S=\"{}\"", dq_escape(path)),
528 None => SSH_SOCK_SEARCH.to_string(),
529 };
530 let bridge = format!(
531 "sh -c '{resolve}; {SSH_AUTOSTART}; if command -v nc >/dev/null 2>&1; then exec nc -U \"$S\"; else exec socat - \"UNIX-CONNECT:$S\"; fi'"
532 );
533 let mut child = tokio::process::Command::new("ssh")
534 .arg("-T")
535 .arg("-o")
536 .arg("ControlMaster=auto")
537 .arg("-o")
538 .arg("ControlPath=/tmp/blit-ssh-%r@%h:%p")
539 .arg("-o")
540 .arg("ControlPersist=300")
541 .arg(host)
542 .arg("--")
543 .arg(&bridge)
544 .stdin(std::process::Stdio::piped())
545 .stdout(std::process::Stdio::piped())
546 .stderr(std::process::Stdio::inherit())
547 .spawn()
548 .map_err(|e| format!("ssh: {e}"))?;
549 let stdin = child
550 .stdin
551 .take()
552 .ok_or_else(|| "ssh: could not get stdin".to_string())?;
553 let stdout = child
554 .stdout
555 .take()
556 .ok_or_else(|| "ssh: could not get stdout".to_string())?;
557 tokio::spawn(async move {
559 let _ = child.wait().await;
560 });
561 Ok(UpstreamConn {
562 reader: Box::new(stdout),
563 writer: Box::new(stdin),
564 })
565}
566
567fn extract_uri_params(uri: &str) -> (String, Option<String>, Option<String>) {
570 if !uri.starts_with("ws://") && !uri.starts_with("wss://") && !uri.starts_with("wt://") {
571 return (uri.to_string(), None, None);
572 }
573 let (base, query) = match uri.find('?') {
575 Some(pos) => (&uri[..pos], Some(&uri[pos + 1..])),
576 None => (uri, None),
577 };
578 let mut passphrase = None;
579 let mut cert_hash = None;
580 if let Some(q) = query {
581 for param in q.split('&') {
582 if let Some(v) = param.strip_prefix("passphrase=") {
583 passphrase = Some(percent_decode(v));
584 } else if let Some(v) = param.strip_prefix("certHash=") {
585 cert_hash = Some(v.to_string());
586 }
587 }
588 }
589 (base.to_string(), passphrase, cert_hash)
590}
591
592fn percent_decode(s: &str) -> String {
593 let mut out = String::with_capacity(s.len());
595 let mut chars = s.chars().peekable();
596 while let Some(c) = chars.next() {
597 if c == '%' {
598 let h1 = chars.next().unwrap_or('0');
599 let h2 = chars.next().unwrap_or('0');
600 if let Ok(b) = u8::from_str_radix(&format!("{h1}{h2}"), 16) {
601 out.push(b as char);
602 continue;
603 }
604 }
605 out.push(c);
606 }
607 out
608}
609
610async fn connect_socket(path: &str) -> Result<UpstreamConn, String> {
611 #[cfg(unix)]
612 {
613 let stream = tokio::net::UnixStream::connect(path)
614 .await
615 .map_err(|e| format!("socket:{path}: {e}"))?;
616 let (r, w) = tokio::io::split(stream);
617 Ok(UpstreamConn {
618 reader: Box::new(r),
619 writer: Box::new(w),
620 })
621 }
622 #[cfg(not(unix))]
623 {
624 use tokio::net::windows::named_pipe::ClientOptions;
625 let pipe = ClientOptions::new()
626 .open(path)
627 .map_err(|e| format!("socket:{path}: {e}"))?;
628 let (r, w) = tokio::io::split(pipe);
629 Ok(UpstreamConn {
630 reader: Box::new(r),
631 writer: Box::new(w),
632 })
633 }
634}
635
636async fn connect_tcp(addr: &str) -> Result<UpstreamConn, String> {
637 let stream = tokio::net::TcpStream::connect(addr)
638 .await
639 .map_err(|e| format!("tcp:{addr}: {e}"))?;
640 let _ = stream.set_nodelay(true);
641 let (r, w) = tokio::io::split(stream);
642 Ok(UpstreamConn {
643 reader: Box::new(r),
644 writer: Box::new(w),
645 })
646}
647
648async fn connect_ws(uri: &str, passphrase: Option<&str>) -> Result<UpstreamConn, String> {
649 use futures_util::{SinkExt, StreamExt};
650 use tokio_tungstenite::tungstenite::Message;
651
652 let (mut ws, _) = tokio_tungstenite::connect_async(uri)
653 .await
654 .map_err(|e| format!("{uri}: {e}"))?;
655
656 let pass = passphrase.unwrap_or("");
657 ws.send(Message::Text(pass.into()))
658 .await
659 .map_err(|e| format!("{uri}: auth send: {e}"))?;
660 match ws.next().await {
661 Some(Ok(Message::Text(t))) if t.trim() == "ok" => {}
662 Some(Ok(Message::Text(t))) => {
663 return Err(format!("{uri}: auth rejected: {}", t.trim()));
664 }
665 other => {
666 return Err(format!("{uri}: unexpected auth response: {other:?}"));
667 }
668 }
669
670 let (ws_write, ws_read) = ws.split();
671 Ok(UpstreamConn {
672 reader: Box::new(WsFrameReader {
673 inner: ws_read,
674 buf: bytes::Bytes::new(),
675 }),
676 writer: Box::new(WsFrameWriter { inner: ws_write }),
677 })
678}
679
680async fn connect_wt(
681 rest: &str,
682 passphrase: Option<&str>,
683 cert_hash: &Option<Vec<u8>>,
684) -> Result<UpstreamConn, String> {
685 use web_transport_quinn as wt;
686
687 let (host, port) = parse_wt_host_port(rest)?;
689 let url: url::Url = format!("https://{host}:{port}/")
690 .parse()
691 .map_err(|e| format!("wt: url: {e}"))?;
692
693 let client: wt::Client = if let Some(hash) = cert_hash {
695 wt::ClientBuilder::new()
696 .with_server_certificate_hashes(vec![hash.clone()])
697 .map_err(|e| format!("wt: client build: {e}"))?
698 } else {
699 wt::ClientBuilder::new()
700 .with_system_roots()
701 .map_err(|e| format!("wt: client build: {e}"))?
702 };
703
704 let session = client
705 .connect(url)
706 .await
707 .map_err(|e| format!("wt: connect {host}:{port}: {e}"))?;
708
709 let (mut send, mut recv) = session
710 .open_bi()
711 .await
712 .map_err(|e| format!("wt: open_bi: {e}"))?;
713
714 let pass = passphrase.unwrap_or("").as_bytes();
716 let mut auth_buf = Vec::with_capacity(2 + pass.len());
717 auth_buf.extend_from_slice(&(pass.len() as u16).to_le_bytes());
718 auth_buf.extend_from_slice(pass);
719 send.write_all(&auth_buf)
720 .await
721 .map_err(|e| format!("wt: auth send: {e}"))?;
722
723 let mut resp = [0u8; 1];
724 recv.read_exact(&mut resp)
725 .await
726 .map_err(|e| format!("wt: auth recv: {e}"))?;
727 if resp[0] != 1 {
728 return Err(format!(
729 "wt: auth rejected (response byte {:#04x})",
730 resp[0]
731 ));
732 }
733
734 Ok(UpstreamConn {
735 reader: Box::new(recv),
736 writer: Box::new(send),
737 })
738}
739
740fn parse_wt_host_port(rest: &str) -> Result<(String, u16), String> {
741 let without_path = rest.split('/').next().unwrap_or(rest);
742 if let Some(colon) = without_path.rfind(':') {
743 let host = without_path[..colon].to_string();
744 let port: u16 = without_path[colon + 1..]
745 .parse()
746 .map_err(|_| format!("wt: invalid port in '{rest}'"))?;
747 Ok((host, port))
748 } else {
749 Ok((without_path.to_string(), 443))
750 }
751}
752
753fn parse_hex(s: &str) -> Option<Vec<u8>> {
754 if !s.len().is_multiple_of(2) {
755 return None;
756 }
757 (0..s.len())
758 .step_by(2)
759 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
760 .collect()
761}
762
763use futures_util::{SinkExt, StreamExt};
768use tokio_tungstenite::tungstenite::Message;
769
770type WsSink = futures_util::stream::SplitSink<
771 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
772 Message,
773>;
774type WsStream = futures_util::stream::SplitStream<
775 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
776>;
777
778struct WsFrameReader {
779 inner: WsStream,
780 buf: bytes::Bytes,
781}
782
783impl AsyncRead for WsFrameReader {
784 fn poll_read(
785 mut self: std::pin::Pin<&mut Self>,
786 cx: &mut std::task::Context<'_>,
787 out: &mut tokio::io::ReadBuf<'_>,
788 ) -> std::task::Poll<std::io::Result<()>> {
789 loop {
790 if !self.buf.is_empty() {
791 let n = out.remaining().min(self.buf.len());
792 out.put_slice(&self.buf[..n]);
793 self.buf = self.buf.slice(n..);
794 return std::task::Poll::Ready(Ok(()));
795 }
796 match self.inner.poll_next_unpin(cx) {
797 std::task::Poll::Pending => return std::task::Poll::Pending,
798 std::task::Poll::Ready(None) => return std::task::Poll::Ready(Ok(())),
799 std::task::Poll::Ready(Some(Err(e))) => {
800 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
801 }
802 std::task::Poll::Ready(Some(Ok(msg))) => {
803 let data = match msg {
804 Message::Binary(d) => d,
805 Message::Close(_) => return std::task::Poll::Ready(Ok(())),
806 _ => continue,
807 };
808 let len = data.len() as u32;
809 let mut framed = Vec::with_capacity(4 + data.len());
810 framed.extend_from_slice(&len.to_le_bytes());
811 framed.extend_from_slice(&data);
812 self.buf = bytes::Bytes::from(framed);
813 }
814 }
815 }
816 }
817}
818
819struct WsFrameWriter {
820 inner: WsSink,
821}
822
823impl AsyncWrite for WsFrameWriter {
824 fn poll_write(
825 mut self: std::pin::Pin<&mut Self>,
826 cx: &mut std::task::Context<'_>,
827 buf: &[u8],
828 ) -> std::task::Poll<std::io::Result<usize>> {
829 if buf.len() < 4 {
830 return std::task::Poll::Ready(Err(std::io::Error::new(
831 std::io::ErrorKind::InvalidData,
832 "ws frame writer: buffer too small for length prefix",
833 )));
834 }
835 let len = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize;
836 if buf.len() < 4 + len {
837 return std::task::Poll::Ready(Err(std::io::Error::new(
838 std::io::ErrorKind::InvalidData,
839 format!(
840 "ws frame writer: buffer ({}) too small for frame payload ({})",
841 buf.len(),
842 4 + len
843 ),
844 )));
845 }
846 let payload = buf[4..4 + len].to_vec();
847 match self.inner.poll_ready_unpin(cx) {
848 std::task::Poll::Pending => return std::task::Poll::Pending,
849 std::task::Poll::Ready(Err(e)) => {
850 return std::task::Poll::Ready(Err(std::io::Error::other(e)));
851 }
852 std::task::Poll::Ready(Ok(())) => {}
853 }
854 match self.inner.start_send_unpin(Message::Binary(payload.into())) {
855 Err(e) => std::task::Poll::Ready(Err(std::io::Error::other(e))),
856 Ok(()) => std::task::Poll::Ready(Ok(4 + len)),
857 }
858 }
859 fn poll_flush(
860 mut self: std::pin::Pin<&mut Self>,
861 cx: &mut std::task::Context<'_>,
862 ) -> std::task::Poll<std::io::Result<()>> {
863 self.inner
864 .poll_flush_unpin(cx)
865 .map_err(std::io::Error::other)
866 }
867 fn poll_shutdown(
868 mut self: std::pin::Pin<&mut Self>,
869 cx: &mut std::task::Context<'_>,
870 ) -> std::task::Poll<std::io::Result<()>> {
871 self.inner
872 .poll_close_unpin(cx)
873 .map_err(std::io::Error::other)
874 }
875}
876
877async fn read_line<S>(stream: &mut S) -> Option<String>
883where
884 S: tokio::io::AsyncRead + Unpin,
885{
886 use tokio::io::AsyncReadExt;
887 let mut buf = Vec::with_capacity(128);
888 let mut byte = [0u8; 1];
889 loop {
890 match stream.read(&mut byte).await {
891 Ok(0) | Err(_) => return None,
892 Ok(_) => {
893 if byte[0] == b'\n' {
894 break;
895 }
896 buf.push(byte[0]);
897 if buf.len() > 4096 {
898 return None;
899 }
900 }
901 }
902 }
903 Some(
904 String::from_utf8_lossy(&buf)
905 .trim_end_matches('\r')
906 .to_string(),
907 )
908}
909
910#[cfg(unix)]
911async fn run_listener(
912 registry: Arc<Registry>,
913 share_registry: Arc<ShareRegistry>,
914 sock_path: &str,
915) {
916 let _ = std::fs::remove_file(sock_path);
917 let listener = tokio::net::UnixListener::bind(sock_path).unwrap_or_else(|e| {
918 eprintln!("blit-proxy: cannot bind to {sock_path}: {e}");
919 std::process::exit(1);
920 });
921 log!("blit-proxy: listening on {sock_path}");
922 loop {
923 match listener.accept().await {
924 Ok((stream, _)) => {
925 let registry = registry.clone();
926 let share_registry = share_registry.clone();
927 tokio::spawn(async move {
928 handle_downstream(registry, share_registry, stream).await;
929 });
930 }
931 Err(e) => log!("blit-proxy: accept error: {e}"),
932 }
933 }
934}
935
936#[cfg(windows)]
937async fn run_listener(
938 registry: Arc<Registry>,
939 share_registry: Arc<ShareRegistry>,
940 pipe_path: &str,
941 first: tokio::net::windows::named_pipe::NamedPipeServer,
942) {
943 use tokio::net::windows::named_pipe::ServerOptions;
944 log!("blit-proxy: listening on {pipe_path}");
945 let mut next_server = Some(first);
948 loop {
949 let server = match next_server.take() {
952 Some(s) => s,
953 None => match ServerOptions::new()
954 .first_pipe_instance(false)
955 .create(pipe_path)
956 {
957 Ok(s) => s,
958 Err(e) => {
959 log!("blit-proxy: create pipe instance: {e}");
960 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
961 continue;
962 }
963 },
964 };
965 let upcoming = ServerOptions::new()
968 .first_pipe_instance(false)
969 .create(pipe_path);
970 if let Err(e) = server.connect().await {
971 log!("blit-proxy: pipe connect: {e}");
972 if let Ok(u) = upcoming {
974 next_server = Some(u);
975 }
976 continue;
977 }
978 if let Ok(u) = upcoming {
979 next_server = Some(u);
980 }
981 let registry = registry.clone();
982 let share_registry = share_registry.clone();
983 tokio::spawn(async move {
984 handle_downstream(registry, share_registry, server).await;
985 });
986 }
987}
988
989async fn handle_downstream<S>(
990 registry: Arc<Registry>,
991 share_registry: Arc<ShareRegistry>,
992 mut downstream: S,
993) where
994 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
995{
996 let line = match read_line(&mut downstream).await {
998 Some(l) => l,
999 None => return,
1000 };
1001 if line == "shutdown" {
1002 let _ = downstream.write_all(b"ok\n").await;
1003 log!("blit-proxy: shutdown requested, exiting");
1004 std::process::exit(0);
1005 }
1006 let uri = match line.strip_prefix("target ") {
1007 Some(u) if !u.is_empty() => u.to_string(),
1008 _ => {
1009 let _ = downstream.write_all(b"error invalid handshake\n").await;
1010 return;
1011 }
1012 };
1013
1014 if uri.starts_with("share:") {
1015 let pool = share_registry.get_or_create(&uri).await;
1017
1018 let (session, channel_handle, upstream) = match pool.acquire().await {
1019 Ok(t) => t,
1020 Err(e) => {
1021 log!("blit-proxy: [{uri}] share upstream unavailable: {e}");
1022 let msg = format!("error {e}\n");
1023 let _ = downstream.write_all(msg.as_bytes()).await;
1024 return;
1025 }
1026 };
1027
1028 if downstream.write_all(b"ok\n").await.is_err() {
1029 session.close_channel(channel_handle);
1030 return;
1031 }
1032
1033 pool.client_connected();
1034
1035 let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1036 let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1037
1038 let mut d2u =
1039 tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1040 let mut u2d =
1041 tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1042
1043 tokio::select! { _ = &mut d2u => { u2d.abort(); }, _ = &mut u2d => { d2u.abort(); } }
1044
1045 session.close_channel(channel_handle);
1047 pool.release(session).await;
1048 pool.client_disconnected();
1049 return;
1050 }
1051
1052 let pool = registry.get_or_create(&uri).await;
1053
1054 let upstream = match pool.acquire().await {
1055 Ok(u) => u,
1056 Err(e) => {
1057 log!("blit-proxy: [{uri}] upstream unavailable: {e}");
1058 let msg = format!("error {e}\n");
1059 let _ = downstream.write_all(msg.as_bytes()).await;
1060 return;
1061 }
1062 };
1063
1064 if downstream.write_all(b"ok\n").await.is_err() {
1065 return;
1066 }
1067
1068 pool.client_connected();
1069
1070 let (mut ds_read, mut ds_write) = tokio::io::split(downstream);
1071 let (mut us_read, mut us_write) = (upstream.reader, upstream.writer);
1072
1073 let mut d2u = tokio::spawn(async move { tokio::io::copy(&mut ds_read, &mut us_write).await });
1074 let mut u2d = tokio::spawn(async move { tokio::io::copy(&mut us_read, &mut ds_write).await });
1075
1076 tokio::select! { _ = &mut d2u => { u2d.abort(); }, _ = &mut u2d => { d2u.abort(); } }
1077
1078 pool.client_disconnected();
1079}
1080
1081pub fn run(verbose: bool) {
1091 if verbose {
1092 VERBOSE.store(true, Ordering::Relaxed);
1093 }
1094
1095 let sock_path = proxy_socket_path();
1096
1097 let pool_size: usize = std::env::var("BLIT_PROXY_POOL")
1098 .ok()
1099 .and_then(|v| v.parse().ok())
1100 .unwrap_or(4);
1101
1102 let idle_secs: Option<u64> = std::env::var("BLIT_PROXY_IDLE")
1103 .ok()
1104 .and_then(|v| v.parse().ok());
1105
1106 tokio::runtime::Builder::new_multi_thread()
1107 .enable_all()
1108 .build()
1109 .expect("blit-proxy: tokio runtime")
1110 .block_on(async move {
1111 rustls::crypto::ring::default_provider()
1112 .install_default()
1113 .ok(); let registry = Registry::new(pool_size);
1116 let share_registry = ShareRegistry::new(pool_size);
1117
1118 if let Some(idle) = idle_secs {
1120 let reg = registry.clone();
1121 let sreg = share_registry.clone();
1122 let sock = sock_path.clone();
1123 tokio::spawn(async move {
1124 loop {
1125 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1126 let last = reg
1127 .latest_activity()
1128 .await
1129 .min(sreg.latest_activity().await);
1130 if last == i64::MAX {
1131 continue;
1132 }
1133 let elapsed = now_secs().saturating_sub(last) as u64;
1134 if elapsed >= idle {
1135 log!("blit-proxy: idle for {elapsed}s (limit {idle}s), exiting");
1136 let _ = std::fs::remove_file(&sock);
1137 std::process::exit(0);
1138 }
1139 }
1140 });
1141 }
1142
1143 #[cfg(unix)]
1144 {
1145 let sock_cleanup = sock_path.clone();
1146 tokio::spawn(async move {
1147 let _ = tokio::signal::ctrl_c().await;
1148 let _ = std::fs::remove_file(&sock_cleanup);
1149 std::process::exit(0);
1150 });
1151 run_listener(registry, share_registry, &sock_path).await;
1152 }
1153 #[cfg(windows)]
1154 {
1155 use tokio::net::windows::named_pipe::ServerOptions;
1158 let first = ServerOptions::new()
1159 .first_pipe_instance(true)
1160 .create(&sock_path)
1161 .unwrap_or_else(|e| {
1162 eprintln!("blit-proxy: cannot create pipe {sock_path}: {e}");
1163 std::process::exit(1);
1164 });
1165 run_listener(registry, share_registry, &sock_path, first).await;
1168 }
1169 });
1170}
1171
1172#[cfg(test)]
1177mod tests {
1178 use super::*;
1179
1180 #[test]
1181 fn proxy_socket_path_default() {
1182 let p = proxy_socket_path();
1184 assert!(!p.is_empty());
1185 assert!(p.contains("blit-proxy"));
1186 }
1187
1188 #[test]
1189 fn parse_hex_valid() {
1190 assert_eq!(parse_hex("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
1191 assert_eq!(parse_hex(""), Some(vec![]));
1192 }
1193
1194 #[test]
1195 fn parse_hex_odd_length() {
1196 assert_eq!(parse_hex("abc"), None);
1197 }
1198
1199 #[test]
1200 fn parse_hex_invalid_char() {
1201 assert_eq!(parse_hex("zz"), None);
1202 }
1203
1204 #[test]
1205 fn extract_uri_params_no_query() {
1206 let (base, pass, cert) = extract_uri_params("wss://host:3264/");
1207 assert_eq!(base, "wss://host:3264/");
1208 assert_eq!(pass, None);
1209 assert_eq!(cert, None);
1210 }
1211
1212 #[test]
1213 fn extract_uri_params_passphrase() {
1214 let (base, pass, cert) = extract_uri_params("wss://host:3264/?passphrase=secret");
1215 assert_eq!(base, "wss://host:3264/");
1216 assert_eq!(pass, Some("secret".into()));
1217 assert_eq!(cert, None);
1218 }
1219
1220 #[test]
1221 fn extract_uri_params_both() {
1222 let (base, pass, cert) =
1223 extract_uri_params("wt://host:4433/?passphrase=abc&certHash=deadbeef");
1224 assert_eq!(base, "wt://host:4433/");
1225 assert_eq!(pass, Some("abc".into()));
1226 assert_eq!(cert, Some("deadbeef".into()));
1227 }
1228
1229 #[test]
1230 fn extract_uri_params_socket_unchanged() {
1231 let (base, pass, cert) = extract_uri_params("socket:/tmp/blit.sock");
1232 assert_eq!(base, "socket:/tmp/blit.sock");
1233 assert_eq!(pass, None);
1234 assert_eq!(cert, None);
1235 }
1236
1237 #[test]
1238 fn parse_wt_host_port_with_port() {
1239 assert_eq!(parse_wt_host_port("host:4433"), Ok(("host".into(), 4433)));
1240 }
1241
1242 #[test]
1243 fn parse_wt_host_port_default() {
1244 assert_eq!(parse_wt_host_port("host"), Ok(("host".into(), 443)));
1245 }
1246
1247 #[test]
1248 fn percent_decode_basic() {
1249 assert_eq!(percent_decode("hello%20world"), "hello world");
1250 assert_eq!(percent_decode("plain"), "plain");
1251 }
1252
1253 #[test]
1254 fn parse_share_uri_no_hub() {
1255 let (pass, hub) = parse_share_uri("myphrase");
1256 assert_eq!(pass, "myphrase");
1257 assert_eq!(
1258 hub,
1259 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1260 );
1261 }
1262
1263 #[test]
1264 fn parse_share_uri_with_hub() {
1265 let (pass, hub) = parse_share_uri("myphrase?hub=wss://custom.example.com");
1266 assert_eq!(pass, "myphrase");
1267 assert_eq!(hub, "wss://custom.example.com");
1268 }
1269
1270 #[test]
1271 fn parse_share_uri_hub_normalized() {
1272 let (pass, hub) = parse_share_uri("myphrase?hub=custom.example.com");
1273 assert_eq!(pass, "myphrase");
1274 assert_eq!(hub, "wss://custom.example.com");
1275 }
1276
1277 #[test]
1278 fn parse_share_uri_percent_encoded_passphrase() {
1279 let (pass, hub) = parse_share_uri("my%3Fphrase");
1280 assert_eq!(pass, "my?phrase");
1281 assert_eq!(
1282 hub,
1283 blit_webrtc_forwarder::normalize_hub(blit_webrtc_forwarder::DEFAULT_HUB_URL)
1284 );
1285 }
1286}