#![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,
extensions::ExtensionsRef,
http::{
Body, Request, Response, StatusCode,
client::EasyHttpWebClient,
layer::{
into_response::IntoResponseLayer,
proxy_auth::ProxyAuthLayer,
remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer},
trace::TraceLayer,
upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer},
},
matcher::{DomainMatcher, MethodMatcher},
server::HttpServer,
service::web::response::Html,
},
layer::{ConsumeErrLayer, HijackLayer},
net::{
address::SocketAddress, http::server::HttpPeekRouter, proxy::IoForwardService,
stream::SocketInfo, user::credentials::basic,
},
proxy::socks5::{
Socks5Acceptor,
server::{LazyConnector, Socks5PeekRouter},
},
rt::Executor,
service::StaticOutput,
service::service_fn,
tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener},
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
};
use std::{convert::Infallible, time::Duration};
fn new_example_hijack_svc() -> impl Clone + Service<Request, Output = Response, Error = Infallible>
{
IntoResponseLayer::new().into_layer(StaticOutput::new(Html(
r##"<!doctype html>
<html>
<head>
<title>Connectivity Example</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Connectivity Example</h1>
<p>This example demonstrates how you can provide a kind of connectivity check
using protocol inspection and hijacking utilities provided by rama
for both http and socks5 proxies.</p>
<p><a href="https://ramaproxy.org">More information...</a></p>
</div>
</body>
</html>
"##,
)))
}
#[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 graceful = rama::graceful::Shutdown::default();
let exec = Executor::graceful(graceful.guard());
let tcp_service = TcpListener::bind_address(SocketAddress::default_ipv4(62030), exec.clone())
.await
.expect("bind tcp interface for connectivity example");
let proxy_service = (
RemoveResponseHeaderLayer::hop_by_hop(),
RemoveRequestHeaderLayer::hop_by_hop(),
HijackLayer::new(
DomainMatcher::exact("example.com"),
new_example_hijack_svc(),
),
)
.into_layer(service_fn(http_plain_proxy));
let http_service = HttpServer::auto(exec.clone()).service(
(
TraceLayer::new_for_http(),
ConsumeErrLayer::default(),
ProxyAuthLayer::new(basic!("tom", "clancy")),
UpgradeLayer::new(
exec.clone(),
MethodMatcher::CONNECT,
DefaultHttpProxyConnectReplyService::new(),
(
ConsumeErrLayer::default(),
IoToProxyBridgeIoLayer::extension_connector_target().with_connector(
rama::dns::client::DnsConnector::new(
rama::tcp::client::service::TcpConnector::new(),
),
),
)
.into_layer(IoForwardService::new(exec.clone())),
),
)
.into_layer(proxy_service.clone()),
);
let socks5_svc = HttpPeekRouter::new(HttpServer::auto(exec.clone()).service(proxy_service))
.with_fallback(
IoToProxyBridgeIoLayer::extension_connector_target()
.with_connector(rama::dns::client::DnsConnector::new(
rama::tcp::client::service::TcpConnector::new(),
))
.into_layer(IoForwardService::new(exec.clone())),
);
let socks5_acceptor = Socks5Acceptor::new(exec.clone())
.with_authorizer(basic!("john", "secret").into_authorizer())
.with_connector(LazyConnector::new(socks5_svc));
let auto_socks5_acceptor = Socks5PeekRouter::new(socks5_acceptor).with_fallback(http_service);
graceful.spawn_task(tcp_service.serve(auto_socks5_acceptor));
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
async fn http_plain_proxy(req: Request) -> Result<Response, Infallible> {
let client = EasyHttpWebClient::default();
match client.serve(req).await {
Ok(resp) => {
if let Some(client_socket_info) = resp
.extensions()
.egress()
.and_then(|e| e.get_ref::<SocketInfo>())
{
tracing::info!(
http.response.status_code = %resp.status(),
network.local.port = client_socket_info.local_addr().map(|addr| addr.port.to_string()).unwrap_or_default(),
network.local.address = client_socket_info.local_addr().map(|addr| addr.ip_addr.to_string()).unwrap_or_default(),
network.peer.port = %client_socket_info.peer_addr().port,
network.peer.address = %client_socket_info.peer_addr().ip_addr,
"http plain text proxy received response",
)
} else {
tracing::info!(
http.response.status_code = %resp.status(),
"http plain text proxy received response, IP info unknown",
)
};
Ok(resp)
}
Err(err) => {
tracing::error!("error in client request: {err:?}");
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap())
}
}
}