1use std::io::{Read, Write};
48use std::net::{TcpListener, TcpStream, ToSocketAddrs};
49use std::sync::{
50 Arc, RwLock,
51 atomic::{AtomicUsize, Ordering},
52};
53use std::time::Duration;
54
55use crate::request::Request;
56use crate::websocket::WebSocket;
57
58pub struct WsProxy {
71 all_backends: Vec<String>,
74 live: Arc<RwLock<Vec<String>>>,
80 counter: Arc<AtomicUsize>,
81 connect_timeout: Duration,
82 read_timeout: Duration,
83}
84
85impl WsProxy {
86 pub fn new<I, S>(backends: I) -> Self
95 where
96 I: IntoIterator<Item = S>,
97 S: Into<String>,
98 {
99 let all_backends: Vec<String> = backends
100 .into_iter()
101 .map(|b| b.into())
102 .filter(|b| WsBackend::parse(b).is_some())
103 .collect();
104 let live = Arc::new(RwLock::new(all_backends.clone()));
105 WsProxy {
106 all_backends,
107 live,
108 counter: Arc::new(AtomicUsize::new(0)),
109 connect_timeout: Duration::from_secs(5),
110 read_timeout: Duration::from_secs(30),
111 }
112 }
113
114 pub fn with_live_backends(all_backends: Vec<String>, live: Arc<RwLock<Vec<String>>>) -> Self {
148 WsProxy {
149 all_backends,
150 live,
151 counter: Arc::new(AtomicUsize::new(0)),
152 connect_timeout: Duration::from_secs(5),
153 read_timeout: Duration::from_secs(30),
154 }
155 }
156
157 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
159 self.connect_timeout = Duration::from_millis(ms);
160 self
161 }
162
163 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
168 self.read_timeout = Duration::from_millis(ms);
169 self
170 }
171
172 pub fn bind(self, addr: &str) -> Result<(), String> {
174 if self.all_backends.is_empty() {
175 return Err("WsProxy: no backends configured".to_string());
176 }
177 let listener = TcpListener::bind(addr)
178 .map_err(|e| format!("WsProxy: bind on {} failed: {}", addr, e))?;
179 println!("WsProxy: listening on {}", addr);
180 let proxy = Arc::new(self);
181 for incoming in listener.incoming() {
182 let client = match incoming {
183 Ok(s) => s,
184 Err(e) => {
185 eprintln!("WsProxy: accept error: {}", e);
186 continue;
187 }
188 };
189 let p = Arc::clone(&proxy);
190 std::thread::spawn(move || {
191 if let Err(e) = p.handle(client) {
192 eprintln!("WsProxy: {}", e);
193 }
194 });
195 }
196 Ok(())
197 }
198
199 fn pick_backend(&self) -> Option<WsBackend> {
204 let live = self.live.read().unwrap_or_else(|e| e.into_inner());
205 if live.is_empty() {
206 return None;
207 }
208 let i = self.counter.fetch_add(1, Ordering::Relaxed) % live.len();
209 WsBackend::parse(&live[i])
210 }
211
212 fn handle(&self, mut client: TcpStream) -> Result<(), String> {
213 client.set_read_timeout(Some(self.read_timeout)).ok();
214
215 let mut buf = vec![0u8; 8192];
217 let n = client.read(&mut buf).map_err(|e| e.to_string())?;
218 if n == 0 {
219 return Ok(());
220 }
221
222 let request = Request::parse(&buf[..n])
223 .map_err(|e| format!("WsProxy: invalid HTTP request: {}", e))?;
224
225 if !WebSocket::is_upgrade_request(&request) {
226 let _ = client.write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n");
227 return Err(format!(
228 "WsProxy: not a WebSocket upgrade — method={}, uri={}",
229 request.method, request.request_uri
230 ));
231 }
232
233 let backend = match self.pick_backend() {
234 Some(b) => b,
235 None => {
236 let _ = client.write_all(
237 b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
238 );
239 return Err("WsProxy: no healthy backend available".to_string());
240 }
241 };
242 let addr_str = &backend.addr;
243 let sock_addr = addr_str
244 .to_socket_addrs()
245 .map_err(|e| format!("WsProxy: DNS lookup for {} failed: {}", addr_str, e))?
246 .next()
247 .ok_or_else(|| format!("WsProxy: no address for {}", addr_str))?;
248
249 let tcp = TcpStream::connect_timeout(&sock_addr, self.connect_timeout)
250 .map_err(|e| format!("WsProxy: connect to {} failed: {}", addr_str, e))?;
251
252 let upgrade_req = build_upgrade_request(&request, &backend.host);
254
255 if backend.tls {
256 self.handle_tls(client, tcp, &request, &backend.host, upgrade_req, addr_str)
257 } else {
258 handle_plain(client, tcp, &request, upgrade_req, addr_str)
259 }
260 }
261
262 fn handle_tls(
263 &self,
264 mut client: TcpStream,
265 tcp: TcpStream,
266 request: &Request,
267 host: &str,
268 upgrade_req: Vec<u8>,
269 addr_str: &str,
270 ) -> Result<(), String> {
271 #[cfg(any(feature = "http-client", feature = "http2"))]
272 {
273 use rustls::pki_types::ServerName;
274 use std::sync::Arc;
275
276 let root_store = rustls::RootCertStore::from_iter(
277 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
278 );
279 let config = Arc::new(
280 crate::tls_client_builder()
281 .with_root_certificates(root_store)
282 .with_no_client_auth(),
283 );
284 let server_name = ServerName::try_from(host)
285 .map_err(|e| format!("WsProxy: invalid hostname '{}': {}", host, e))?
286 .to_owned();
287 let conn = rustls::ClientConnection::new(config, server_name)
288 .map_err(|e| format!("WsProxy: TLS init failed: {}", e))?;
289 let mut tls = rustls::StreamOwned::new(conn, tcp);
290
291 tls.write_all(&upgrade_req)
293 .map_err(|e| format!("WsProxy: write upgrade to {} failed: {}", addr_str, e))?;
294
295 let mut resp_buf = vec![0u8; 4096];
297 let m = tls
298 .read(&mut resp_buf)
299 .map_err(|e| format!("WsProxy: read 101 from {} failed: {}", addr_str, e))?;
300 let preview = &resp_buf[..m.min(20)];
301 if !preview.starts_with(b"HTTP/1.1 101") && !preview.starts_with(b"HTTP/1.0 101") {
302 return Err(format!(
303 "WsProxy: backend {} did not send 101 (got {:?})",
304 addr_str,
305 std::str::from_utf8(&resp_buf[..m.min(80)]).unwrap_or("?")
306 ));
307 }
308
309 let response_101 = WebSocket::handshake_response(request)?;
311 let raw_101 = format_response_head(&response_101);
312 client
313 .write_all(&raw_101)
314 .map_err(|e| format!("WsProxy: write 101 to client failed: {}", e))?;
315
316 tls.sock.set_read_timeout(Some(Duration::from_millis(5))).ok();
319 client.set_read_timeout(Some(Duration::from_millis(5))).ok();
320 relay_tls(client, tls);
321 Ok(())
322 }
323
324 #[cfg(not(any(feature = "http-client", feature = "http2")))]
325 {
326 let _ = (tcp, request, host, upgrade_req, addr_str);
327 let _ = client.write_all(
328 b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n",
329 );
330 Err("WsProxy: wss:// upstreams require the http-client or http2 Cargo feature".to_string())
331 }
332 }
333}
334
335fn handle_plain(
337 mut client: TcpStream,
338 mut backend: TcpStream,
339 request: &Request,
340 upgrade_req: Vec<u8>,
341 addr_str: &str,
342) -> Result<(), String> {
343 backend
344 .write_all(&upgrade_req)
345 .map_err(|e| format!("WsProxy: write upgrade to {} failed: {}", addr_str, e))?;
346
347 let mut resp_buf = vec![0u8; 4096];
348 let m = backend
349 .read(&mut resp_buf)
350 .map_err(|e| format!("WsProxy: read 101 from {} failed: {}", addr_str, e))?;
351 let preview = &resp_buf[..m.min(20)];
352 if !preview.starts_with(b"HTTP/1.1 101") && !preview.starts_with(b"HTTP/1.0 101") {
353 return Err(format!(
354 "WsProxy: backend {} did not send 101 (got {:?})",
355 addr_str,
356 std::str::from_utf8(&resp_buf[..m.min(80)]).unwrap_or("?")
357 ));
358 }
359
360 let response_101 = WebSocket::handshake_response(request)?;
361 let raw_101 = format_response_head(&response_101);
362 client
363 .write_all(&raw_101)
364 .map_err(|e| format!("WsProxy: write 101 to client failed: {}", e))?;
365
366 let mut client_r = client.try_clone().map_err(|e| e.to_string())?;
368 let mut backend_r = backend.try_clone().map_err(|e| e.to_string())?;
369 let mut client_w = client;
370 let mut backend_w = backend;
371
372 let t1 = std::thread::spawn(move || {
373 std::io::copy(&mut client_r, &mut backend_w).ok();
374 let _ = backend_w.shutdown(std::net::Shutdown::Write);
375 });
376 let t2 = std::thread::spawn(move || {
377 std::io::copy(&mut backend_r, &mut client_w).ok();
378 let _ = client_w.shutdown(std::net::Shutdown::Write);
379 });
380
381 let _ = t1.join();
382 let _ = t2.join();
383 Ok(())
384}
385
386#[cfg(any(feature = "http-client", feature = "http2"))]
395fn relay_tls(
396 mut client: TcpStream,
397 mut backend: rustls::StreamOwned<rustls::ClientConnection, TcpStream>,
398) {
399 use std::io::ErrorKind::{TimedOut, WouldBlock};
400 let mut buf = [0u8; 8192];
401
402 loop {
403 let mut active = false;
404
405 let cn = match client.read(&mut buf) {
407 Ok(0) => break, Ok(n) => n,
409 Err(ref e) if e.kind() == TimedOut || e.kind() == WouldBlock => 0,
410 Err(_) => break,
411 };
412 if cn > 0 {
413 if backend.write_all(&buf[..cn]).is_err() {
414 break;
415 }
416 active = true;
417 }
418
419 let bn = match backend.read(&mut buf) {
421 Ok(0) => break, Ok(n) => n,
423 Err(ref e) if e.kind() == TimedOut || e.kind() == WouldBlock => 0,
424 Err(_) => break,
425 };
426 if bn > 0 {
427 if client.write_all(&buf[..bn]).is_err() {
428 break;
429 }
430 active = true;
431 }
432
433 if !active {
434 std::thread::sleep(Duration::from_millis(1));
435 }
436 }
437}
438
439fn build_upgrade_request(request: &Request, backend_host: &str) -> Vec<u8> {
442 let mut req = format!(
443 "{} {} HTTP/1.1\r\nHost: {}\r\n",
444 request.method, request.request_uri, backend_host
445 );
446 for header in &request.headers {
447 if header.name.to_lowercase() == "host" {
448 continue;
449 }
450 req.push_str(&format!("{}: {}\r\n", header.name, header.value));
451 }
452 req.push_str("\r\n");
453 req.into_bytes()
454}
455
456fn format_response_head(response: &crate::response::Response) -> Vec<u8> {
457 let mut out = format!(
458 "HTTP/1.1 {} {}\r\n",
459 response.status_code, response.reason_phrase
460 )
461 .into_bytes();
462 for h in &response.headers {
463 out.extend_from_slice(h.name.as_bytes());
464 out.extend_from_slice(b": ");
465 out.extend_from_slice(h.value.as_bytes());
466 out.extend_from_slice(b"\r\n");
467 }
468 out.extend_from_slice(b"\r\n");
469 out
470}
471
472struct WsBackend {
475 addr: String,
477 host: String,
479 tls: bool,
481}
482
483impl WsBackend {
484 fn parse(url: &str) -> Option<Self> {
485 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("wss://") {
486 (r, true, 443u16)
487 } else if let Some(r) = url.strip_prefix("ws://") {
488 (r, false, 80u16)
489 } else {
490 (url, false, 80u16)
491 };
492
493 let host_port = rest.split('/').next().unwrap_or(rest);
495
496 let (host, port) = if let Some(colon) = host_port.rfind(':') {
497 let port_str = &host_port[colon + 1..];
498 if let Ok(p) = port_str.parse::<u16>() {
499 (host_port[..colon].to_string(), p)
500 } else {
501 (host_port.to_string(), default_port)
502 }
503 } else {
504 (host_port.to_string(), default_port)
505 };
506
507 if host.is_empty() {
508 return None;
509 }
510
511 Some(WsBackend {
512 addr: format!("{}:{}", host, port),
513 host,
514 tls,
515 })
516 }
517}
518
519#[cfg(test)]
522mod backend_parse_tests {
523 use super::WsBackend;
524
525 fn parse(url: &str) -> Option<(String, String, bool)> {
526 WsBackend::parse(url).map(|b| (b.addr, b.host, b.tls))
527 }
528
529 #[test]
530 fn bare_host_port() {
531 assert_eq!(
532 Some(("chat:9000".into(), "chat".into(), false)),
533 parse("chat:9000")
534 );
535 }
536
537 #[test]
538 fn ws_scheme_plain() {
539 assert_eq!(
540 Some(("backend:3000".into(), "backend".into(), false)),
541 parse("ws://backend:3000")
542 );
543 }
544
545 #[test]
546 fn ws_scheme_default_port() {
547 assert_eq!(
548 Some(("api.example.com:80".into(), "api.example.com".into(), false)),
549 parse("ws://api.example.com")
550 );
551 }
552
553 #[test]
554 fn wss_scheme_sets_tls() {
555 assert_eq!(
556 Some(("secure.example.com:443".into(), "secure.example.com".into(), true)),
557 parse("wss://secure.example.com")
558 );
559 }
560
561 #[test]
562 fn wss_scheme_explicit_port() {
563 assert_eq!(
564 Some(("secure.example.com:8443".into(), "secure.example.com".into(), true)),
565 parse("wss://secure.example.com:8443")
566 );
567 }
568
569 #[test]
570 fn wss_default_port_is_443() {
571 let b = WsBackend::parse("wss://api.example.com").unwrap();
572 assert_eq!("api.example.com:443", b.addr);
573 assert_eq!("api.example.com", b.host);
574 assert!(b.tls);
575 }
576
577 #[test]
578 fn ws_default_port_is_80() {
579 let b = WsBackend::parse("ws://api.example.com").unwrap();
580 assert_eq!("api.example.com:80", b.addr);
581 assert!(!b.tls);
582 }
583
584 #[test]
585 fn empty_host_returns_none() {
586 assert_eq!(None, parse("wss://"));
587 }
588
589 #[test]
590 fn bare_host_no_port_defaults_to_80() {
591 assert_eq!(
592 Some(("myhost:80".into(), "myhost".into(), false)),
593 parse("myhost")
594 );
595 }
596
597 #[test]
598 fn path_component_is_ignored() {
599 let b = WsBackend::parse("ws://backend:9000/ws").unwrap();
601 assert_eq!("backend:9000", b.addr);
602 assert_eq!("backend", b.host);
603 }
604}
605
606#[cfg(test)]
609mod live_backends_tests {
610 use super::WsProxy;
611 use std::sync::{Arc, RwLock};
612
613 #[test]
614 fn new_picks_backends_in_round_robin_order() {
615 let proxy = WsProxy::new(["ws://a:1", "ws://b:1", "ws://c:1"]);
616 let picked: Vec<String> = (0..6)
617 .map(|_| proxy.pick_backend().unwrap().addr)
618 .collect();
619 assert_eq!(
620 vec!["a:1", "b:1", "c:1", "a:1", "b:1", "c:1"],
621 picked
622 );
623 }
624
625 #[test]
626 fn new_filters_out_unparseable_backends() {
627 let proxy = WsProxy::new(["ws://good:1", "wss://"]);
629 assert_eq!(vec!["ws://good:1".to_string()], proxy.all_backends);
630 }
631
632 #[test]
633 fn pick_backend_returns_none_when_live_list_is_empty() {
634 let proxy = WsProxy::with_live_backends(
635 vec!["ws://a:1".to_string()],
636 Arc::new(RwLock::new(vec![])),
637 );
638 assert!(proxy.pick_backend().is_none(), "no live backends means no pick");
639 }
640
641 #[test]
642 fn pick_backend_reflects_live_list_updates() {
643 let live = Arc::new(RwLock::new(vec!["ws://a:1".to_string(), "ws://b:1".to_string()]));
646 let proxy = WsProxy::with_live_backends(
647 vec!["ws://a:1".to_string(), "ws://b:1".to_string()],
648 Arc::clone(&live),
649 );
650
651 assert!(proxy.pick_backend().is_some());
652
653 *live.write().unwrap() = vec!["ws://b:1".to_string()];
655 for _ in 0..4 {
656 assert_eq!("b:1", proxy.pick_backend().unwrap().addr);
657 }
658
659 live.write().unwrap().clear();
661 assert!(proxy.pick_backend().is_none());
662
663 *live.write().unwrap() = vec!["ws://a:1".to_string()];
665 assert_eq!("a:1", proxy.pick_backend().unwrap().addr);
666 }
667}