Skip to main content

rust_web_server/proxy/
upgrade.rs

1//! Upgraded connections (WebSocket) through a proxy (a Gateway API route, or
2//! a config-file `type = "proxy"` route since 18.21.0): the request
3//! goes to the backend over its own connection; a `101` hands the client's
4//! connection to a byte relay with the backend, anything else is answered
5//! as a normal response.
6
7use std::io::{Read, Write};
8use std::net::{Shutdown, TcpStream, ToSocketAddrs};
9use std::time::Duration;
10
11use crate::core::New;
12use crate::header::Header;
13use crate::request::Request;
14use crate::response::Response;
15use crate::websocket::WebSocket;
16
17/// The most a backend's response head may take.
18const MAX_HEAD: usize = 64 * 1024;
19
20/// Whether the request asks to switch protocols (`Connection: upgrade`
21/// and an `Upgrade` header).
22pub fn is_upgrade(request: &Request) -> bool {
23    let connection = request.header("connection").map(|h| h.value.to_ascii_lowercase()).unwrap_or_default();
24    request.header("upgrade").is_some() && connection.split(',').any(|t| t.trim() == "upgrade")
25}
26
27/// The request head as sent to the backend: the client's headers (with
28/// `Host` = `host`) plus `X-Forwarded-For` and, unless the client sent one,
29/// `X-Forwarded-Host` (the client's own `Host`).
30pub fn request_head(request: &Request, client: crate::proxy::Peer<'_>, host: &str) -> Vec<u8> {
31    let mut head = format!("{} {} HTTP/1.1\r\nHost: {host}\r\n", request.method, request.request_uri);
32    let forwarded = crate::proxy::forwarded_value(request, client);
33    for header in &request.headers {
34        if header.name.eq_ignore_ascii_case("host") || header.name.eq_ignore_ascii_case("content-length") || (forwarded.is_some() && header.name.eq_ignore_ascii_case("forwarded")) {
35            continue;
36        }
37        head.push_str(&format!("{}: {}\r\n", header.name, header.value));
38    }
39    head.push_str(&format!("X-Forwarded-For: {}\r\n", client.ip));
40    if let Some(value) = &forwarded {
41        head.push_str(&format!("Forwarded: {value}\r\n"));
42    }
43    if request.header("X-Forwarded-Proto").is_none() {
44        head.push_str(&format!("X-Forwarded-Proto: {}\r\n", client.proto()));
45    }
46    if request.header("X-Forwarded-Host").is_none() {
47        if let Some(original) = request.header("host").filter(|h| !h.value.is_empty()) {
48            head.push_str(&format!("X-Forwarded-Host: {}\r\n", original.value));
49        }
50    }
51    if !request.body.is_empty() {
52        head.push_str(&format!("Content-Length: {}\r\n", request.body.len()));
53    }
54    head.push_str("\r\n");
55    head.into_bytes()
56}
57
58/// Forward an upgrade request to `ip:port`.
59pub fn forward(request: &Request, client: crate::proxy::Peer<'_>, ip: &str, port: u16, host: &str, timeout: Duration) -> Result<Response, String> {
60    let address = (ip, port).to_socket_addrs().map_err(|e| e.to_string())?.next().ok_or("no address")?;
61    let mut backend = TcpStream::connect_timeout(&address, timeout.min(Duration::from_secs(10))).map_err(|e| e.to_string())?;
62    backend.set_read_timeout(Some(timeout)).map_err(|e| e.to_string())?;
63    backend.write_all(&request_head(request, client, host)).map_err(|e| e.to_string())?;
64    backend.write_all(&request.body).map_err(|e| e.to_string())?;
65
66    let mut received = Vec::new();
67    let head_end = loop {
68        if let Some(at) = received.windows(4).position(|w| w == b"\r\n\r\n") {
69            break at + 4;
70        }
71        if received.len() > MAX_HEAD {
72            return Err("backend response head too large".to_string());
73        }
74        let mut buffer = [0u8; 8192];
75        let n = backend.read(&mut buffer).map_err(|e| e.to_string())?;
76        if n == 0 {
77            return Err("backend closed before responding".to_string());
78        }
79        received.extend_from_slice(&buffer[..n]);
80    };
81    let head = String::from_utf8_lossy(&received[..head_end]).to_string();
82    let status: i16 = head.split_whitespace().nth(1).and_then(|s| s.parse().ok()).ok_or("bad status line")?;
83    if status != 101 {
84        // Not switching: read the body it announces, and answer normally.
85        let length = head
86            .lines()
87            .find_map(|l| l.split_once(':').filter(|(n, _)| n.trim().eq_ignore_ascii_case("content-length")).and_then(|(_, v)| v.trim().parse::<usize>().ok()))
88            .unwrap_or(0);
89        while received.len() < head_end + length {
90            let mut buffer = [0u8; 8192];
91            match backend.read(&mut buffer) {
92                Ok(0) | Err(_) => break,
93                Ok(n) => received.extend_from_slice(&buffer[..n]),
94            }
95        }
96        return Ok(crate::proxy::normalize_upstream_response(Response::parse(&received)?));
97    }
98    let mut response = Response::new();
99    response.status_code = 101;
100    response.reason_phrase = "Switching Protocols".to_string();
101    response.headers = head
102        .lines()
103        .skip(1)
104        .filter_map(|l| l.split_once(':'))
105        .map(|(n, v)| Header::new(n.trim().to_string(), v.trim().to_string()))
106        .collect();
107    let leftover = received[head_end..].to_vec();
108    backend.set_read_timeout(None).map_err(|e| e.to_string())?;
109    let to_backend = backend.try_clone().map_err(|e| e.to_string())?;
110    Ok(WebSocket::upgrade_raw(response, move |client_reader, client_writer| relay(client_reader, client_writer, backend, to_backend, leftover)))
111}
112
113/// Copy bytes both ways: backend → client here until the backend closes;
114/// client → backend on a thread that ends when the client closes.
115fn relay(mut client_reader: Box<dyn Read + Send>, mut client_writer: Box<dyn Write + Send>, mut from_backend: TcpStream, mut to_backend: TcpStream, leftover: Vec<u8>) {
116    std::thread::spawn(move || {
117        let _ = std::io::copy(&mut client_reader, &mut to_backend);
118        let _ = to_backend.shutdown(Shutdown::Write);
119    });
120    if client_writer.write_all(&leftover).is_ok() {
121        let _ = std::io::copy(&mut from_backend, &mut client_writer);
122    }
123    let _ = client_writer.flush();
124    let _ = from_backend.shutdown(Shutdown::Both);
125}
126
127#[cfg(test)]
128mod tests;