Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 354 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ libc = "0.2"
once_cell = "1.5.2"
color-eyre = "0.5.1"
async-stream = "0.3.0"
tokio-rustls = "0.22.0"
rcgen = { version = "0.8.5", features = ["x509-parser"] }
rand = "0.8.3"
parking_lot = "0.11.0"
webpki = "0.21.3"

[[bin]]
name = "echo-server"
path = "tests/helpers/echo_server.rs"

[target.'cfg(target_os = "macos")'.dependencies]
trust-dns-server = "0.20.0"

[dev-dependencies]
rand = "0.8.3"
28 changes: 28 additions & 0 deletions _wildcard.test-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD52ZaoVc8nMMCN
3FTn7FBYwQNxFFor1OaNCu1dlW9V531Sf4bi1Gdrf5/zWpvsg666lvwoSItDqKEh
qhrKUfJKeOqYzeheIloS4N9ucjH+LO+1EMo3YYJXGoQpQSqiQmTukfJQUcVsGUTc
q+IYJFfzlnJmPllyOD0GqPnJfde9J4DR+fhpvGQ+OOdntx/SFhOZQICMLsVyOQuF
aGbJIIj+zYUzZLKUDV6F7CsDL++fdWWlrcvCLNFkPmNnNOc6CPLYZNJEjWi0BV1v
Zwa8NgjoAXZ+4h9othWCX67AJJFIt39FMl1tQxo395x47tTqyNGNvjDMLBwYlwnC
RgV4cC2VAgMBAAECggEAPTIiSJDb8ElsoFJ7KWMkOtjrsuK9Q0ceQSWQBf/4CR5t
/6rkquJDgnz7/GsRDdkjDui0UlmSYrwG22wCq9NuePcs3shwRb48OauCjlbCD/OJ
stut6+qiNht0i3Y+rwd8GUL+CtY8eMGnsDUZZ7hfInaTBp/24JcNu3ff1o5QLS/n
44tfN8fE232jSMq+Y+r9YudW/9hcAutOwTvGHsim02ktyVZixafuC/nmNA3LgM49
+qWwr3QzbqqIXuEPOCFK9p0goRtmKRW3kcFB4FePUl+aONDFmwSAN+jm8xWqQ/4m
PCez+Vx64SNyHL9Qe43Ly+wqJ3nA5bg5XBisWz4igQKBgQD/ujiGRRSS/w4LamsT
gUgp3WJoXI4sXr8Lk/dc4iVpVmzEPJVoMNr+lqNL5/l9y4odA5WLC/yoGTmLPc5b
ATPa7K681MV+ke+J1FTDwjjjQYAE1vlDMvU2IP329fG10ZBi9lSUmLMYyuTwH+Qr
DyRDZw8cYcLA9REsM/zzyt0FvQKBgQD6HcOSHejgL6BB5DG2+33VGT+aZl8mIhiP
yXeOS4mss8v4XBwYX5TPw8dmwx1x6faof9fliKBDRlSqFLV9Vt7eQO/UA3XjjgZA
MHXpYtdSyOg0WC9P/z2pybqJGAQTYprb1zljgrWTW57Wcqne0u4/BVIqHGUDHP+I
YzFlDm+ouQKBgD04DloOZYt/JZSUCEgmFel3xxwmtB5pHCEgbgI9XSlneChOPJIx
x+tUkokUYoS72jdx6TXdS8HOMBlmVWUx14EcUgSAhzryor6DJzup3kaBIq2F7Swq
IcuwgDvDyvZ00bTvNXZRS+aug7n8WHn6aPr9y/9GZAIfaNoFJBQUx26dAoGBAO/c
j2uXZ3dn9SZ7svmqoXg16Hsn5ePqGuf567/4zSVkoB2kKAVv1ISTWq1APQK7vyLE
x8WGizs5PYSGq65yGvXGDLmkP/BkibYRQ2L4uUrZBWb9kxIC0536qftDntUAYUan
VpAKEBwrZ159RE8+teCWN7/Oz0h3DNA9YGdrusVxAoGBAN4ctVGm9fvJKIpzqAQW
dEk8utSPWqVGDzZDMacOvvh5IPKTumtVrAmkB0JUcAgReLayLxGkBPxH1o4FYS0s
J+CL9QzGqX73CelrwTUZajIkUuTCuTd0i6+v+5j75F2LVOZgWxlEXR3EQCB/ZZRz
aIPXe0hsM47qN5d44Nby7hZq
-----END PRIVATE KEY-----
25 changes: 25 additions & 0 deletions _wildcard.test.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIETDCCArSgAwIBAgIRAOJNEXKYBsTeh2v5q8jBbB0wDQYJKoZIhvcNAQELBQAw
gYExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTErMCkGA1UECwwiam9u
bWFzdEBuZXctaG9zdC0zLmhvbWUgKEpvbiBNYXN0KTEyMDAGA1UEAwwpbWtjZXJ0
IGpvbm1hc3RAbmV3LWhvc3QtMy5ob21lIChKb24gTWFzdCkwHhcNMTkwNjAxMDAw
MDAwWhcNMzAwNjI3MjA0MDEyWjBWMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1l
bnQgY2VydGlmaWNhdGUxKzApBgNVBAsMImpvbm1hc3RAbmV3LWhvc3QtMy5ob21l
IChKb24gTWFzdCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD52Zao
Vc8nMMCN3FTn7FBYwQNxFFor1OaNCu1dlW9V531Sf4bi1Gdrf5/zWpvsg666lvwo
SItDqKEhqhrKUfJKeOqYzeheIloS4N9ucjH+LO+1EMo3YYJXGoQpQSqiQmTukfJQ
UcVsGUTcq+IYJFfzlnJmPllyOD0GqPnJfde9J4DR+fhpvGQ+OOdntx/SFhOZQICM
LsVyOQuFaGbJIIj+zYUzZLKUDV6F7CsDL++fdWWlrcvCLNFkPmNnNOc6CPLYZNJE
jWi0BV1vZwa8NgjoAXZ+4h9othWCX67AJJFIt39FMl1tQxo395x47tTqyNGNvjDM
LBwYlwnCRgV4cC2VAgMBAAGjaTBnMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAK
BggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFPL/lhTVlX/tC0Js
L2/P+w/NwJP0MBEGA1UdEQQKMAiCBioudGVzdDANBgkqhkiG9w0BAQsFAAOCAYEA
Q8Vn4vtBmWcifQhrSaWu3cFHDvLQ5+8fd20yuF7P7Bv+lOt/49iFVmHUDXXmepJH
yDd/cuq7HMSTyTOCF0gO+sTUHUCFO6RwzIpo5RtHBgJQ/VFmJLOngxAB3oStjuQf
av0qDNOyXugT/RwUX0C4BYl/GiOBJokpixL2zFdx8aJvaas70HfyHSf8wcTa6uco
ZylvDMsTMl2fQ5uvZG49Sar1nmucBNnWVHpESHXmylMklWOp62InfD1H9S8Qm3kH
YH3EK7MM6FCDASzNMNVcccIXz89KIlkUZwL+USYakJL2Ad/bmwPSe6vJD2lsA4Il
DWi8fsushFvOd/+qfbFwsxC+P3UmtWN/FqGaiVmMTVFZPtiyHycDHta/9m7LgEu4
/kjuleqABL2/idO2alDUFlThjABLnA36azt+5qdOoe5sPWHO7mzwwdUl5t6Ri+RE
kw5eQDa3nK3sDHIOGOEt1AEZqoG4bCkRA08fR9ETZGK9W48/A9L9X0aKJ3yexEAr
-----END CERTIFICATE-----
9 changes: 8 additions & 1 deletion src/ipc_listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ async fn restart_app(
) {
let process = {
let process_manager = ProcessManager::global_read().await;
lookup_process(&process_manager, process_name, directory).await

if process_name.is_some() {
lookup_process(&process_manager, process_name, directory).await
} else {
let app = process_manager
.find_app_for_directory(directory)
.ok_or_else(|| eyre!("Failed to find app to restart"));
}
};

let process = process.ok_or_else(|| eyre!("Failed to find app to restart"));
Expand Down
8 changes: 6 additions & 2 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,13 @@ impl Process {
pub async fn restart(&self) {
eprintln!("restarting");

match self.run_state().await {
let state = self.run_state().await;
match state {
RunState::Restarting(_) | RunState::Starting => {
eprintln!("Ignoring restart request, process is in invalid state");
eprintln!(
"Ignoring restart request, process is in invalid state {:?}",
state
);
}
RunState::Stopped => self.start().await.unwrap_or_else(|e| eprintln!("{}", e)),
RunState::Running(pid) | RunState::Terminating(pid) => {
Expand Down
59 changes: 38 additions & 21 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ 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 hyper::{Body, Client, Request, Response, Server, Uri};
use url::Url;

use crate::host_resolver;

mod autostart_response;
mod host_missing;
mod meta_server;
mod tls;

use crate::{app::App, config::Config, process_manager::ProcessManager};

Expand All @@ -32,7 +33,8 @@ async fn error_response(error: &hyper::Error, app: &App) -> Response<Body> {
}

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 [http_socket, https_socket] = get_proxy_sockets();
let (addr, server): (_, color_eyre::Result<_>) = if let Ok(listener) = http_socket {
let addr = listener.local_addr().unwrap();
(addr, Server::from_tcp(listener).map_err(|e| e.into()))
} else {
Expand All @@ -44,14 +46,14 @@ pub async fn start_server(config: Config, shutdown_handler: impl Future<Output =
)
};

if let Ok(listener) = https_socket {
tokio::spawn(async move { tls::tls_server(&config.clone(), listener).await.unwrap() });
}

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 proxy =
make_service_fn(|_| async move { Ok::<_, eyre::Error>(service_fn(handle_request)) });

let server = server
.unwrap()
Expand All @@ -64,27 +66,40 @@ pub async fn start_server(config: Config, shutdown_handler: impl Future<Output =
}

#[cfg(not(target_os = "macos"))]
fn get_activation_socket() -> color_eyre::Result<TcpListener> {
fn get_proxy_sockets() -> [color_eyre::Result<TcpListener>; 2] {
let mut listenfd = listenfd::ListenFd::from_env();
listenfd
.take_tcp_listener(0)?
.ok_or_else(|| eyre::eyre!("No socket provided"))

[
listenfd
.take_tcp_listener(0)
.map_err(Into::into)
.and_then(|option| option.ok_or_else(|| eyre::eyre!("No socket provided"))),
listenfd
.take_tcp_listener(1)
.map_err(Into::into)
.and_then(|option| option.ok_or_else(|| eyre::eyre!("No socket provided"))),
]
}

#[cfg(target_os = "macos")]
pub(crate) mod launchd;
#[cfg(target_os = "macos")]
fn get_activation_socket() -> color_eyre::Result<TcpListener> {
let result = launchd::get_tcp_socket("HttpSocket");

result.map_err(|e| e.into())
fn get_proxy_sockets() -> [color_eyre::Result<TcpListener>; 2] {
[
launchd::get_tcp_socket("HttpSocket").map_err(Into::into),
launchd::get_tcp_socket("HttpsSocket").map_err(Into::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();
async fn handle_request(mut request: Request<Body>) -> color_eyre::Result<Response<Body>> {
let host = request
.headers()
.get("HOST")
.and_then(|v| v.to_str().ok())
// HTTP2 requests only set uri, not host header
.or_else(|| request.uri().host())
.unwrap_or_default();

eprintln!("Serving request for host {:?}", host);
eprintln!("Full req URI {}", request.uri());

Expand All @@ -104,12 +119,14 @@ async fn handle_request(

let destination_url = app_url(&app, request.uri());
*request.uri_mut() = destination_url;
*request.version_mut() = hyper::Version::HTTP_11;

app.touch().await;

// Apply header overrides from config
request.headers_mut().extend(app.headers().clone());

let client = Client::new();
let result = client.request(request).await;

match result {
Expand Down
175 changes: 175 additions & 0 deletions src/proxy/tls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::{create_dir, File};
use std::io::prelude::*;
use std::path::Path;
use std::sync::Arc;

use async_stream::stream;
use futures::stream::{Stream, StreamExt};
use hyper::service::{make_service_fn, service_fn};
use hyper::Server;
use parking_lot::RwLock;
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::rustls::{self, sign};
use tokio_rustls::{server::TlsStream, TlsAcceptor};
use webpki::{DNSName, DNSNameRef};

use crate::config::Config;

pub(crate) async fn tls_server(
config: &Config,
socket: std::net::TcpListener,
) -> color_eyre::Result<()> {
let proxy =
make_service_fn(|_| async move { Ok::<_, eyre::Error>(service_fn(super::handle_request)) });

let incoming_tls_stream = tls_stream(config, socket)?.filter(|s| {
eprintln!("Filtering");
futures::future::ready(match s {
Ok(_) => {
eprintln!("Done filtering");
true
}
Err(e) => {
eprintln!("Error in TLS stream:\n{:#}", e);
false
}
})
});

let server =
Server::builder(hyper::server::accept::from_stream(incoming_tls_stream)).serve(proxy);

server.await?;
Ok(())
}

fn tls_stream(
config: &Config,
socket: std::net::TcpListener,
) -> color_eyre::Result<impl Stream<Item = color_eyre::Result<TlsStream<TcpStream>>>> {
let config_dir = config.general.config_dir.clone();

let tls_cfg = {
let cert_resolver = CertificateResolver::new(root_cert(&config_dir).unwrap());

let mut cfg = tokio_rustls::rustls::ServerConfig::new(rustls::NoClientAuth::new());
cfg.cert_resolver = Arc::new(cert_resolver);
// Configure ALPN to accept HTTP/2, HTTP/1.1 in that order.
cfg.set_protocols(&[b"h2".to_vec(), b"http/1.1".to_vec()]);
Arc::new(cfg)
};

let tls_acceptor = TlsAcceptor::from(tls_cfg);

eprintln!("Starting TLS server on {}", socket.local_addr().unwrap());

let listener = TcpListener::from_std(socket)?;

let stream = stream! {
loop {
eprintln!("Starting loop");
match listener.accept().await {
Ok((sock, _addr)) => {
eprintln!("Accepted");
yield tls_acceptor.accept(sock).await.map_err(Into::into);
},
Err(e) => yield Err(e.into()),
}
eprintln!("Loop complete");
}
};

Ok(stream)
}

/// Get root certificate, autogenerating it if it doesn't exist
fn root_cert(config_dir: &Path) -> color_eyre::Result<rcgen::Certificate> {
let certs_dir = config_dir.join("certs");
let cert_path = certs_dir.join("root-cert.der");
let key_path = certs_dir.join("root-key.der");

if cert_path.exists() && key_path.exists() {
let mut root_key = Vec::new();
File::open(key_path)?.read_to_end(&mut root_key)?;

let mut cert_data = Vec::new();
File::open(cert_path)?.read_to_end(&mut cert_data)?;

let key = rcgen::KeyPair::try_from(root_key.as_slice())?;
let cert_params = rcgen::CertificateParams::from_ca_cert_der(&cert_data, key)?;
Ok(rcgen::Certificate::from_params(cert_params)?)
} else {
create_dir(certs_dir).ok();
let mut cert_params = rcgen::CertificateParams::new(vec!["Oxidux".to_string()]);
cert_params
.distinguished_name
.push(rcgen::DnType::CommonName, "Oxidux CA");
cert_params
.distinguished_name
.push(rcgen::DnType::OrganizationName, "Oxidux CA");
cert_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let generated_cert = rcgen::Certificate::from_params(cert_params)?;

File::create(cert_path)?.write_all(&generated_cert.serialize_der()?)?;
File::create(key_path)?.write_all(&generated_cert.serialize_private_key_der())?;

Ok(generated_cert)
}
}

struct CertificateResolver {
root_cert: rcgen::Certificate,
cert_cache: RwLock<HashMap<DNSName, sign::CertifiedKey>>,
}

impl CertificateResolver {
fn new(root_cert: rcgen::Certificate) -> Self {
Self {
root_cert,
cert_cache: RwLock::new(HashMap::default()),
}
}

/// Generate a new key for the provided domain and sign it with our CA key
fn generate_certified_key(&self, domain: DNSNameRef) -> sign::CertifiedKey {
let domain: &str = domain.into();
let mut cert_params = rcgen::CertificateParams::new(vec![domain.to_string()]);
cert_params
.distinguished_name
.push(rcgen::DnType::CommonName, domain);
cert_params
.distinguished_name
.push(rcgen::DnType::OrganizationName, "Oxidux CA");
cert_params.serial_number = Some(rand::random());
let generated_cert = rcgen::Certificate::from_params(cert_params).unwrap();
let cert = rustls::Certificate(
generated_cert
.serialize_der_with_signer(&self.root_cert)
.unwrap(),
);

let pkey = rustls::PrivateKey(generated_cert.serialize_private_key_der());

let signing_key = rustls::sign::any_supported_type(&pkey).unwrap();

sign::CertifiedKey::new(vec![cert], Arc::new(signing_key))
}
}

impl rustls::ResolvesServerCert for CertificateResolver {
fn resolve(&self, client_hello: rustls::ClientHello) -> Option<sign::CertifiedKey> {
let domain = client_hello.server_name()?.to_owned();

if let Some(key) = self.cert_cache.read().get(&domain) {
return Some(key.clone());
}

let key = self.generate_certified_key(domain.as_ref());

self.cert_cache.write().insert(domain, key.clone());

Some(key)
}
}