use alloc::string::String;
use tokio::sync::watch;
use ts_control_serde::PingType;
use ts_http_util::{BytesBody, ClientExt, Http2, Request};
use url::Url;
use crate::StateUpdate;
const C2N_PATH_ECHO: &str = "/echo";
const C2N_PATH_UNKNOWN: &str = "HTTP/1.1 400 Bad Request\r\n\r\nunknown c2n path";
const C2N_RESPONSE_ECHO_PREAMBLE: &str = "HTTP/1.1 200 OK\r\n\r\n";
#[derive(Debug, thiserror::Error)]
pub enum PingError {
#[error(transparent)]
Http(#[from] ts_http_util::Error),
#[error(transparent)]
JoinFailed(#[from] tokio::task::JoinError),
#[error("C2N ping request is missing payload")]
MissingPayload,
#[error(transparent)]
WatchRecv(#[from] watch::error::RecvError),
}
fn parse_c2n_ping(payload: &str) -> Result<Request<String>, PingError> {
let req = ts_http_util::http1::parse_request(payload.as_bytes()).map_err(PingError::Http)?;
tracing::trace!(
payload_len = req.body().len(),
payload = req.body(),
"extracted payload from ping request body"
);
Ok(req)
}
pub async fn handle_ping(
state: &StateUpdate,
control_url: &Url,
http2_client: &Http2<BytesBody>,
) -> Result<(), PingError> {
let Some(ping_request) = &state.ping else {
return Ok(());
};
tracing::trace!(request = ?ping_request, "handling ping request");
for typ in &ping_request.types {
if typ != &PingType::C2N {
tracing::warn!(ping_type = ?typ, "ignoring unsupported ping type");
continue;
}
let ping_request_body = ping_request
.payload
.as_ref()
.ok_or(PingError::MissingPayload)?;
let c2n_request = match parse_c2n_ping(ping_request_body) {
Ok(c2n_request) => {
tracing::trace!(?c2n_request, "parsed c2n ping");
c2n_request
}
Err(_) => {
tracing::warn!(?ping_request_body, "ignoring malformed c2n ping");
continue;
}
};
let c2n_request_path = c2n_request.uri().path();
let c2n_response = match c2n_request_path {
C2N_PATH_ECHO => {
tracing::trace!(c2n_request_path, "handling c2n echo");
format!("{}{}", C2N_RESPONSE_ECHO_PREAMBLE, c2n_request.body())
}
_ => {
tracing::debug!(c2n_request_path, "no handler for c2n path");
C2N_PATH_UNKNOWN.to_string()
}
};
let ping_response_url = control_url
.join(ping_request.url.path())
.map_err(|_| PingError::MissingPayload)?;
tracing::trace!(%ping_response_url, ?c2n_response, "posting c2n response");
let response = http2_client
.post(&ping_response_url, None, c2n_response.into())
.await?;
if !response.status().is_success() {
tracing::error!(status = %response.status(), "responding to c2n ping");
} else {
tracing::debug!("c2n response sent");
}
}
Ok(())
}