Skip to main content

oxidux/
proxy.rs

1use std::future::Future;
2use std::net::{SocketAddr, TcpListener};
3
4use hyper::service::{make_service_fn, service_fn};
5use hyper::{client::HttpConnector, Body, Client, Request, Response, Server, Uri};
6use url::Url;
7
8use crate::host_resolver;
9
10mod autostart_response;
11mod host_missing;
12mod meta_server;
13
14use crate::{app::App, config::Config, process_manager::ProcessManager};
15
16const ERROR_MESSAGE: &str = "No response from server";
17
18async fn error_response(error: &hyper::Error, app: &App) -> Response<Body> {
19    eprintln!("Request to backend failed with error \"{}\"", error);
20
21    if app.is_running().await {
22        let body = Body::from(ERROR_MESSAGE);
23        Response::builder()
24            .header("Content-Type", "text/plain; charset=utf-8")
25            .body(body)
26            .unwrap()
27    } else {
28        app.start().await;
29
30        autostart_response::autostart_response()
31    }
32}
33
34pub async fn start_server(config: Config, shutdown_handler: impl Future<Output = ()>) {
35    let (addr, server): (_, color_eyre::Result<_>) = if let Ok(listener) = get_activation_socket() {
36        let addr = listener.local_addr().unwrap();
37        (addr, Server::from_tcp(listener).map_err(|e| e.into()))
38    } else {
39        use eyre::WrapErr;
40        let addr = &build_address(&config);
41        (
42            *addr,
43            Server::try_bind(addr).context("Failed to start proxy on specified port"),
44        )
45    };
46
47    eprintln!("Starting proxy server on {}", addr);
48
49    let proxy = make_service_fn(|_| async move {
50        Ok::<_, eyre::Error>(service_fn(move |req| {
51            let client = Client::new();
52            handle_request(req, client)
53        }))
54    });
55
56    let server = server
57        .unwrap()
58        .serve(proxy)
59        .with_graceful_shutdown(shutdown_handler);
60
61    if let Err(e) = server.await {
62        eprintln!("server error: {}", e);
63    }
64}
65
66#[cfg(not(target_os = "macos"))]
67fn get_activation_socket() -> color_eyre::Result<TcpListener> {
68    let mut listenfd = listenfd::ListenFd::from_env();
69    listenfd
70        .take_tcp_listener(0)?
71        .ok_or_else(|| eyre::eyre!("No socket provided"))
72}
73
74#[cfg(target_os = "macos")]
75mod launchd;
76#[cfg(target_os = "macos")]
77fn get_activation_socket() -> color_eyre::Result<TcpListener> {
78    let result = launchd::get_activation_socket("HttpSocket");
79
80    result.map_err(|e| e.into())
81}
82
83async fn handle_request(
84    mut request: Request<Body>,
85    client: Client<HttpConnector>,
86) -> color_eyre::Result<Response<Body>> {
87    let host = request.headers().get("HOST").unwrap().to_str().unwrap();
88    eprintln!("Serving request for host {:?}", host);
89    eprintln!("Full req URI {}", request.uri());
90
91    let app = {
92        match host_resolver::resolve(&host).await {
93            Some(app) => app.clone(),
94            None => {
95                let process_manager = ProcessManager::global().read().await;
96                return Ok(host_missing::missing_host_response(host, &process_manager).await);
97            }
98        }
99    };
100
101    if meta_server::is_meta_request(&request) {
102        return meta_server::handle_request(request, app).await;
103    }
104
105    let destination_url = app_url(&app, request.uri());
106    *request.uri_mut() = destination_url;
107
108    app.touch().await;
109
110    // Apply header overrides from config
111    request.headers_mut().extend(app.headers().clone());
112
113    let result = client.request(request).await;
114
115    match result {
116        Ok(response) => {
117            eprintln!("Proxying response");
118
119            Ok(response)
120        }
121        Err(e) => Ok(error_response(&e, &app).await),
122    }
123}
124
125fn build_address(config: &Config) -> SocketAddr {
126    let port = config.general.proxy_port;
127    format!("127.0.0.1:{}", port).parse().unwrap()
128}
129
130fn app_url(process: &App, request_url: &Uri) -> Uri {
131    let base_url = Url::parse("http://localhost/").unwrap();
132
133    let mut destination_url = base_url
134        .join(request_url.path_and_query().unwrap().as_str())
135        .expect("Invalid request URL");
136
137    destination_url.set_port(Some(process.port())).unwrap();
138
139    eprintln!("Starting request to backend {}", destination_url);
140
141    destination_url.as_str().parse().unwrap()
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::config;
148
149    #[test]
150    fn build_bind_address_from_config() {
151        let config = config::Config {
152            general: config::ProxyConfig {
153                proxy_port: 80,
154                ..Default::default()
155            },
156        };
157
158        let addr = build_address(&config);
159
160        assert_eq!(addr.port(), 80);
161        let localhost: std::net::IpAddr = "127.0.0.1".parse().unwrap();
162        assert_eq!(addr.ip(), localhost);
163    }
164
165    #[test]
166    fn app_url_test() {
167        let config = config::App {
168            name: "testapp".to_string(),
169            command_config: config::CommandConfig::Procfile,
170            port: Some(42),
171            ..Default::default()
172        };
173        let app = App::from_config(&config, 0, "test".to_string());
174        let source_uri = "http://testapp.test/path?query=true".parse().unwrap();
175
176        let result = app_url(&app, &source_uri);
177
178        assert_eq!(result, "http://localhost:42/path?query=true")
179    }
180}