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 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}