use std::future::Future;
use std::net::{SocketAddr, TcpListener};
use hyper::service::{make_service_fn, service_fn};
use hyper::{client::HttpConnector, Body, Client, Request, Response, Server, Uri};
use url::Url;
use crate::host_resolver;
mod autostart_response;
mod host_missing;
mod meta_server;
use crate::{app::App, config::Config, process_manager::ProcessManager};
const ERROR_MESSAGE: &str = "No response from server";
async fn error_response(error: &hyper::Error, app: &App) -> Response<Body> {
eprintln!("Request to backend failed with error \"{}\"", error);
if app.is_running().await {
let body = Body::from(ERROR_MESSAGE);
Response::builder()
.header("Content-Type", "text/plain; charset=utf-8")
.body(body)
.unwrap()
} else {
app.start().await;
autostart_response::autostart_response()
}
}
pub async fn start_server(config: Config, shutdown_handler: impl Future<Output = ()>) {
let (addr, server): (_, color_eyre::Result<_>) = if let Ok(listener) = get_activation_socket() {
let addr = listener.local_addr().unwrap();
(addr, Server::from_tcp(listener).map_err(|e| e.into()))
} else {
use eyre::WrapErr;
let addr = &build_address(&config);
(
*addr,
Server::try_bind(addr).context("Failed to start proxy on specified port"),
)
};
eprintln!("Starting proxy server on {}", addr);
let proxy = make_service_fn(|_| async move {
Ok::<_, eyre::Error>(service_fn(move |req| {
let client = Client::new();
handle_request(req, client)
}))
});
let server = server
.unwrap()
.serve(proxy)
.with_graceful_shutdown(shutdown_handler);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
#[cfg(not(target_os = "macos"))]
fn get_activation_socket() -> color_eyre::Result<TcpListener> {
let mut listenfd = listenfd::ListenFd::from_env();
listenfd
.take_tcp_listener(0)?
.ok_or_else(|| eyre::eyre!("No socket provided"))
}
#[cfg(target_os = "macos")]
mod launchd;
#[cfg(target_os = "macos")]
fn get_activation_socket() -> color_eyre::Result<TcpListener> {
let result = launchd::get_activation_socket("HttpSocket");
result.map_err(|e| e.into())
}
async fn handle_request(
mut request: Request<Body>,
client: Client<HttpConnector>,
) -> color_eyre::Result<Response<Body>> {
let host = request.headers().get("HOST").unwrap().to_str().unwrap();
eprintln!("Serving request for host {:?}", host);
eprintln!("Full req URI {}", request.uri());
let app = {
match host_resolver::resolve(&host).await {
Some(app) => app.clone(),
None => {
let process_manager = ProcessManager::global().read().await;
return Ok(host_missing::missing_host_response(host, &process_manager).await);
}
}
};
if meta_server::is_meta_request(&request) {
return meta_server::handle_request(request, app).await;
}
let destination_url = app_url(&app, request.uri());
*request.uri_mut() = destination_url;
app.touch().await;
request.headers_mut().extend(app.headers().clone());
let result = client.request(request).await;
match result {
Ok(response) => {
eprintln!("Proxying response");
Ok(response)
}
Err(e) => Ok(error_response(&e, &app).await),
}
}
fn build_address(config: &Config) -> SocketAddr {
let port = config.general.proxy_port;
format!("127.0.0.1:{}", port).parse().unwrap()
}
fn app_url(process: &App, request_url: &Uri) -> Uri {
let base_url = Url::parse("http://localhost/").unwrap();
let mut destination_url = base_url
.join(request_url.path_and_query().unwrap().as_str())
.expect("Invalid request URL");
destination_url.set_port(Some(process.port())).unwrap();
eprintln!("Starting request to backend {}", destination_url);
destination_url.as_str().parse().unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config;
#[test]
fn build_bind_address_from_config() {
let config = config::Config {
general: config::ProxyConfig {
proxy_port: 80,
..Default::default()
},
};
let addr = build_address(&config);
assert_eq!(addr.port(), 80);
let localhost: std::net::IpAddr = "127.0.0.1".parse().unwrap();
assert_eq!(addr.ip(), localhost);
}
#[test]
fn app_url_test() {
let config = config::App {
name: "testapp".to_string(),
command_config: config::CommandConfig::Procfile,
port: Some(42),
..Default::default()
};
let app = App::from_config(&config, 0, "test".to_string());
let source_uri = "http://testapp.test/path?query=true".parse().unwrap();
let result = app_url(&app, &source_uri);
assert_eq!(result, "http://localhost:42/path?query=true")
}
}