Skip to main content

reverse_proxy/
reverse_proxy.rs

1//! Run with `cargo run --all-features --example reverse_proxy` command.
2//!
3//! Terminates TLS and forwards every request to an HTTP upstream. Set the
4//! `UPSTREAM` environment variable to change the target, for example:
5//!
6//! ```text
7//! UPSTREAM=http://127.0.0.1:8080 cargo run --all-features --example reverse_proxy
8//! ```
9//!
10//! To connect through a browser, navigate to "https://localhost:3443".
11//!
12//! This is intentionally minimal: a production proxy should also handle
13//! hop-by-hop headers, timeouts, request size limits, and upstream TLS.
14
15use axum::{
16    body::Body,
17    extract::{ConnectInfo, State},
18    http::{
19        header::{HeaderValue, HOST},
20        Request, StatusCode,
21    },
22    response::{IntoResponse, Response},
23    Router,
24};
25use axum_server::tls_rustls::RustlsConfig;
26use hyper_util::{
27    client::legacy::{connect::HttpConnector, Client},
28    rt::TokioExecutor,
29};
30use std::{net::SocketAddr, sync::Arc};
31
32#[derive(Clone)]
33struct AppState {
34    upstream: String,
35    client: Client<HttpConnector, Body>,
36}
37
38#[tokio::main]
39async fn main() {
40    let upstream = std::env::var("UPSTREAM")
41        .unwrap_or_else(|_| "http://127.0.0.1:8080".to_owned())
42        .trim_end_matches('/')
43        .to_owned();
44
45    // Validate the upstream URL early.
46    upstream.parse::<http::Uri>().expect("invalid UPSTREAM");
47
48    let state = Arc::new(AppState {
49        upstream,
50        client: Client::builder(TokioExecutor::new()).build_http(),
51    });
52
53    let app = Router::new().fallback(proxy).with_state(state);
54
55    let config = RustlsConfig::from_pem_file(
56        "examples/self-signed-certs/cert.pem",
57        "examples/self-signed-certs/key.pem",
58    )
59    .await
60    .unwrap();
61
62    let addr = SocketAddr::from(([127, 0, 0, 1], 3443));
63    println!("listening on https://{addr}");
64
65    axum_server::bind_rustls(addr, config)
66        .serve(app.into_make_service_with_connect_info::<SocketAddr>())
67        .await
68        .unwrap();
69}
70
71async fn proxy(
72    State(state): State<Arc<AppState>>,
73    ConnectInfo(remote): ConnectInfo<SocketAddr>,
74    mut req: Request<Body>,
75) -> Response {
76    let path_and_query = req
77        .uri()
78        .path_and_query()
79        .map(|path| path.as_str())
80        .unwrap_or("/");
81
82    let host = req.headers().get(HOST).cloned();
83
84    *req.uri_mut() = match format!("{}{}", state.upstream, path_and_query).parse() {
85        Ok(uri) => uri,
86        Err(_) => return (StatusCode::BAD_REQUEST, "invalid upstream request URI").into_response(),
87    };
88
89    // Preserve the original Host header and record the external request
90    // information instead of trusting client-supplied headers.
91    if let Some(host) = host {
92        req.headers_mut().insert(HOST, host);
93    }
94    req.headers_mut()
95        .insert("x-forwarded-proto", HeaderValue::from_static("https"));
96    if let Ok(ip) = HeaderValue::from_str(&remote.ip().to_string()) {
97        req.headers_mut().insert("x-forwarded-for", ip);
98    }
99
100    match state.client.request(req).await {
101        Ok(response) => response.into_response(),
102        Err(error) => (
103            StatusCode::BAD_GATEWAY,
104            format!("upstream request failed: {error}"),
105        )
106            .into_response(),
107    }
108}