#![expect(
clippy::unwrap_used,
clippy::expect_used,
reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]
use rama::{
Layer, Service,
error::{BoxError, ErrorContext},
extensions::{Extension, ExtensionsRef},
http::{
BodyLimitLayer, HeaderValue, Request, Response, StatusCode,
client::EasyHttpWebClient,
layer::{
compression::CompressionLayer,
decompression::DecompressionLayer,
har::{
self,
layer::HARExportLayer,
recorder::{FileRecorder, HarFilePath, Recorder},
},
map_response_body::MapResponseBodyLayer,
proxy_auth::ProxyAuthLayer,
remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer},
required_header::AddRequiredRequestHeadersLayer,
trace::TraceLayer,
upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer, Upgraded},
},
matcher::{DomainMatcher, MethodMatcher},
server::HttpServer,
service::web::{WebService, response::IntoResponse},
},
layer::{AddInputExtensionLayer, ConsumeErrLayer, HijackLayer},
net::user::credentials::basic,
rt::Executor,
service::service_fn,
tcp::server::TcpListener,
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
tls::boring::{
client::{BoringClientConfigExt, EmulateTlsProfileLayer},
server::TlsAcceptorLayer,
},
tls::{
SecureTransport,
client::{ServerVerifyMode, TlsClientConfig},
server::{SelfSignedData, TlsServerConfig},
},
ua::{
layer::emulate::{
UserAgentEmulateHttpConnectModifierLayer, UserAgentEmulateHttpRequestModifierLayer,
UserAgentEmulateLayer,
},
profile::UserAgentDatabase,
},
utils::octets::mib,
};
use std::{
convert::Infallible,
sync::{Arc, atomic::AtomicBool},
time::Duration,
};
use tokio::sync::mpsc;
#[derive(Debug, Clone, Extension)]
struct State {
mitm_tls_service_config: TlsServerConfig,
ua_db: Arc<UserAgentDatabase>,
har_layer: HARExportLayer<FileRecorder, Arc<AtomicBool>>,
har_toggle_ctl: mpsc::Sender<()>,
exec: Executor,
}
#[tokio::main]
async fn main() -> Result<(), BoxError> {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
let mitm_tls_service_config = new_mitm_tls_service_config();
let graceful = rama::graceful::Shutdown::default();
let (har_toggle, har_toggle_ctl) =
har::toggle::mpsc_toggle(8, graceful.guard_weak().into_cancelled());
let har_layer = HARExportLayer::new(FileRecorder::default(), har_toggle);
let state = State {
mitm_tls_service_config,
ua_db: Arc::new(UserAgentDatabase::try_embedded()?),
har_layer,
har_toggle_ctl,
exec: Executor::graceful(graceful.guard()),
};
graceful.spawn_task_fn(async |guard| {
let exec = Executor::graceful(guard);
let tcp_service = TcpListener::build(exec.clone())
.bind_address("127.0.0.1:62040")
.await
.expect("bind tcp proxy to 127.0.0.1:62040");
let http_mitm_service = new_http_mitm_proxy(&state);
let http_service = HttpServer::auto(exec.clone()).service(Arc::new(
(
TraceLayer::new_for_http(),
ConsumeErrLayer::default(),
ProxyAuthLayer::new(basic!("john", "secret")),
HijackLayer::new(
DomainMatcher::exact("har.toggle.internal"),
Arc::new(WebService::default().with_post("/switch", async |req: Request| {
let state = req.extensions().get_ref::<State>().unwrap();
if let Err(err) = state.har_toggle_ctl.send(()).await {
tracing::error!("failed to toggle HAR Recording: {err}");
return StatusCode::INTERNAL_SERVER_ERROR;
} else {
tracing::debug!(
"force a stop recording so the file immediately flushes (DX etc)"
);
state.har_layer.recorder().stop_record().await;
}
StatusCode::OK
})),
),
UpgradeLayer::new(
exec,
MethodMatcher::CONNECT,
DefaultHttpProxyConnectReplyService::new(),
service_fn(http_connect_proxy),
),
)
.into_layer(http_mitm_service)),
);
tcp_service
.serve(
(
AddInputExtensionLayer::new(state),
BodyLimitLayer::symmetric(mib(2)),
)
.into_layer(http_service),
)
.await;
});
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.context("graceful shutdown")?;
Ok(())
}
async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
let state = upgraded.extensions().get_ref::<State>().unwrap();
let http_service = Arc::new(new_http_mitm_proxy(state));
let executor = state.exec.clone();
let mut http_tp = HttpServer::auto(executor);
http_tp.h2_mut().set_enable_connect_protocol();
let http_transport_service = http_tp.service(http_service);
let https_service = TlsAcceptorLayer::new(state.mitm_tls_service_config.clone())
.with_store_client_hello(true)
.into_layer(http_transport_service);
https_service.serve(upgraded).await.expect("infallible");
Ok(())
}
fn new_http_mitm_proxy(
state: &State,
) -> impl Service<Request, Output = Response, Error = Infallible> {
(
MapResponseBodyLayer::new_boxed_streaming_body(),
TraceLayer::new_for_http(),
ConsumeErrLayer::default(),
UserAgentEmulateLayer::new(state.ua_db.clone())
.with_try_auto_detect_user_agent(true)
.with_is_optional(true),
CompressionLayer::new(),
AddRequiredRequestHeadersLayer::new(),
EmulateTlsProfileLayer::new(),
)
.into_layer(service_fn(http_mitm_proxy))
}
async fn http_mitm_proxy(req: Request) -> Result<Response, Infallible> {
let tls_config = req
.extensions()
.get_ref::<SecureTransport>()
.and_then(|st| st.client_hello())
.map(TlsClientConfig::new_from_client_hello)
.unwrap_or_else(TlsClientConfig::default_http)
.with_server_verify(ServerVerifyMode::Disable);
let state = req.extensions().get_ref::<State>().unwrap();
let client = EasyHttpWebClient::connector_builder()
.with_default_transport_connector()
.with_default_dns_connector()
.with_tls_proxy_support_using_boringssl()
.with_proxy_support()
.with_tls_support_using_boringssl(tls_config)
.with_custom_connector(UserAgentEmulateHttpConnectModifierLayer::default())
.with_default_http_connector(state.exec.clone())
.build_client()
.with_jit_layer(UserAgentEmulateHttpRequestModifierLayer::default());
let client = (
RemoveResponseHeaderLayer::hop_by_hop(),
RemoveRequestHeaderLayer::hop_by_hop(),
MapResponseBodyLayer::new_boxed_streaming_body(),
DecompressionLayer::new(),
state.har_layer.clone(),
)
.into_layer(client);
match client.serve(req).await {
Ok(mut resp) => {
if let Some(har_fp) = resp
.extensions()
.get_ref::<HarFilePath>()
.map(|fp| fp.display().to_string())
.map(|fp| HeaderValue::try_from(fp).unwrap())
{
resp.headers_mut().insert("x-rama-har-file-path", har_fp);
}
Ok(resp)
}
Err(err) => {
tracing::error!("error in client request: {err:?}");
Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}
}
fn new_mitm_tls_service_config() -> TlsServerConfig {
TlsServerConfig::new()
.try_with_self_signed(SelfSignedData {
organisation_name: Some("Example Server Acceptor".to_owned()),
..Default::default()
})
.expect("self-signed")
.with_alpn_http_auto()
}