rama 0.3.0

modular service framework
Documentation
#![expect(
    clippy::expect_used,
    reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]

use std::time::Duration;

use crate::examples::example_tests::utils::ExampleRunner;

use super::utils;

use rama::{
    Layer,
    http::{
        BodyExtractExt, layer::error_handling::ErrorHandlerLayer, server::HttpServer,
        service::web::Router,
    },
    layer::ArcLayer,
    net::{
        Protocol,
        address::{ProxyAddress, SocketAddress},
        user::{Basic, ProxyCredential},
    },
    rt::Executor,
    tcp::server::TcpListener,
    telemetry::tracing,
    tls::boring::server::TlsAcceptorService,
    tls::server::{SelfSignedData, TlsServerConfig},
    utils::str::non_empty_str,
};

#[tokio::test]
#[ignore]
async fn test_socks5_connect_proxy_mitm_proxy() {
    utils::init_tracing();

    let runner = utils::ExampleRunner::interactive(
        "socks5_connect_proxy_mitm_proxy",
        Some("socks5,boring,dns"),
    );

    // wait for example to run... this is dirty
    tokio::time::sleep(Duration::from_secs(10)).await;

    let http_socket_addr = spawn_http_server().await;
    let https_socket_addr = spawn_https_server().await;

    test_http_client_over_socks5_proxy_connect_with_mitm_cap(
        http_socket_addr,
        https_socket_addr,
        runner,
    )
    .await;
}

async fn test_http_client_over_socks5_proxy_connect_with_mitm_cap(
    http_socket_addr: SocketAddress,
    https_socket_addr: SocketAddress,
    runner: ExampleRunner,
) {
    let proxy_socket_addr = SocketAddress::local_ipv4(62022);

    tracing::info!(
        "local servers up and running (proxy = {}; http = {}; https = {})",
        proxy_socket_addr,
        http_socket_addr,
        https_socket_addr,
    );

    let proxy_address = ProxyAddress {
        protocol: Some(Protocol::SOCKS5),
        address: proxy_socket_addr.into(),
        credential: Some(ProxyCredential::Basic(Basic::new(
            non_empty_str!("john"),
            non_empty_str!("secret"),
        ))),
    };

    let test_uris = [
        format!("http://{http_socket_addr}/ping"),
        format!("https://{https_socket_addr}/ping"),
    ];

    for uri in test_uris {
        tracing::info!(
            url.full = %uri,
            "try to establish proxied connection over SOCKS5 MITM Proxy",
        );

        tracing::info!(
            url.full = %uri,
            "try to make GET http(s) request and try to receive response text",
        );

        let resp = runner
            .get(uri)
            .extension(proxy_address.clone())
            .send()
            .await
            .expect("make http(s) request via socks5 proxy")
            .try_into_string()
            .await
            .expect("get response text");

        assert_eq!("pong", resp);
        tracing::info!("ping-pong succeeded");
    }

    tracing::info!("bye now!");
}

async fn spawn_http_server() -> SocketAddress {
    let tcp_service =
        TcpListener::bind_address(SocketAddress::default_ipv4(63009), Executor::default())
            .await
            .expect("bind HTTP server on open port");

    let bind_addr = tcp_service
        .local_addr()
        .expect("get bind address of http server")
        .into();

    let app = Router::new().with_get("/ping", "pong");
    let server = HttpServer::auto(Executor::default())
        .service((ArcLayer::new(), ErrorHandlerLayer::new()).into_layer(app));

    tokio::spawn(tcp_service.serve(server));

    bind_addr
}

async fn spawn_https_server() -> SocketAddress {
    let tcp_service =
        TcpListener::bind_address(SocketAddress::default_ipv4(63010), Executor::default())
            .await
            .expect("bind HTTP server on open port");

    let bind_addr = tcp_service
        .local_addr()
        .expect("get bind address of http server")
        .into();

    let app = Router::new().with_get("/ping", "pong");
    let http_server = HttpServer::auto(Executor::default())
        .service((ArcLayer::new(), ErrorHandlerLayer::new()).into_layer(app));

    let config = TlsServerConfig::new()
        .try_with_self_signed(SelfSignedData {
            organisation_name: Some("Socks5 Https Test Server".to_owned()),
            ..Default::default()
        })
        .expect("self-signed")
        .with_alpn_http_auto();
    let https_server = TlsAcceptorService::new(config, http_server, false);

    tokio::spawn(tcp_service.serve(https_server));

    bind_addr
}