use crate::{ReplayProxy, Upstream};
use amberfork_record::{Body, CapturedRequest, CapturedResponse, Cassette, retain_request_headers};
use axum::Router;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use std::fmt;
use std::io;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
type Shared<U> = Arc<Mutex<ReplayProxy<U>>>;
#[derive(Debug)]
#[non_exhaustive]
pub enum ReplayError {
Bind {
addr: SocketAddr,
source: io::Error,
},
Serve(io::Error),
}
impl fmt::Display for ReplayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bind { addr, source } => write!(f, "cannot bind {addr}: {source}"),
Self::Serve(source) => write!(f, "replay listener failed: {source}"),
}
}
}
impl std::error::Error for ReplayError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Bind { source, .. } | Self::Serve(source) => Some(source),
}
}
}
pub struct ReplayServer<U> {
addr: SocketAddr,
proxy: Shared<U>,
}
impl<U: Upstream + 'static> ReplayServer<U> {
pub async fn bind(
cassette: Cassette,
upstream: U,
reexecuted_id: impl Into<String>,
) -> Result<Self, ReplayError> {
let proxy: Shared<U> = Arc::new(Mutex::new(ReplayProxy::new(
cassette,
upstream,
reexecuted_id,
)));
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0));
let listener = TcpListener::bind(addr)
.await
.map_err(|source| ReplayError::Bind { addr, source })?;
let addr = listener.local_addr().map_err(ReplayError::Serve)?;
let app = Router::new()
.fallback(answer::<U>)
.with_state(Arc::clone(&proxy));
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
Ok(Self { addr, proxy })
}
#[must_use]
pub fn base_url(&self) -> String {
format!("http://{}", self.addr)
}
#[must_use]
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub async fn reexecuted(&self) -> Cassette {
self.proxy.lock().await.reexecuted().clone()
}
}
async fn answer<U: Upstream + 'static>(
State(proxy): State<Shared<U>>,
request: Request,
) -> Response {
let method = request.method().to_string();
let path = request
.uri()
.path_and_query()
.map_or_else(|| request.uri().path().to_string(), ToString::to_string);
let (parts, body) = request.into_parts();
let body = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes,
Err(err) => {
return (
StatusCode::BAD_REQUEST,
format!("cannot read request body: {err}"),
)
.into_response();
}
};
let captured = CapturedRequest {
method,
path,
headers: retain_request_headers(
parts
.headers
.iter()
.map(|(n, v)| (n.as_str(), v.as_bytes())),
),
body: Body::from_bytes(&body),
};
let answered = { proxy.lock().await.answer(captured).await };
match answered {
Ok(response) => into_http(response),
Err(err) => (StatusCode::BAD_GATEWAY, format!("live relay failed: {err}")).into_response(),
}
}
fn into_http(response: CapturedResponse) -> Response {
let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::BAD_GATEWAY);
let (bytes, default_content_type) = match response.body {
Body::Json(value) => (
serde_json::to_vec(&value).unwrap_or_default(),
"application/json",
),
Body::Text(text) => (text.into_bytes(), "text/plain; charset=utf-8"),
};
let mut builder = Response::builder().status(status);
let mut has_content_type = false;
for (name, value) in &response.headers {
if name.eq_ignore_ascii_case("content-type") {
has_content_type = true;
}
builder = builder.header(name, value);
}
if !has_content_type {
builder = builder.header("content-type", default_content_type);
}
builder
.body(axum::body::Body::from(bytes))
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}