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