Skip to content

Commit

Permalink
support and showcase https connect proxies
Browse files Browse the repository at this point in the history
Closes #193
  • Loading branch information
GlenDC committed May 20, 2024
1 parent 0496774 commit b50fbc0
Show file tree
Hide file tree
Showing 9 changed files with 417 additions and 7 deletions.
3 changes: 3 additions & 0 deletions docs/book/src/proxies/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
- [/examples/http_connect_proxy.rs](https://github.com/plabayo/rama/tree/main/examples/http_connect_proxy.rs):
Spawns a minimal http proxy which accepts http/1.1 and h2 connections alike,
and proxies them to the target host.
- [/examples/https_connect_proxy.rs](https://github.com/plabayo/rama/tree/main/examples/https_connect_proxy.rs):
Spawns a minimal https connect proxy which accepts http/1.1 and h2 connections alike,
and proxies them to the target host through a TLS tunnel.

## Description

Expand Down
230 changes: 230 additions & 0 deletions examples/https_connect_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! This example demonstrates how to create an https proxy.
//!
//! This proxy example does not perform any TLS termination on the actual proxied traffic.
//! It is an adoptation of the `http_connect_proxy` example with tls termination for the incoming connections.
//!
//! # Run the example
//!
//! ```sh
//! cargo run --example https_connect_proxy
//! ```
//!
//! # Expected output
//!
//! The server will start and listen on `:62016`. You can use `curl` to interact with the service:
//!
//! ```sh
//! curl --proxy-insecure -v -x https://127.0.0.1:62016 --proxy-user 'john:secret' http://www.example.com
//! curl --proxy-insecure -k -v https://127.0.0.1:62016 --proxy-user 'john:secret' https://www.example.com
//! ```
//!
//! You should see in both cases the responses from the example domains.
//!
//! In case you want to use it in a standard browser,
//! you'll need to first import and trust the generated certificate.

use rama::{
http::{
client::HttpClient,
layer::{
proxy_auth::ProxyAuthLayer,
trace::TraceLayer,
upgrade::{UpgradeLayer, Upgraded},
},
matcher::MethodMatcher,
server::HttpServer,
Body, IntoResponse, Request, RequestContext, Response, StatusCode,
},
rt::Executor,
service::{service_fn, Context, Service, ServiceBuilder},
stream::layer::http::BodyLimitLayer,
tcp::{server::TcpListener, utils::is_connection_error},
tls::{
dep::rcgen::KeyPair,
rustls::{
dep::{
pki_types::{CertificateDer, PrivatePkcs8KeyDer},
rustls::ServerConfig,
},
server::{IncomingClientHello, TlsAcceptorLayer, TlsClientConfigHandler},
},
},
utils::graceful::Shutdown,
};

use std::convert::Infallible;
use std::time::Duration;
use tracing::metadata::LevelFilter;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();

let shutdown = Shutdown::default();

// Create an issuer CA cert.
let alg = &rcgen::PKCS_ECDSA_P256_SHA256;
let ca_key_pair = KeyPair::generate_for(alg).expect("generate ca key pair");

let mut ca_params = rcgen::CertificateParams::new(Vec::new()).expect("create ca params");
ca_params
.distinguished_name
.push(rcgen::DnType::OrganizationName, "Rustls Server Acceptor");
ca_params
.distinguished_name
.push(rcgen::DnType::CommonName, "Example CA");
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::DigitalSignature,
rcgen::KeyUsagePurpose::CrlSign,
];
let ca_cert = ca_params.self_signed(&ca_key_pair).expect("create ca cert");

// Create a server end entity cert issued by the CA.
let server_key_pair = KeyPair::generate_for(alg).expect("generate server key pair");
let mut server_ee_params = rcgen::CertificateParams::new(vec!["127.0.0.1".to_string()])
.expect("create server ee params");
server_ee_params.is_ca = rcgen::IsCa::NoCa;
server_ee_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
let server_cert = server_ee_params
.signed_by(&server_key_pair, &ca_cert, &ca_key_pair)
.expect("create server cert");
let server_cert_der: CertificateDer = server_cert.into();
let server_key_der = PrivatePkcs8KeyDer::from(server_key_pair.serialize_der());

// create tls proxy
shutdown.spawn_task_fn(|guard| async move {
let tls_client_config_handler = TlsClientConfigHandler::default()
.store_client_hello()
.server_config_provider(|client_hello: IncomingClientHello| async move {
tracing::debug!(?client_hello, "client hello");

// Return None in case you want to use the default acceptor Tls config
// Usually though when implementing this trait it's because you
// want to use the client hello to determine which server config to use.
Ok(None)
});

let tls_server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![server_cert_der],
PrivatePkcs8KeyDer::from(server_key_der.secret_pkcs8_der().to_owned()).into(),
)
.expect("create tls server config");

let tcp_service = TcpListener::build()
.bind("127.0.0.1:62016")
.await
.expect("bind tcp proxy to 127.0.0.1:62016");

let exec = Executor::graceful(guard.clone());
let http_service = HttpServer::auto(exec).service(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
// See [`ProxyAuthLayer::with_labels`] for more information,
// e.g. can also be used to extract upstream proxy filters
.layer(ProxyAuthLayer::basic(("john", "secret")))
.layer(UpgradeLayer::new(
MethodMatcher::CONNECT,
service_fn(http_connect_accept),
service_fn(http_connect_proxy),
))
.service_fn(http_plain_proxy),
);

tcp_service
.serve_graceful(
guard,
ServiceBuilder::new()
// protect the http proxy from too large bodies, both from request and response end
.layer(BodyLimitLayer::symmetric(2 * 1024 * 1024))
.layer(TlsAcceptorLayer::with_client_config_handler(
tls_server_config,
tls_client_config_handler,
))
.service(http_service),
)
.await;
});

shutdown
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}

async fn http_connect_accept<S>(
mut ctx: Context<S>,
req: Request,
) -> Result<(Response, Context<S>, Request), Response>
where
S: Send + Sync + 'static,
{
match ctx
.get_or_insert_with::<RequestContext>(|| RequestContext::from(&req))
.host
.as_ref()
{
Some(host) => tracing::info!("accept CONNECT to {host}"),
None => {
tracing::error!("error extracting host");
return Err(StatusCode::BAD_REQUEST.into_response());
}
}

Ok((StatusCode::OK.into_response(), ctx, req))
}

async fn http_connect_proxy<S>(ctx: Context<S>, mut upgraded: Upgraded) -> Result<(), Infallible>
where
S: Send + Sync + 'static,
{
let host = ctx
.get::<RequestContext>()
.unwrap()
.host
.as_ref()
.unwrap()
.clone();
tracing::info!("CONNECT to {}", host);
let mut stream = match tokio::net::TcpStream::connect(&host).await {
Ok(stream) => stream,
Err(err) => {
tracing::error!(error = %err, "error connecting to host");
return Ok(());
}
};
if let Err(err) = tokio::io::copy_bidirectional(&mut upgraded, &mut stream).await {
if !is_connection_error(&err) {
tracing::error!(error = %err, "error copying data");
}
}
Ok(())
}

async fn http_plain_proxy<S>(ctx: Context<S>, req: Request) -> Result<Response, Infallible>
where
S: Send + Sync + 'static,
{
let client = HttpClient::default();
match client.serve(ctx, req).await {
Ok(resp) => Ok(resp),
Err(err) => {
tracing::error!(error = %err, "error in client request");
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap())
}
}
}
26 changes: 20 additions & 6 deletions src/proxy/http/client/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::http::{Request, RequestContext};
use crate::proxy::{ProxyCredentials, ProxySocketAddr};
use crate::service::{Context, Layer, Service};
use crate::stream::Stream;
use crate::tls::HttpsTunnel;
use std::fmt;
use std::future::Future;
use std::net::SocketAddr;
Expand Down Expand Up @@ -52,6 +53,9 @@ impl<P: Clone> Clone for HttpProxyConnectorLayer<P> {
pub struct HttpProxyInfo {
/// The proxy address to connect to.
pub proxy: SocketAddr,
/// Indicates if the proxy requires a Tls connection.
/// TODO: what about custom configs?!
pub secure: bool,
/// The credentials to use for the proxy connection.
pub credentials: Option<ProxyCredentials>,
}
Expand All @@ -67,12 +71,16 @@ impl FromStr for HttpProxyInfo {
.with_context(|| format!("parse http proxy address '{}'", raw_uri))
})?;

if uri.scheme().map(|s| s.as_str()).unwrap_or("http") != "http" {
return Err(OpaqueError::from_display(format!(
"only http proxies are supported: '{}'",
raw_uri
)));
}
let secure = match uri.scheme().map(|s| s.as_str()).unwrap_or("http") {
"http" => false,
"https" => true,
_ => {
return Err(OpaqueError::from_display(format!(
"only http proxies are supported: '{}'",
raw_uri
)));
}
};

// TODO: allow for dns address (proxy routers?);
// see: https://github.com/plabayo/rama/issues/202
Expand All @@ -93,6 +101,7 @@ impl FromStr for HttpProxyInfo {

Ok(Self {
proxy,
secure,
credentials: None,
})
}
Expand Down Expand Up @@ -240,6 +249,11 @@ where
// in case the provider gave us a proxy info, we insert it into the context
if let Some(info) = info.as_ref() {
ctx.insert(ProxySocketAddr::new(info.proxy));
if info.secure {
ctx.insert(HttpsTunnel {
server_name: info.proxy.ip().to_string(),
});
}
}

let established_conn =
Expand Down
8 changes: 8 additions & 0 deletions src/tls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

pub mod rustls;

#[derive(Debug, Clone)]
/// Context information that can be provided `https` connectors`,
/// to configure the connection in function on an https tunnel.
pub struct HttpsTunnel {
/// The server name to use for the connection.
pub server_name: String,
}

pub mod dep {
//! Dependencies for rama tls modules.
//!
Expand Down

0 comments on commit b50fbc0

Please sign in to comment.