support auto proxy connector http+https+socks5+socks5h
As a follow up of https://github.com/plabayo/rama/issues/15
In case it is not done yet by then, it might also be time to add the enum approach for connectors, which is similar to Either but different. As originally proposed by @soundofspace .
Based on the protocol it will make use of the correct proxy connector.
@GlenDC I'm already working on this as part of splitting of boring and rustls. Will split the enum part of that work to separate PR and post it so you can already use that where needed
Either approach for connectors will be implemented in #501
I'm happy to share that the necessary components to achieve "Support auto proxy connectors for http, https, socks5 and socks5h" are already available within rama! I've stitched together an example (inspired by other examples in rama) that demonstrates this functionality, and I've successfully tested it locally over both IPv4 and IPv6.
Here is the code example:
//! An example to showcase how one can build a proxy that is both a HTTP, HTTPS, SOCKS5 and SOCKS5H Proxy in one.
//!
//! # Run the example
//!
//! ```sh
//! cargo run --example http_https_socks5_and_socks5h_proxy --features=dns,socks5,http-full,borinng
//! ```
//!
//! # Expected output
//!
//! The server will start and listen on `:62023`. You can use `curl` to interact with the service:
//!
//! ```sh
//! curl -v -x http://127.0.0.1:62023 --proxy-user 'tom:clancy' http://api64.ipify.org/
//! curl -v -x http://127.0.0.1:62023 --proxy-user 'tom:clancy' https://api64.ipify.org/
//! curl --proxy-insecure -v -x https://127.0.0.1:62023 --proxy-user 'tom:clancy' http://api64.ipify.org/
//! curl --proxy-insecure -v -x https://127.0.0.1:62023 --proxy-user 'tom:clancy' https://api64.ipify.org/
//! curl -v -x socks5://127.0.0.1:62023 --proxy-user 'john:secret' http://api64.ipify.org/
//! curl -v -x socks5h://127.0.0.1:62023 --proxy-user 'john:secret' https://api64.ipify.org/
//! curl -v -x socks5://127.0.0.1:62023 --proxy-user 'john:secret' http://api64.ipify.org/
//! curl -v -x socks5h://127.0.0.1:62023 --proxy-user 'john:secret' https://api64.ipify.org/
//! ```
//!
//! You should see in all the above examples the responses from the server.
use rama::{
Context, Layer, Service,
context::RequestContextExt,
http::{
Body, Request, Response, StatusCode,
client::EasyHttpWebClient,
layer::{
proxy_auth::ProxyAuthLayer,
remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer},
trace::TraceLayer,
upgrade::UpgradeLayer,
},
matcher::MethodMatcher,
server::HttpServer,
service::web::response::IntoResponse,
},
layer::ConsumeErrLayer,
net::{
http::RequestContext,
proxy::ProxyTarget,
stream::ClientSocketInfo,
tls::{
SecureTransport,
server::{SelfSignedData, TlsPeekRouter},
},
user::Basic,
},
proxy::socks5::{Socks5Acceptor, Socks5Auth, server::Socks5PeekRouter},
rt::Executor,
service::service_fn,
tcp::{client::service::Forwarder, server::TcpListener},
tls::boring::server::TlsAcceptorService,
};
#[cfg(feature = "boring")]
use rama::net::tls::{
ApplicationProtocol,
server::{ServerAuth, ServerConfig},
};
#[cfg(all(feature = "rustls", not(feature = "boring")))]
use rama::tls::rustls::server::{TlsAcceptorDataBuilder, TlsAcceptorLayer};
use std::{convert::Infallible, time::Duration};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.from_env_lossy(),
)
.init();
let graceful = rama::graceful::Shutdown::default();
#[cfg(feature = "boring")]
let tls_service_data = {
let tls_server_config = ServerConfig {
application_layer_protocol_negotiation: Some(vec![
ApplicationProtocol::HTTP_2,
ApplicationProtocol::HTTP_11,
]),
..ServerConfig::new(ServerAuth::SelfSigned(SelfSignedData {
organisation_name: Some("Example Server Acceptor".to_owned()),
..Default::default()
}))
};
tls_server_config
.try_into()
.expect("create tls server config")
};
#[cfg(all(feature = "rustls", not(feature = "boring")))]
let tls_service_data = {
TlsAcceptorDataBuilder::new_self_signed(SelfSignedData {
organisation_name: Some("Example Server Acceptor".to_owned()),
..Default::default()
})
.expect("self signed acceptor data")
.with_alpn_protocols_http_auto()
.with_env_key_logger()
.expect("with env key logger")
.build()
};
let tcp_service = TcpListener::bind("127.0.0.1:62023")
.await
.expect("bind http+https+socks5+socks5h proxy to 127.0.0.1:62023");
let socks5_acceptor =
Socks5Acceptor::default().with_auth(Socks5Auth::username_password("john", "secret"));
let exec = Executor::graceful(graceful.guard());
let http_service = HttpServer::auto(exec).service(
(
TraceLayer::new_for_http(),
ProxyAuthLayer::new(Basic::new("tom", "clancy")),
UpgradeLayer::new(
MethodMatcher::CONNECT,
service_fn(http_connect_accept),
ConsumeErrLayer::default().into_layer(Forwarder::ctx()),
),
RemoveResponseHeaderLayer::hop_by_hop(),
RemoveRequestHeaderLayer::hop_by_hop(),
)
.into_layer(service_fn(http_plain_proxy)),
);
let tls_acceptor = TlsAcceptorService::new(tls_service_data, http_service.clone(), true);
let auto_tls_acceptor = TlsPeekRouter::new(tls_acceptor).with_fallback(http_service);
let auto_socks5_acceptor =
Socks5PeekRouter::new(socks5_acceptor).with_fallback(auto_tls_acceptor);
graceful.spawn_task_fn(|guard| tcp_service.serve_graceful(guard, auto_socks5_acceptor));
graceful
.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: Clone + Send + Sync + 'static,
{
match ctx
.get_or_try_insert_with_ctx::<RequestContext, _>(|ctx| (ctx, &req).try_into())
.map(|ctx| ctx.authority.clone())
{
Ok(authority) => {
tracing::info!(%authority, "accept CONNECT (lazy): insert proxy target into context");
ctx.insert(ProxyTarget(authority));
}
Err(err) => {
tracing::error!(err = %err, "error extracting authority");
return Err(StatusCode::BAD_REQUEST.into_response());
}
}
tracing::info!(
"proxy secure transport ingress: {:?}",
ctx.get::<SecureTransport>()
);
Ok((StatusCode::OK.into_response(), ctx, req))
}
async fn http_plain_proxy<S>(ctx: Context<S>, req: Request) -> Result<Response, Infallible>
where
S: Clone + Send + Sync + 'static,
{
let client = EasyHttpWebClient::default();
match client.serve(ctx, req).await {
Ok(resp) => {
match resp
.extensions()
.get::<RequestContextExt>()
.and_then(|ext| ext.get::<ClientSocketInfo>())
{
Some(client_socket_info) => tracing::info!(
status = %resp.status(),
local_addr = ?client_socket_info.local_addr(),
server_addr = %client_socket_info.peer_addr(),
"http plain text proxy received response",
),
None => tracing::info!(
status = %resp.status(),
"http plain text proxy received response, IP info unknown",
),
};
Ok(resp)
}
Err(err) => {
tracing::error!(error = %err, "error in client request");
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap())
}
}
}
@ShabbirHasan1 this is about the client side, while your example is about the server side. So this issue is basically about a service which would make use of the other connectors based on the "Protocol" part of its ProxyAddress found in ctx. Or something like that.
@GlenDC My sincerest apologies 🙏, You are absolutely right. I completely misunderstood 🤦 the scope of this issue and focused on the server-side proxy example, when this issue is indeed about client-side auto proxy connectors. Thank you for clarifying this. 😀