use crate::egress::{EgressError, EgressRequest, EgressRequestKind, EgressService, EgressSigning};
use crate::network_access::NetworkAccessList;
use async_trait::async_trait;
use bashkit::{HttpResponse, HttpTransport, HttpTransportError, HttpTransportRequest};
use std::net::SocketAddr;
use std::sync::Arc;
pub(crate) struct BashkitEgressTransport {
egress: Arc<dyn EgressService>,
network_access: Option<NetworkAccessList>,
}
impl BashkitEgressTransport {
pub(crate) fn new(
egress: Arc<dyn EgressService>,
network_access: Option<NetworkAccessList>,
) -> Self {
Self {
egress,
network_access,
}
}
}
#[async_trait]
impl HttpTransport for BashkitEgressTransport {
async fn execute(
&self,
request: HttpTransportRequest,
) -> Result<HttpResponse, HttpTransportError> {
let url = url::Url::parse(&request.url)
.map_err(|e| HttpTransportError::Transport(format!("invalid URL: {e}")))?;
let host = url.host_str().unwrap_or_default().to_string();
let port = url.port_or_known_default().unwrap_or(0);
let pinned: Vec<SocketAddr> = request
.pinned_addrs
.iter()
.map(|ip| SocketAddr::new(*ip, port))
.collect();
let mut egress_request = EgressRequest::new(
request.method.as_str(),
&request.url,
EgressRequestKind::Capability,
)
.signing(EgressSigning::PlatformDefault)
.network_access(self.network_access.clone())
.pinned_addrs(host, pinned)
.timeout_ms(u64::try_from(request.timeout.as_millis()).unwrap_or(u64::MAX));
for (name, value) in &request.headers {
egress_request = egress_request.header(name, value);
}
if let Some(body) = request.body {
egress_request = egress_request.body(body);
}
let response = self
.egress
.send(egress_request)
.await
.map_err(map_egress_error)?;
if response.body.len() > request.max_response_bytes {
return Err(HttpTransportError::TooLarge(format!(
"{} bytes (max: {} bytes)",
response.body.len(),
request.max_response_bytes
)));
}
Ok(HttpResponse {
status: response.status,
headers: response.headers.into_iter().collect(),
body: response.body,
})
}
}
fn map_egress_error(error: EgressError) -> HttpTransportError {
match error {
EgressError::NetworkAccessDenied { url } => {
HttpTransportError::Denied(format!("outbound request blocked by network policy: {url}"))
}
EgressError::InvalidRequest(message) => HttpTransportError::Transport(message),
EgressError::SigningUnavailable => {
HttpTransportError::Transport("outbound request signing unavailable".to_string())
}
EgressError::Transport(message) => {
if message.contains("timed out") || message.contains("timeout") {
HttpTransportError::Timeout
} else {
HttpTransportError::Transport(message)
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::egress::{EgressResponse, EgressResult, EgressStreamResponse};
use std::sync::Mutex;
pub(crate) struct MockEgress {
responses: Mutex<Vec<EgressResult<EgressResponse>>>,
pub(crate) requests: Mutex<Vec<EgressRequest>>,
}
impl MockEgress {
pub(crate) fn with_responses(responses: Vec<EgressResult<EgressResponse>>) -> Self {
Self {
responses: Mutex::new(responses),
requests: Mutex::new(Vec::new()),
}
}
pub(crate) fn ok(
status: u16,
headers: &[(&str, &str)],
body: &str,
) -> EgressResult<EgressResponse> {
Ok(EgressResponse {
status,
headers: headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
body: body.as_bytes().to_vec(),
})
}
}
#[async_trait]
impl EgressService for MockEgress {
async fn send(&self, request: EgressRequest) -> EgressResult<EgressResponse> {
self.requests.lock().unwrap().push(request);
let mut responses = self.responses.lock().unwrap();
assert!(!responses.is_empty(), "MockEgress ran out of responses");
responses.remove(0)
}
async fn send_stream(&self, request: EgressRequest) -> EgressResult<EgressStreamResponse> {
let response = self.send(request).await?;
Ok(EgressStreamResponse {
status: response.status,
headers: response.headers,
body: Box::pin(futures::stream::once(async move { Ok(response.body) })),
})
}
}
#[test]
fn maps_network_denial_to_denied() {
let err = map_egress_error(EgressError::NetworkAccessDenied {
url: "http://93.184.216.34/blocked".to_string(),
});
assert_eq!(
err,
HttpTransportError::Denied(
"outbound request blocked by network policy: http://93.184.216.34/blocked"
.to_string()
)
);
}
#[test]
fn maps_transport_timeout_to_timeout() {
let err = map_egress_error(EgressError::Transport(
"error sending request: operation timed out".to_string(),
));
assert_eq!(err, HttpTransportError::Timeout);
}
#[test]
fn maps_other_errors_to_transport() {
let err = map_egress_error(EgressError::Transport("connection refused".to_string()));
assert_eq!(
err,
HttpTransportError::Transport("connection refused".to_string())
);
let err = map_egress_error(EgressError::SigningUnavailable);
assert!(matches!(err, HttpTransportError::Transport(_)));
}
}