use crate::error::{ConductorApiError, ConductorApiResult};
use crate::util::AbortOnDropHandle;
use holo_hash::{ActionHash, DnaHash};
use holochain_conductor_api::{
AdminInterfaceConfig, AdminRequest, AdminResponse, AppAuthenticationToken,
AppAuthenticationTokenIssued, AppInfo, AppInterfaceInfo, AppStatusFilter, FullStateDump,
IssueAppAuthenticationTokenPayload, PeerMetaInfo, StorageInfo,
};
use holochain_types::network::HolochainTransportStats;
use holochain_types::websocket::AllowedOrigins;
use holochain_types::{
dna::AgentPubKey,
prelude::{
AppCapGrantInfo, CellId, DeleteCloneCellPayload, InstallAppPayload,
UpdateCoordinatorsPayload,
},
};
use holochain_websocket::{connect, ConnectRequest, WebsocketConfig, WebsocketSender};
use holochain_zome_types::{
capability::GrantedFunctions,
prelude::{DnaDef, GrantZomeCallCapabilityPayload, Record},
};
use kitsune2_api::Url;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt::Formatter;
use std::{net::ToSocketAddrs, sync::Arc};
#[derive(Clone)]
pub struct AdminWebsocket {
tx: WebsocketSender,
_poll_handle: Arc<AbortOnDropHandle>,
}
impl std::fmt::Debug for AdminWebsocket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AdminWebsocket").finish()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EnableAppResponse(AppInfo);
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthorizeSigningCredentialsPayload {
pub cell_id: CellId,
pub functions: Option<GrantedFunctions>,
}
impl AdminWebsocket {
pub async fn connect(
socket_addr: impl ToSocketAddrs,
origin: Option<String>,
) -> ConductorApiResult<Self> {
Self::connect_with_config(
socket_addr,
Arc::new(WebsocketConfig::CLIENT_DEFAULT),
origin,
)
.await
}
pub async fn connect_with_config(
socket_addr: impl ToSocketAddrs,
websocket_config: Arc<WebsocketConfig>,
origin: Option<String>,
) -> ConductorApiResult<Self> {
let mut last_err = None;
for addr in socket_addr.to_socket_addrs()? {
let request: ConnectRequest = if let Some(o) = &origin {
Into::<ConnectRequest>::into(addr).try_set_header("Origin", o.as_str())?
} else {
addr.into()
};
match Self::connect_with_request_and_config(request, websocket_config.clone()).await {
Ok(admin_ws) => return Ok(admin_ws),
Err(e) => {
last_err = Some(e);
}
}
}
Err(last_err.unwrap_or_else(|| {
ConductorApiError::WebsocketError(holochain_websocket::WebsocketError::Other(
"No addresses resolved".to_string(),
))
}))
}
pub async fn connect_with_request_and_config(
request: ConnectRequest,
websocket_config: Arc<WebsocketConfig>,
) -> ConductorApiResult<Self> {
let (tx, mut rx) = connect(websocket_config.clone(), request).await?;
let poll_handle =
tokio::task::spawn(async move { while rx.recv::<AdminResponse>().await.is_ok() {} });
Ok(Self {
tx,
_poll_handle: Arc::new(AbortOnDropHandle::new(poll_handle.abort_handle())),
})
}
pub async fn issue_app_auth_token(
&self,
payload: IssueAppAuthenticationTokenPayload,
) -> ConductorApiResult<AppAuthenticationTokenIssued> {
let response = self
.send(AdminRequest::IssueAppAuthenticationToken(payload))
.await?;
match response {
AdminResponse::AppAuthenticationTokenIssued(issued) => Ok(issued),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn revoke_app_authentication_token(
&self,
token: AppAuthenticationToken,
) -> ConductorApiResult<()> {
let response = self
.send(AdminRequest::RevokeAppAuthenticationToken(token))
.await?;
match response {
AdminResponse::AppAuthenticationTokenRevoked => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn generate_agent_pub_key(&self) -> ConductorApiResult<AgentPubKey> {
let response = self.send(AdminRequest::GenerateAgentPubKey).await?;
match response {
AdminResponse::AgentPubKeyGenerated(key) => Ok(key),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn add_admin_interfaces(
&self,
configs: Vec<AdminInterfaceConfig>,
) -> ConductorApiResult<()> {
let msg = AdminRequest::AddAdminInterfaces(configs);
let response = self.send(msg).await?;
match response {
AdminResponse::AdminInterfacesAdded => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn list_app_interfaces(&self) -> ConductorApiResult<Vec<AppInterfaceInfo>> {
let msg = AdminRequest::ListAppInterfaces;
let response = self.send(msg).await?;
match response {
AdminResponse::AppInterfacesListed(interfaces) => Ok(interfaces),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn attach_app_interface(
&self,
port: u16,
danger_bind_addr: Option<String>,
allowed_origins: AllowedOrigins,
installed_app_id: Option<String>,
) -> ConductorApiResult<u16> {
let msg = AdminRequest::AttachAppInterface {
port: Some(port),
danger_bind_addr,
allowed_origins,
installed_app_id,
};
let response = self.send(msg).await?;
match response {
AdminResponse::AppInterfaceAttached { port } => Ok(port),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn list_apps(
&self,
status_filter: Option<AppStatusFilter>,
) -> ConductorApiResult<Vec<AppInfo>> {
let response = self.send(AdminRequest::ListApps { status_filter }).await?;
match response {
AdminResponse::AppsListed(apps_infos) => Ok(apps_infos),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn install_app(&self, payload: InstallAppPayload) -> ConductorApiResult<AppInfo> {
let msg = AdminRequest::InstallApp(Box::new(payload));
let response = self.send(msg).await?;
match response {
AdminResponse::AppInstalled(app_info) => Ok(app_info),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn uninstall_app(
&self,
installed_app_id: String,
force: bool,
) -> ConductorApiResult<()> {
let msg = AdminRequest::UninstallApp {
installed_app_id,
force,
};
let response = self.send(msg).await?;
match response {
AdminResponse::AppUninstalled => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn list_dnas(&self) -> ConductorApiResult<Vec<DnaHash>> {
let response = self.send(AdminRequest::ListDnas).await?;
match response {
AdminResponse::DnasListed(dnas) => Ok(dnas),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn enable_app(
&self,
installed_app_id: String,
) -> ConductorApiResult<EnableAppResponse> {
let msg = AdminRequest::EnableApp { installed_app_id };
let response = self.send(msg).await?;
match response {
AdminResponse::AppEnabled(app) => Ok(EnableAppResponse(app)),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn disable_app(&self, installed_app_id: String) -> ConductorApiResult<()> {
let msg = AdminRequest::DisableApp { installed_app_id };
let response = self.send(msg).await?;
match response {
AdminResponse::AppDisabled => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn list_cell_ids(&self) -> ConductorApiResult<Vec<CellId>> {
let response = self.send(AdminRequest::ListCellIds).await?;
match response {
AdminResponse::CellIdsListed(cell_ids) => Ok(cell_ids),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn get_dna_definition(&self, cell_id: CellId) -> ConductorApiResult<DnaDef> {
let msg = AdminRequest::GetDnaDefinition(Box::new(cell_id));
let response = self.send(msg).await?;
match response {
AdminResponse::DnaDefinitionReturned(dna_definition) => Ok(dna_definition),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn grant_zome_call_capability(
&self,
payload: GrantZomeCallCapabilityPayload,
) -> ConductorApiResult<ActionHash> {
let msg = AdminRequest::GrantZomeCallCapability(Box::new(payload));
let response = self.send(msg).await?;
match response {
AdminResponse::ZomeCallCapabilityGranted(action_hash) => Ok(action_hash),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn list_capability_grants(
&self,
installed_app_id: String,
include_revoked: bool,
) -> ConductorApiResult<AppCapGrantInfo> {
let msg = AdminRequest::ListCapabilityGrants {
installed_app_id,
include_revoked,
};
let response = self.send(msg).await?;
match response {
AdminResponse::CapabilityGrantsInfo(info) => Ok(info),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn revoke_zome_call_capability(
&self,
cell_id: CellId,
action_hash: ActionHash,
) -> ConductorApiResult<()> {
let msg = AdminRequest::RevokeZomeCallCapability {
action_hash,
cell_id,
};
let response = self.send(msg).await?;
match response {
AdminResponse::ZomeCallCapabilityRevoked => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn delete_clone_cell(
&self,
payload: DeleteCloneCellPayload,
) -> ConductorApiResult<()> {
let msg = AdminRequest::DeleteCloneCell(Box::new(payload));
let response = self.send(msg).await?;
match response {
AdminResponse::CloneCellDeleted => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn storage_info(&self) -> ConductorApiResult<StorageInfo> {
let msg = AdminRequest::StorageInfo;
let response = self.send(msg).await?;
match response {
AdminResponse::StorageInfo(info) => Ok(info),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn dump_network_stats(&self) -> ConductorApiResult<HolochainTransportStats> {
let msg = AdminRequest::DumpNetworkStats;
let response = self.send(msg).await?;
match response {
AdminResponse::NetworkStatsDumped(stats) => Ok(stats),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn dump_state(&self, cell_id: CellId) -> ConductorApiResult<String> {
let msg = AdminRequest::DumpState {
cell_id: Box::new(cell_id),
};
let response = self.send(msg).await?;
match response {
AdminResponse::StateDumped(state) => Ok(state),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn dump_conductor_state(&self) -> ConductorApiResult<String> {
let msg = AdminRequest::DumpConductorState;
let response = self.send(msg).await?;
match response {
AdminResponse::ConductorStateDumped(state) => Ok(state),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn dump_full_state(
&self,
cell_id: CellId,
dht_ops_cursor: Option<u64>,
) -> ConductorApiResult<FullStateDump> {
let msg = AdminRequest::DumpFullState {
cell_id: Box::new(cell_id),
dht_ops_cursor,
};
let response = self.send(msg).await?;
match response {
AdminResponse::FullStateDumped(state) => Ok(state),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn dump_network_metrics(
&self,
dna_hash: Option<DnaHash>,
include_dht_summary: bool,
) -> ConductorApiResult<
std::collections::HashMap<DnaHash, holochain_types::network::Kitsune2NetworkMetrics>,
> {
let msg = AdminRequest::DumpNetworkMetrics {
dna_hash,
include_dht_summary,
};
let response = self.send(msg).await?;
match response {
AdminResponse::NetworkMetricsDumped(metrics) => Ok(metrics),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn update_coordinators(
&self,
update_coordinators_payload: UpdateCoordinatorsPayload,
) -> ConductorApiResult<()> {
let msg = AdminRequest::UpdateCoordinators(Box::new(update_coordinators_payload));
let response = self.send(msg).await?;
match response {
AdminResponse::CoordinatorsUpdated => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn graft_records(
&self,
cell_id: CellId,
validate: bool,
records: Vec<Record>,
) -> ConductorApiResult<()> {
let msg = AdminRequest::GraftRecords {
cell_id,
validate,
records,
};
let response = self.send(msg).await?;
match response {
AdminResponse::RecordsGrafted => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn agent_info(
&self,
dna_hashes: Option<Vec<DnaHash>>,
) -> ConductorApiResult<Vec<String>> {
let msg = AdminRequest::AgentInfo { dna_hashes };
let response = self.send(msg).await?;
match response {
AdminResponse::AgentInfo(agent_info) => Ok(agent_info),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn add_agent_info(&self, agent_infos: Vec<String>) -> ConductorApiResult<()> {
let msg = AdminRequest::AddAgentInfo { agent_infos };
let response = self.send(msg).await?;
match response {
AdminResponse::AgentInfoAdded => Ok(()),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn peer_meta_info(
&self,
url: Url,
dna_hashes: Option<Vec<DnaHash>>,
) -> ConductorApiResult<BTreeMap<DnaHash, BTreeMap<String, PeerMetaInfo>>> {
let msg = AdminRequest::PeerMetaInfo { url, dna_hashes };
let response = self.send(msg).await?;
match response {
AdminResponse::PeerMetaInfo(info) => Ok(info),
_ => unreachable!("Unexpected response {:?}", response),
}
}
pub async fn authorize_signing_credentials(
&self,
request: AuthorizeSigningCredentialsPayload,
) -> ConductorApiResult<crate::signing::client_signing::SigningCredentials> {
use holochain_zome_types::capability::{ZomeCallCapGrant, CAP_SECRET_BYTES};
use rand::{rngs::OsRng, RngCore};
use std::collections::BTreeSet;
let mut csprng = OsRng;
let keypair = ed25519_dalek::SigningKey::generate(&mut csprng);
let public_key = keypair.verifying_key();
let signing_agent_key = AgentPubKey::from_raw_32(public_key.as_bytes().to_vec());
let mut cap_secret = [0; CAP_SECRET_BYTES];
csprng.fill_bytes(&mut cap_secret);
self.grant_zome_call_capability(GrantZomeCallCapabilityPayload {
cell_id: request.cell_id,
cap_grant: ZomeCallCapGrant {
tag: "zome-call-signing-key".to_string(),
access: holochain_zome_types::capability::CapAccess::Assigned {
secret: cap_secret.into(),
assignees: BTreeSet::from([signing_agent_key.clone()]),
},
functions: request.functions.unwrap_or(GrantedFunctions::All),
},
})
.await?;
Ok(crate::signing::client_signing::SigningCredentials {
signing_agent_key,
keypair,
cap_secret: cap_secret.into(),
})
}
async fn send(&self, msg: AdminRequest) -> ConductorApiResult<AdminResponse> {
let response: AdminResponse = self
.tx
.request(msg)
.await
.map_err(ConductorApiError::WebsocketError)?;
match response {
AdminResponse::Error(error) => Err(ConductorApiError::ExternalApiWireError(error)),
_ => Ok(response),
}
}
}