strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use reqwest::ClientBuilder;

use crate::args::{HttpVersion, TesterArgs, TlsVersion};
use crate::error::{AppError, AppResult, ValidationError};

#[derive(Debug, Clone, Copy)]
pub(crate) enum AlpnChoice {
    Default,
    Http1Only,
    Http2Only,
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct AlpnSelection {
    pub(crate) choice: AlpnChoice,
    pub(crate) has_h3: bool,
}

pub(super) fn apply_tls_settings(
    mut builder: ClientBuilder,
    args: &TesterArgs,
) -> AppResult<ClientBuilder> {
    if let (Some(min), Some(max)) = (args.tls_min, args.tls_max)
        && tls_version_rank(min) > tls_version_rank(max)
    {
        return Err(AppError::validation(ValidationError::TlsMinGreaterThanMax));
    }

    if let Some(min) = args.tls_min {
        builder = builder.min_tls_version(to_reqwest_tls_version(min));
    }
    if let Some(max) = args.tls_max {
        builder = builder.max_tls_version(to_reqwest_tls_version(max));
    }

    let alpn = resolve_alpn(&args.alpn)?;

    if let Some(version) = args.http_version {
        #[cfg(feature = "http3")]
        {
            builder = apply_explicit_http_version(builder, version);
            return Ok(builder);
        }
        #[cfg(not(feature = "http3"))]
        {
            builder = apply_explicit_http_version(builder, version)?;
            return Ok(builder);
        }
    }

    if alpn.has_h3 && !args.http3 {
        return Err(AppError::validation(ValidationError::AlpnH3WithoutHttp3));
    }
    if args.http2 && args.http3 {
        return Err(AppError::validation(ValidationError::Http2Http3Conflict));
    }
    if args.http3 {
        if matches!(alpn.choice, AlpnChoice::Http1Only) {
            return Err(AppError::validation(
                ValidationError::Http3WithHttp1OnlyAlpn,
            ));
        }
        if matches!(alpn.choice, AlpnChoice::Http2Only) && !alpn.has_h3 {
            return Err(AppError::validation(ValidationError::Http3WithH2OnlyAlpn));
        }
        #[cfg(feature = "http3")]
        {
            builder = builder.http3_prior_knowledge();
            return Ok(builder);
        }
        #[cfg(not(feature = "http3"))]
        {
            return Err(AppError::validation(ValidationError::Http3NotEnabled));
        }
    }
    if args.http2 && matches!(alpn.choice, AlpnChoice::Http1Only) {
        return Err(AppError::validation(
            ValidationError::Http2WithHttp1OnlyAlpn,
        ));
    }

    builder = match alpn.choice {
        AlpnChoice::Http1Only => builder.http1_only(),
        AlpnChoice::Http2Only => builder.http2_prior_knowledge(),
        AlpnChoice::Default => {
            if args.http2 {
                builder.http2_prior_knowledge()
            } else {
                builder
            }
        }
    };

    Ok(builder)
}

#[cfg(feature = "http3")]
fn apply_explicit_http_version(mut builder: ClientBuilder, version: HttpVersion) -> ClientBuilder {
    match version {
        HttpVersion::V0_9 | HttpVersion::V1_0 | HttpVersion::V1_1 => {
            builder = builder.http1_only();
        }
        HttpVersion::V2 => {
            builder = builder.http2_prior_knowledge();
        }
        HttpVersion::V3 => {
            builder = builder.http3_prior_knowledge();
        }
    }
    builder
}

#[cfg(not(feature = "http3"))]
fn apply_explicit_http_version(
    mut builder: ClientBuilder,
    version: HttpVersion,
) -> AppResult<ClientBuilder> {
    match version {
        HttpVersion::V0_9 | HttpVersion::V1_0 | HttpVersion::V1_1 => {
            builder = builder.http1_only();
        }
        HttpVersion::V2 => {
            builder = builder.http2_prior_knowledge();
        }
        HttpVersion::V3 => {
            return Err(AppError::validation(ValidationError::Http3NotEnabled));
        }
    }
    Ok(builder)
}

const fn to_reqwest_tls_version(version: TlsVersion) -> reqwest::tls::Version {
    match version {
        TlsVersion::V1_0 => reqwest::tls::Version::TLS_1_0,
        TlsVersion::V1_1 => reqwest::tls::Version::TLS_1_1,
        TlsVersion::V1_2 => reqwest::tls::Version::TLS_1_2,
        TlsVersion::V1_3 => reqwest::tls::Version::TLS_1_3,
    }
}

const fn tls_version_rank(version: TlsVersion) -> u8 {
    match version {
        TlsVersion::V1_0 => 0,
        TlsVersion::V1_1 => 1,
        TlsVersion::V1_2 => 2,
        TlsVersion::V1_3 => 3,
    }
}

pub(crate) fn resolve_alpn(alpn: &[String]) -> AppResult<AlpnSelection> {
    if alpn.is_empty() {
        return Ok(AlpnSelection {
            choice: AlpnChoice::Default,
            has_h3: false,
        });
    }

    let mut has_h2 = false;
    let mut has_http1 = false;
    let mut has_h3 = false;
    for proto in alpn {
        let normalized = proto.trim().to_ascii_lowercase();
        match normalized.as_str() {
            "h2" | "http2" | "http/2" => has_h2 = true,
            "http/1.1" | "http1" | "http/1" => has_http1 = true,
            "h3" | "http3" | "http/3" => has_h3 = true,
            other if other.starts_with("h3-") => {
                has_h3 = true;
            }
            "" => {}
            _ => {
                return Err(AppError::validation(
                    ValidationError::UnsupportedAlpnProtocol {
                        protocol: proto.to_owned(),
                    },
                ));
            }
        }
    }

    let choice = match (has_h2, has_http1) {
        (true, true) => AlpnChoice::Default,
        (true, false) => AlpnChoice::Http2Only,
        (false, true) => AlpnChoice::Http1Only,
        (false, false) => AlpnChoice::Default,
    };
    Ok(AlpnSelection { choice, has_h3 })
}