use super::conn::Conn;
use super::pool::{self, PoolKey};
use super::ssrf::{self, Scheme};
use super::wire;
use super::{build_response_map, error_response};
use crate::value::Value;
pub(crate) fn perform_request(method: &str, url: &str, body: Option<(&str, &[u8])>) -> Value {
let target = match ssrf::validate_url(url) {
Ok(t) => t,
Err(msg) => return error_response(msg),
};
if target.addrs.is_empty() {
return error_response(format!("DNS resolution failed for {}", target.host));
}
perform_validated(method, target, body)
}
#[cfg(test)]
pub(crate) fn perform_request_with_target(
method: &str,
target: ssrf::ValidatedTarget,
body: Option<(&str, &[u8])>,
) -> Value {
perform_validated(method, target, body)
}
fn perform_validated(
method: &str,
target: ssrf::ValidatedTarget,
body: Option<(&str, &[u8])>,
) -> Value {
let key = PoolKey {
scheme: target.scheme,
host: target.host.clone(),
port: target.port,
};
let pooled = pool::checkout(&key);
let from_pool = pooled.is_some();
let stream: Conn = match pooled {
Some(s) => s,
None => match dial_fresh(&target) {
Ok(s) => s,
Err(msg) => return error_response(msg),
},
};
match run_once(stream, method, &target, body) {
Ok((resp, stream)) => {
pool::release(key, stream, resp.keep_alive);
let ok = (200..300).contains(&resp.status);
build_response_map(resp.status as i64, resp.body, ok, None)
}
Err(msg) => {
if from_pool && is_idempotent(method) {
match dial_fresh(&target) {
Ok(stream) => match run_once(stream, method, &target, body) {
Ok((resp, stream)) => {
pool::release(key, stream, resp.keep_alive);
let ok = (200..300).contains(&resp.status);
build_response_map(resp.status as i64, resp.body, ok, None)
}
Err(msg) => error_response(format!("Connection error: {msg}")),
},
Err(msg) => error_response(msg),
}
} else {
error_response(format!("Connection error: {msg}"))
}
}
}
}
fn run_once(
mut stream: Conn,
method: &str,
target: &ssrf::ValidatedTarget,
body: Option<(&str, &[u8])>,
) -> Result<(wire::Response, Conn), String> {
wire::write_request(&mut stream, method, target, body)
.map_err(|e| format!("write request: {e}"))?;
let resp = wire::read_response(&mut stream)?;
Ok((resp, stream))
}
fn dial_fresh(target: &ssrf::ValidatedTarget) -> Result<Conn, String> {
let tcp = crate::tcp::connect_to_addrs(&target.addrs, target.port).ok_or_else(|| {
format!(
"Connection error: all {} addresses for {} unreachable",
target.addrs.len(),
target.host
)
})?;
match target.scheme {
Scheme::Http => Ok(Box::new(tcp) as Conn),
Scheme::Https => crate::tls::dial_tls(tcp, target.host.clone())
.map_err(|()| "TLS handshake failed".to_string()),
}
}
fn is_idempotent(method: &str) -> bool {
matches!(method, "GET" | "PUT" | "DELETE")
}