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}"))
}
}
}
}
const DEFAULT_HTTP_REQUEST_TIMEOUT_MS: u64 = 30_000;
static HTTP_REQUEST_TIMEOUT: std::sync::LazyLock<std::time::Duration> =
std::sync::LazyLock::new(|| {
let ms = std::env::var("SEQ_HTTP_REQUEST_TIMEOUT_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_HTTP_REQUEST_TIMEOUT_MS);
std::time::Duration::from_millis(ms)
});
#[cfg(test)]
static HTTP_REQUEST_TIMEOUT_OVERRIDE: std::sync::Mutex<Option<std::time::Duration>> =
std::sync::Mutex::new(None);
#[cfg(test)]
pub(crate) fn set_test_http_request_timeout(dur: Option<std::time::Duration>) {
*HTTP_REQUEST_TIMEOUT_OVERRIDE.lock().unwrap() = dur;
}
fn http_request_timeout() -> std::time::Duration {
#[cfg(test)]
if let Some(dur) = *HTTP_REQUEST_TIMEOUT_OVERRIDE.lock().unwrap() {
return dur;
}
*HTTP_REQUEST_TIMEOUT
}
fn run_once(
mut stream: Conn,
method: &str,
target: &ssrf::ValidatedTarget,
body: Option<(&str, &[u8])>,
) -> Result<(wire::Response, Conn), String> {
let timeout = Some(http_request_timeout());
let _ = stream.set_read_timeout(timeout);
let _ = stream.set_write_timeout(timeout);
let result = (|| {
wire::write_request(&mut stream, method, target, body)
.map_err(|e| format!("write request: {e}"))?;
wire::read_response(&mut stream)
})();
let _ = stream.set_read_timeout(None);
let _ = stream.set_write_timeout(None);
let resp = result?;
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")
}