Files
hoppscotch/packages/hoppscotch-relay/src/relay.rs

596 lines
20 KiB
Rust

use curl::easy::{Easy, List};
use openssl::{pkcs12::Pkcs12, ssl::SslContextBuilder, x509::X509};
use openssl_sys::SSL_CTX;
use std::time::SystemTime;
use tokio_util::sync::CancellationToken;
use crate::{
error::RelayError,
interop::{
BodyDef, ClientCertDef, FormDataValue, KeyValuePair, RequestWithMetadata,
ResponseWithMetadata,
},
util::get_status_text,
};
pub fn run_request_task(
req: &RequestWithMetadata,
cancel_token: CancellationToken,
) -> Result<ResponseWithMetadata, RelayError> {
log::info!(
"Starting request task: [Method: {}] [URL: {}] [Validate Certs: {}] [Has Body: {}] [Proxy Enabled: {}]",
req.method,
req.endpoint,
req.validate_certs,
req.body.is_some(),
req.proxy.is_some()
);
let mut curl_handle = Easy::new();
log::debug!("Initialized new curl handle with default settings");
match curl_handle.progress(true) {
Ok(_) => log::debug!("Progress tracking enabled for request monitoring"),
Err(err) => {
log::error!(
"Critical failure enabling progress tracking: {}\nError details: {:?}",
err,
err
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match curl_handle.custom_request(&req.method) {
Ok(_) => log::debug!("HTTP method set: {}", req.method),
Err(err) => {
log::error!("Failed to set HTTP method '{}'. Error: {}", req.method, err);
return Err(RelayError::InvalidMethod);
}
}
match curl_handle.url(&req.endpoint) {
Ok(_) => log::debug!("Target URL configured: {}", req.endpoint),
Err(err) => {
log::error!(
"URL configuration failed for '{}'\nError: {}",
req.endpoint,
err
);
return Err(RelayError::InvalidUrl);
}
}
let headers = match get_headers_list(&req) {
Ok(headers) => {
log::debug!("Generated headers list");
headers
}
Err(err) => {
log::error!("Header generation failed:\nError: {:?}", err);
return Err(err);
}
};
match curl_handle.http_headers(headers) {
Ok(_) => log::debug!("Successfully configured request headers"),
Err(err) => {
log::error!("Failed to set HTTP headers: {}", err);
return Err(RelayError::InvalidHeaders);
}
}
if let Err(err) = apply_body_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Request body application failed:\nError: {:?}\nContent-Type: {:?}",
err,
req.headers
.iter()
.find(|h| h.key.to_lowercase() == "content-type")
.map(|h| &h.value)
);
return Err(err);
}
log::debug!("Request body configured successfully");
match curl_handle.ssl_verify_peer(req.validate_certs) {
Ok(_) => log::debug!(
"SSL peer verification setting applied: {}",
req.validate_certs
),
Err(err) => {
log::error!(
"SSL peer verification configuration failed: {}\nRequested setting: {}",
err,
req.validate_certs
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match curl_handle.ssl_verify_host(req.validate_certs) {
Ok(_) => log::debug!(
"SSL host verification setting applied: {}",
req.validate_certs
),
Err(err) => {
log::error!(
"SSL host verification configuration failed: {}\nRequested setting: {}",
err,
req.validate_certs
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
if let Err(err) = apply_client_cert_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Client certificate configuration failed:\nError: {:?}\nCert Info: {:#?}",
err,
req.client_cert.as_ref()
);
return Err(err);
}
log::debug!("Client certificate configuration successful");
if let Err(err) = apply_proxy_config_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Proxy configuration failed:\nError: {:?}\nProxy Info: {:?}",
err,
req.proxy.as_ref()
);
return Err(err);
}
log::debug!("Proxy configuration applied successfully");
let mut response_body = Vec::new();
let mut response_headers = Vec::new();
let (start_time_ms, end_time_ms) = {
let mut transfer = curl_handle.transfer();
log::debug!("Created curl transfer object for request execution");
match transfer.ssl_ctx_function(|ssl_ctx_ptr| {
let cert_list = match get_x509_certs_from_root_cert_bundle_safe(&req) {
Ok(certs) => {
log::debug!("Found {} certificates in root bundle", certs.len());
certs
}
Err(e) => {
log::error!("Failed to load certificates from bundle: {:?}", e);
return Ok(());
}
};
if !cert_list.is_empty() {
let mut ssl_ctx_builder =
unsafe { SslContextBuilder::from_ptr(ssl_ctx_ptr as *mut SSL_CTX) };
let cert_store = ssl_ctx_builder.cert_store_mut();
for (index, cert) in cert_list.iter().enumerate() {
log::debug!(
"Processing certificate {}: Subject: {:?}, Not Before: {:?}, Not After: {:?}",
index,
cert.subject_name(),
cert.not_before(),
cert.not_after()
);
if let Err(e) = cert_store.add_cert(cert.clone()) {
log::warn!(
"Failed to add certificate {} to store\nError: {}\nCert details: {:?}",
index,
e,
cert.subject_name()
);
} else {
log::debug!(
"Successfully added certificate {} to store\nSubject: {:?}",
index,
cert.subject_name()
);
}
}
// SAFETY: We need to prevent Rust from dropping the `SslContextBuilder` because
// the underlying `SSL_CTX` pointer is owned and managed by curl, not us.
// From curl docs: "libcurl does not guarantee the lifetime of the passed in
// object once this callback function has returned"
// and `SslContextBuilder` is just a safe wrapper around curl's `SSL_CTX` from
// `openssl_sys::SSL_CTX`.
// If dropped, Rust would try to free the `SSL_CTX` which curl still needs.
//
// This intentional "leak" is safe because:
// - We're only leaking the thin Rust wrapper
// - Curl manages the actual `SSL_CTX` memory
// - Curl will free the `SSL_CTX` during connection cleanup
//
// See: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html
std::mem::forget(ssl_ctx_builder);
}
Ok(())
}) {
Ok(_) => log::debug!("SSL context function configured successfully"),
Err(err) => {
log::error!("SSL context function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.progress_function(|dltotal, dlnow, ultotal, ulnow| {
let cancelled = cancel_token.is_cancelled();
if cancelled {
log::warn!(
"Request cancelled by user\nDownload: {}/{} bytes\nUpload: {}/{} bytes",
dlnow,
dltotal,
ulnow,
ultotal
);
} else {
log::debug!(
"Progress - Download: {}/{} bytes, Upload: {}/{} bytes",
dlnow,
dltotal,
ulnow,
ultotal
);
}
!cancelled
}) {
Ok(_) => log::debug!("Progress monitoring function configured"),
Err(err) => {
log::error!("Progress function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.header_function(|header| {
let header = String::from_utf8_lossy(header).into_owned();
if let Some((key, value)) = header.split_once(':') {
log::debug!("Received header: [{}] = [{}]", key.trim(), value.trim());
response_headers.push(KeyValuePair {
key: key.trim().to_string(),
value: value.trim().to_string(),
});
} else {
log::debug!("Received header line (no key-value): {}", header.trim());
}
true
}) {
Ok(_) => log::debug!("Header processing function configured"),
Err(err) => {
log::error!("Header function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.write_function(|data| {
let chunk_size = data.len();
response_body.extend_from_slice(data);
log::debug!(
"Received response chunk: {} bytes (Total size so far: {} bytes)",
chunk_size,
response_body.len()
);
Ok(chunk_size)
}) {
Ok(_) => log::debug!("Response body processing function configured"),
Err(err) => {
log::error!("Write function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
let start_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
log::info!(
"Initiating request transfer at timestamp: {}",
start_time_ms
);
if let Err(err) = transfer.perform() {
log::error!(
"Request transfer failed:\nError: {}\nTime elapsed: {}ms",
err,
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
- start_time_ms,
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
let end_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
log::info!(
"Request transfer completed:\nDuration: {}ms",
end_time_ms - start_time_ms,
);
(start_time_ms, end_time_ms)
};
let response_status = match curl_handle.response_code() {
Ok(status) => {
let status = status as u16;
log::info!(
"Response status code: {} ({})",
status,
get_status_text(status)
);
status
}
Err(err) => {
log::error!("Failed to retrieve response code: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
};
let response_status_text = get_status_text(response_status).to_string();
log::info!(
"Request completed successfully:\nStatus: {} ({})\nDuration: {}ms\n\
Response size: {} bytes\nHeaders: {} received\nEndpoint: {}",
response_status,
response_status_text,
end_time_ms - start_time_ms,
response_body.len(),
response_headers.len(),
req.endpoint
);
Ok(ResponseWithMetadata {
status: response_status,
status_text: response_status_text,
headers: response_headers,
data: response_body,
time_start_ms: start_time_ms,
time_end_ms: end_time_ms,
})
}
fn get_headers_list(req: &RequestWithMetadata) -> Result<List, RelayError> {
let mut result = List::new();
for KeyValuePair { key, value } in &req.headers {
result
.append(&format!("{}: {}", key, value))
.map_err(|err| RelayError::RequestRunError(err.description().to_string()))?;
}
Ok(result)
}
fn apply_body_to_curl_handle(
curl_handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
match &req.body {
Some(BodyDef::Text(text)) => {
curl_handle
.post_fields_copy(text.as_bytes())
.map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::FormData(entries)) => {
let mut form = curl::easy::Form::new();
for entry in entries {
let mut part = form.part(&entry.key);
match &entry.value {
FormDataValue::Text(data) => {
part.contents(data.as_bytes());
}
FormDataValue::File {
filename,
data,
mime,
} => {
part.buffer(filename, data.clone()).content_type(mime);
}
};
part.add().map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
curl_handle.httppost(form).map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::URLEncoded(entries)) => {
let data = entries
.iter()
.map(|KeyValuePair { key, value }| {
format!(
"{}={}",
&url_escape::encode_www_form_urlencoded(key),
url_escape::encode_www_form_urlencoded(value)
)
})
.collect::<Vec<String>>()
.join("&");
curl_handle
.post_fields_copy(data.as_bytes())
.map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
None => {}
};
Ok(())
}
fn apply_client_cert_to_curl_handle(
handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
match &req.client_cert {
Some(ClientCertDef::PEMCert {
certificate_pem,
key_pem,
}) => {
handle.ssl_cert_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Type: {}",
err.description()
))
})?;
handle.ssl_cert_blob(certificate_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Blob: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key type: {}",
err.description()
))
})?;
handle.ssl_key_blob(key_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert blob: {}",
err.description()
))
})?;
}
Some(ClientCertDef::PFXCert {
certificate_pfx,
password,
}) => {
let pkcs12 = Pkcs12::from_der(&certificate_pfx).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to parse PFX certificate from DER: {}",
err
))
})?;
let parsed = pkcs12.parse2(password).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to parse PFX certificate with provided password: {}",
err
))
})?;
if let (Some(cert), Some(key)) = (parsed.cert, parsed.pkey) {
let certificate_pem = cert.to_pem().map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to convert PFX certificate to PEM format: {}",
err
))
})?;
let key_pem = key.private_key_to_pem_pkcs8().map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to convert PFX private key to PEM format: {}",
err
))
})?;
handle.ssl_cert_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_cert_blob(&certificate_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Blob for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_blob(&key_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key blob for converted PFX: {}",
err.description()
))
})?;
} else {
return Err(RelayError::RequestRunError(
"PFX certificate parsing succeeded, but either cert or private key is missing"
.to_string(),
));
}
}
None => {}
};
Ok(())
}
fn get_x509_certs_from_root_cert_bundle_safe(
req: &RequestWithMetadata,
) -> Result<Vec<X509>, openssl::error::ErrorStack> {
let mut certs = Vec::new();
for pem_bundle in &req.root_cert_bundle_files {
match openssl::x509::X509::stack_from_pem(pem_bundle) {
Ok(mut bundle_certs) => certs.append(&mut bundle_certs),
Err(e) => {
log::warn!("Failed to parse certificate bundle: {:?}", e);
}
}
}
Ok(certs)
}
fn apply_proxy_config_to_curl_handle(
handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
if let Some(proxy_config) = &req.proxy {
handle
.proxy_auth(curl::easy::Auth::new().auto(true))
.map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to set proxy Auth Mode: {}",
err.description()
))
})?;
handle.proxy(&proxy_config.url).map_err(|err| {
RelayError::RequestRunError(format!("Failed to set proxy URL: {}", err.description()))
})?;
}
Ok(())
}