noxy 0.0.7

HTTP forward and reverse proxy with a pluggable tower middleware pipeline
Documentation
mod common;

use axum::Router;
use axum::routing::get;
use axum_server::tls_rustls::RustlsConfig;
use common::*;
use noxy::http::HttpService;
use noxy::middleware::ModifyHeaders;
use rcgen::{CertificateParams, KeyPair};
use tower::Layer;

#[tokio::test]
async fn modify_headers_sets_request_header() {
    let upstream_addr = start_echo_upstream().await;

    let proxy_addr = start_proxy(vec![Box::new(|inner: HttpService| {
        let layer = ModifyHeaders::new().set_request("x-custom", "injected");
        tower::util::BoxService::new(layer.layer(inner))
    })])
    .await;
    let client = http_client(proxy_addr);

    let resp = client
        .get(format!("https://localhost:{}/", upstream_addr.port()))
        .send()
        .await
        .unwrap();

    let body = resp.text().await.unwrap();
    assert!(
        body.contains("x-custom: injected"),
        "upstream should see x-custom header, got:\n{body}"
    );
}

#[tokio::test]
async fn modify_headers_removes_request_header() {
    let upstream_addr = start_echo_upstream().await;

    let proxy_addr = start_proxy(vec![Box::new(|inner: HttpService| {
        let layer = ModifyHeaders::new().remove_request("x-remove-me");
        tower::util::BoxService::new(layer.layer(inner))
    })])
    .await;
    let client = http_client(proxy_addr);

    let resp = client
        .get(format!("https://localhost:{}/", upstream_addr.port()))
        .header("x-remove-me", "should-be-gone")
        .send()
        .await
        .unwrap();

    let body = resp.text().await.unwrap();
    assert!(
        !body.contains("x-remove-me"),
        "upstream should not see x-remove-me header, got:\n{body}"
    );
}

#[tokio::test]
async fn modify_headers_sets_response_header() {
    let upstream_addr = start_upstream("hello").await;

    let proxy_addr = start_proxy(vec![Box::new(|inner: HttpService| {
        let layer = ModifyHeaders::new().set_response("x-custom", "injected");
        tower::util::BoxService::new(layer.layer(inner))
    })])
    .await;
    let client = http_client(proxy_addr);

    let resp = client
        .get(format!("https://localhost:{}/", upstream_addr.port()))
        .send()
        .await
        .unwrap();

    assert_eq!(
        resp.headers().get("x-custom").unwrap().to_str().unwrap(),
        "injected"
    );
}

#[tokio::test]
async fn modify_headers_removes_response_header() {
    install_crypto_provider();
    let key_pair = KeyPair::generate().unwrap();
    let params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
    let cert = params.self_signed(&key_pair).unwrap();
    let cert_der = cert.der().to_vec();
    let key_der = key_pair.serialized_der().to_vec();

    let config = RustlsConfig::from_der(vec![cert_der], key_der)
        .await
        .unwrap();

    let app = Router::new().route(
        "/",
        get(|| async {
            (
                [(
                    http::header::HeaderName::from_static("x-remove-me"),
                    "present",
                )],
                "hello",
            )
        }),
    );

    let handle = axum_server::Handle::new();
    let listener_handle = handle.clone();

    tokio::spawn(async move {
        axum_server::bind_rustls("127.0.0.1:0".parse().unwrap(), config)
            .handle(handle)
            .serve(app.into_make_service())
            .await
            .unwrap();
    });

    let upstream_addr = listener_handle.listening().await.unwrap();

    let proxy_addr = start_proxy(vec![Box::new(|inner: HttpService| {
        let layer = ModifyHeaders::new().remove_response("x-remove-me");
        tower::util::BoxService::new(layer.layer(inner))
    })])
    .await;
    let client = http_client(proxy_addr);

    let resp = client
        .get(format!("https://localhost:{}/", upstream_addr.port()))
        .send()
        .await
        .unwrap();

    assert!(
        resp.headers().get("x-remove-me").is_none(),
        "x-remove-me should have been removed"
    );
    assert_eq!(resp.text().await.unwrap(), "hello");
}