feat: openssl based hoppscotch-relay for request forwarding (#4442)
This commit is contained in:
595
packages/hoppscotch-relay/src/relay.rs
Normal file
595
packages/hoppscotch-relay/src/relay.rs
Normal file
@@ -0,0 +1,595 @@
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user