use std::path::{Path, PathBuf};
use std::time::Duration;
use reqwest::{Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
use trusty_common::uds::server::{JSONRPC_VERSION, RpcResponse};
use super::DaemonClient;
use super::config;
use super::socket_routes;
#[cfg(test)]
#[path = "transport_tests.rs"]
mod transport_tests;
#[derive(Debug, thiserror::Error)]
pub enum DaemonCallError {
#[error("trusty-mpm daemon unreachable over {target}: {source}")]
Unreachable {
target: String,
#[source]
source: anyhow::Error,
},
#[error("trusty-mpm daemon call over {target} failed: {source}")]
Failed {
target: String,
#[source]
source: anyhow::Error,
},
}
impl DaemonCallError {
pub fn is_connect(&self) -> bool {
matches!(self, Self::Unreachable { .. })
}
pub fn denied_os_error(&self) -> Option<&std::io::Error> {
let (Self::Unreachable { source, .. } | Self::Failed { source, .. }) = self;
source
.chain()
.filter_map(|e| e.downcast_ref::<std::io::Error>())
.find(|io| io.kind() == std::io::ErrorKind::PermissionDenied)
}
pub fn is_timeout(&self) -> bool {
let Self::Failed { source, .. } = self else {
return false;
};
source
.downcast_ref::<reqwest::Error>()
.is_some_and(reqwest::Error::is_timeout)
|| matches!(
source.downcast_ref::<trusty_common::uds::UdsRpcError>(),
Some(trusty_common::uds::UdsRpcError::Timeout { .. })
)
}
}
#[must_use = "a DaemonRequest does nothing until `send` is awaited"]
pub struct DaemonRequest {
inner: Pending,
}
enum Pending {
Http {
target: String,
builder: reqwest::RequestBuilder,
},
Socket(SocketCall),
}
struct SocketCall {
socket: PathBuf,
method: Method,
path: String,
params: Map<String, Value>,
raw_body: Option<Value>,
timeout: Duration,
error: Option<anyhow::Error>,
}
impl DaemonRequest {
pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
self.map(|b| b.query(query), |call| call.merge(query, "query"))
}
pub fn json<T: Serialize + ?Sized>(self, body: &T) -> Self {
self.map(
|b| b.json(body),
|call| match serde_json::to_value(body) {
Ok(Value::Object(map)) => call.params.extend(map),
Ok(other) => call.raw_body = Some(other),
Err(e) => call.error = Some(anyhow::anyhow!("encode request body: {e}")),
},
)
}
pub fn timeout(self, timeout: Duration) -> Self {
self.map(|b| b.timeout(timeout), |call| call.timeout = timeout)
}
fn map(
mut self,
http: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
socket: impl FnOnce(&mut SocketCall),
) -> Self {
self.inner = match self.inner {
Pending::Http { target, builder } => Pending::Http {
target,
builder: http(builder),
},
Pending::Socket(mut call) => {
socket(&mut call);
Pending::Socket(call)
}
};
self
}
pub async fn send(self) -> Result<DaemonResponse, DaemonCallError> {
match self.inner {
Pending::Http { target, builder } => send_http(target, builder).await,
Pending::Socket(call) => call.send().await,
}
}
}
async fn send_http(
target: String,
builder: reqwest::RequestBuilder,
) -> Result<DaemonResponse, DaemonCallError> {
let classify = |e: reqwest::Error| {
if e.is_connect() {
DaemonCallError::Unreachable {
target: target.clone(),
source: e.into(),
}
} else {
DaemonCallError::Failed {
target: target.clone(),
source: e.into(),
}
}
};
let resp = builder.send().await.map_err(classify)?;
let status = resp.status();
let resume_reason = resp
.headers()
.get(RESUME_REASON_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let body = resp.bytes().await.map_err(classify)?.to_vec();
Ok(DaemonResponse {
status,
body,
resume_reason,
})
}
impl SocketCall {
fn merge<T: Serialize + ?Sized>(&mut self, value: &T, what: &str) {
match serde_json::to_value(value) {
Ok(Value::Object(map)) => self.params.extend(map),
Ok(Value::Array(pairs)) => {
for pair in pairs {
match pair {
Value::Array(kv) if matches!(kv.as_slice(), [Value::String(_), _]) => {
if let [Value::String(k), v] = kv.as_slice() {
self.params.insert(k.clone(), v.clone());
}
}
other => {
self.error = Some(anyhow::anyhow!(
"{what} entry {other} is not a (string key, value) pair"
));
return;
}
}
}
}
Ok(Value::Null) => {}
Ok(other) => self.error = Some(anyhow::anyhow!("unsupported {what} shape: {other}")),
Err(e) => self.error = Some(anyhow::anyhow!("encode {what}: {e}")),
}
}
async fn send(mut self) -> Result<DaemonResponse, DaemonCallError> {
let target = self.socket.display().to_string();
let failed = |source: anyhow::Error| DaemonCallError::Failed {
target: target.clone(),
source,
};
if let Some(e) = self.error.take() {
return Err(failed(e));
}
let Some((rpc_method, captures)) = socket_routes::resolve(&self.method, &self.path) else {
return Err(failed(anyhow::anyhow!(
"`{} {}` has no method on the daemon socket yet (#6288); \
this tm does not fall back to TCP",
self.method,
self.path
)));
};
self.params.extend(captures);
let params = match self.raw_body.take() {
Some(raw) if self.params.is_empty() => raw,
_ => Value::Object(self.params),
};
let frame = serde_json::json!({
"jsonrpc": JSONRPC_VERSION,
"id": 1,
"method": rpc_method,
"params": params,
});
let response: RpcResponse =
trusty_common::uds::send_framed_request(&self.socket, &frame, self.timeout)
.await
.map_err(|e| {
if e.is_dial_failure() {
DaemonCallError::Unreachable {
target: target.clone(),
source: e.into(),
}
} else {
failed(e.into())
}
})?;
match (response.result, response.error) {
(Some(result), _) => Ok(DaemonResponse {
status: StatusCode::OK,
body: serde_json::to_vec(&result).map_err(|e| failed(e.into()))?,
resume_reason: None,
}),
(None, Some(e)) if e.code == socket_routes::CODE_METHOD_NOT_FOUND => {
Err(failed(anyhow::anyhow!(
"the daemon does not serve `{rpc_method}` — it predates this tm; \
restart it (`tm restart`) ({})",
e.message
)))
}
(None, Some(e)) => Ok(DaemonResponse {
status: socket_routes::status_for_code(e.code),
resume_reason: socket_routes::resume_reason_for_code(e.code).map(str::to_owned),
body: e.message.into_bytes(),
}),
(None, None) => Err(failed(anyhow::anyhow!(
"`{rpc_method}` answered with neither a result nor an error"
))),
}
}
}
#[derive(Debug)]
pub struct DaemonResponse {
status: StatusCode,
body: Vec<u8>,
resume_reason: Option<String>,
}
const RESUME_REASON_HEADER: &str = "x-trusty-resume-reason";
impl DaemonResponse {
pub fn resume_reason(&self) -> Option<&str> {
self.resume_reason.as_deref()
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn error_for_status(self) -> anyhow::Result<Self> {
if self.status.is_success() {
return Ok(self);
}
let text = String::from_utf8_lossy(&self.body).trim().to_string();
if text.is_empty() {
anyhow::bail!("{}", self.status)
}
anyhow::bail!("{}: {text}", self.status)
}
pub async fn json<T: DeserializeOwned>(self) -> anyhow::Result<T> {
Ok(serde_json::from_slice(&self.body)?)
}
pub async fn text(self) -> anyhow::Result<String> {
Ok(String::from_utf8_lossy(&self.body).into_owned())
}
}
impl DaemonClient {
pub fn over_socket(socket: impl Into<PathBuf>) -> Self {
let socket = socket.into();
Self {
base: format!("unix:{}", socket.display()),
http: config::default_client(),
home: None,
socket: Some(socket),
base_url_pinned: false,
}
}
pub fn from_resolved_socket() -> anyhow::Result<Self> {
Ok(Self::over_socket(resolve_daemon_socket()?))
}
pub fn socket_path(&self) -> Option<&Path> {
self.socket.as_deref()
}
pub fn transport_label(&self) -> String {
match &self.socket {
Some(path) => format!("unix socket {}", path.display()),
None => format!("http {}", self.base),
}
}
pub fn http(&self) -> &reqwest::Client {
&self.http
}
pub fn request(&self, method: Method, path: impl Into<String>) -> DaemonRequest {
let path = path.into();
let inner = match &self.socket {
Some(socket) => Pending::Socket(SocketCall {
socket: socket.clone(),
method,
path,
params: Map::new(),
raw_body: None,
timeout: config::DEFAULT_REQUEST_TIMEOUT,
error: None,
}),
None => Pending::Http {
target: self.base.clone(),
builder: self.http.request(method, format!("{}{path}", self.base)),
},
};
DaemonRequest { inner }
}
pub fn get(&self, path: impl Into<String>) -> DaemonRequest {
self.request(Method::GET, path)
}
pub fn post(&self, path: impl Into<String>) -> DaemonRequest {
self.request(Method::POST, path)
}
pub fn patch(&self, path: impl Into<String>) -> DaemonRequest {
self.request(Method::PATCH, path)
}
pub fn delete(&self, path: impl Into<String>) -> DaemonRequest {
self.request(Method::DELETE, path)
}
}
pub fn resolve_daemon_socket() -> anyhow::Result<PathBuf> {
if let Ok(raw) = std::env::var(trusty_common::mpm_rpc::TRUSTY_MPM_SOCKET_ENV) {
let trimmed = raw.trim();
if !trimmed.is_empty() {
return Ok(PathBuf::from(trimmed));
}
}
trusty_common::daemon_socket_path("trusty-mpm")
}