use anyhow::{Context, Result, bail}; use base64::Engine; use clap::{Args, Parser, Subcommand}; use geth_config::GethPaths; use geth_control::{ControlRequest, ControlResponse}; use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport}; use std::io::Read; use std::path::PathBuf; #[derive(Debug, Parser)] #[command(name = "geth", about = "Personal local-first Iroh mesh runtime")] pub struct Cli { #[arg(long, global = true)] pub json: bool, #[arg(long, global = true)] pub jsonl: bool, #[command(subcommand)] pub command: Command, } #[derive(Debug, Subcommand)] pub enum Command { Init { #[arg(long)] admin_key: Option, #[arg(long)] signing_key: Option, #[arg(long, default_value = "owner")] owner: String, #[arg(long, default_value = "local")] node_name: String, #[arg(long = "capability")] capabilities: Vec, }, Daemon { #[command(subcommand)] command: DaemonCommand, }, Status, Sync { #[command(subcommand)] command: SyncCommand, }, Node { #[command(subcommand)] command: NodeCommand, }, Peer { #[command(subcommand)] command: PeerCommand, }, Resource { #[command(subcommand)] command: ResourceCommand, }, Keychain { #[command(subcommand)] command: KeychainCommand, }, Auth { #[command(subcommand)] command: AuthCommand, }, Secret { #[command(subcommand)] command: SecretCommand, }, Cas { #[command(subcommand)] command: CasCommand, }, Kv { #[command(subcommand)] command: KvCommand, }, Pubsub { #[command(subcommand)] command: PubsubCommand, }, Pipe { #[command(subcommand)] command: PipeCommand, }, Db { #[command(subcommand)] command: DbCommand, }, Document { #[command(subcommand)] command: DocumentCommand, }, Ssh { #[command(subcommand)] command: SshCommand, }, } #[derive(Debug, Subcommand)] pub enum DaemonCommand { Run, Service { #[command(subcommand)] command: ServiceCommand, }, } #[derive(Debug, Subcommand)] pub enum ServiceCommand { Install { #[arg(long, default_value = "auto")] manager: String, #[arg(long)] bin: Option, #[arg(long)] start: bool, }, Uninstall { #[arg(long, default_value = "auto")] manager: String, }, Start { #[arg(long, default_value = "auto")] manager: String, }, Stop { #[arg(long, default_value = "auto")] manager: String, }, Status { #[arg(long, default_value = "auto")] manager: String, }, Print { #[arg(long, default_value = "auto")] manager: String, #[arg(long)] bin: Option, }, } #[derive(Debug, Subcommand)] pub enum SyncCommand { Status, Now { node: Option }, } #[derive(Debug, Subcommand)] pub enum NodeCommand { Id, Status, List, Enroll { #[command(subcommand)] command: NodeEnrollCommand, }, Rename { node: String, name: String, #[arg(long)] signing_key: Option, }, Revoke { node: String, #[arg(long)] signing_key: Option, }, Grant { node: String, resource: String, capability: String, #[arg(long)] grant_id: Option, #[arg(long)] signing_key: PathBuf, #[arg(long)] admin_key: Option, }, RevokeGrant { resource: String, grant_id: String, #[arg(long)] signing_key: PathBuf, #[arg(long)] admin_key: Option, }, EndpointAdd { node: String, endpoint: String, #[arg(long)] signing_key: PathBuf, }, EndpointRevoke { node: String, endpoint: String, #[arg(long)] signing_key: PathBuf, }, } #[derive(Debug, Subcommand)] pub enum NodeEnrollCommand { Request { #[arg(long)] node_name: String, #[arg(long = "capability")] capabilities: Vec, #[arg(long)] reason: Option, #[arg(long)] out: Option, }, Submit { owner_node: String, #[arg(long)] request_id: Option, #[arg(long)] path: Option, }, Import { path: PathBuf, }, List { #[arg(long)] status: Option, }, Approve { request_id: String, #[arg(long)] signing_key: PathBuf, #[arg(long)] admin_key: Option, #[arg(long)] node_name: Option, #[arg(long = "capability")] capabilities: Vec, }, Sync { owner_node: String, }, } #[derive(Debug, Subcommand)] pub enum PeerCommand { Export { #[arg(long)] out: Option, }, Import { path: PathBuf, }, List, Ping { node: String, }, AuthCheck { node: String, resource: String, capability: String, }, } #[derive(Debug, Subcommand)] pub enum ResourceCommand { List, Create { kind: String, name: String }, } #[derive(Debug, Subcommand)] pub enum KeychainCommand { Init { #[arg(long)] admin_key: Option, #[arg(long)] signing_key: Option, }, Status, Sync { node: String, }, } #[derive(Debug, Subcommand)] pub enum AuthCommand { Explain { subject: String, resource: String, capability: String, }, Sync { node: String, }, Grant { subject: String, resource: String, capability: String, #[arg(long)] grant_id: Option, #[arg(long)] signing_key: PathBuf, #[arg(long)] admin_key: Option, }, Revoke { resource: String, grant_id: String, #[arg(long)] signing_key: PathBuf, #[arg(long)] admin_key: Option, }, } #[derive(Debug, Subcommand)] pub enum SecretCommand { Status, Create { resource: String, }, Rotate { resource: String, }, Bearer { #[command(subcommand)] command: SecretBearerCommand, }, } #[derive(Debug, Subcommand)] pub enum SecretBearerCommand { Create { resource: String, #[arg(long = "capability", required = true)] capabilities: Vec, #[arg(long)] expires_at_ms: Option, }, List, Challenge { resource: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Prove { token: String, resource: String, #[arg(long)] nonce: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Verify { token: String, resource: String, #[arg(long)] nonce: String, #[arg(long)] response: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Revoke { resource: String, bearer_id: String, }, } #[derive(Debug, Subcommand)] pub enum CasCommand { Add { path: PathBuf, }, AddPrivate { resource: String, path: PathBuf, }, Get { hash: String, #[arg(long)] out: PathBuf, }, GetPrivate { resource: String, hash: String, #[arg(long)] out: PathBuf, }, Fetch { node: String, hash: String, #[arg(long)] bearer_secret: Option, }, Hash { path: PathBuf, }, Has { hash: String, }, Pin { hash: String, }, Unpin { hash: String, }, Cleanup { #[arg(long)] dry_run: bool, }, Providers { hash: String, }, List, Root { #[command(subcommand)] command: CasRootCommand, }, Conflict { #[command(subcommand)] command: CasConflictCommand, }, } #[derive(Debug, Subcommand)] pub enum CasRootCommand { Add { name: String, path: PathBuf, }, List, Scan { name: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, Apply { source: String, #[arg(long)] to: PathBuf, #[arg(long)] dry_run: bool, }, } #[derive(Debug, Subcommand)] pub enum CasConflictCommand { Record { root: String, path: String, kind: String, #[arg(long)] detail: String, #[arg(long)] base_tree: Option, #[arg(long)] local_tree: Option, #[arg(long)] remote_tree: Option, }, List { #[arg(long)] root: Option, }, Resolve { conflict_id: String, resolution: String, #[arg(long)] note: Option, }, } #[derive(Debug, Subcommand)] pub enum KvCommand { Create { name: String, }, Set { name: String, key: String, value: String, #[arg(long)] subject: Option, }, Get { name: String, key: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum PubsubCommand { Pub { topic: String, message: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, Sub { topic: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum PipeCommand { Listen { name: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, Connect { target: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, ForwardTcp { #[arg(long)] listen: String, #[arg(long)] node: String, #[arg(long)] target: String, #[arg(long)] bearer_secret: Option, }, ForwardUnix { #[arg(long)] listen: PathBuf, #[arg(long)] node: String, #[arg(long)] target: PathBuf, #[arg(long)] bearer_secret: Option, }, Send { target: String, message: Option, #[arg(long = "in", value_name = "PATH")] input: Option, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, Recv { name: String, #[arg(long)] peek: bool, }, } #[derive(Debug, Subcommand)] pub enum DbCommand { Add { name: String, path: PathBuf, }, Status { name: String, }, Changes { name: String, #[arg(long)] after_db_version: Option, #[arg(long, default_value_t = 100)] limit: u32, }, Sync { node: String, name: String, #[arg(long, default_value_t = 100)] limit: u32, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum DocumentCommand { Create { name: String, }, Status { name: String, }, Set { name: String, state_json: String, }, Get { name: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum SshCommand { Proxy { node: String, #[arg(long)] bearer_secret: Option, }, AdminShell { node: String, command: String, #[arg(long)] bearer_secret: Option, }, Cert { #[command(subcommand)] command: SshCertCommand, }, Revocation { #[command(subcommand)] command: SshRevocationCommand, }, } #[derive(Debug, Subcommand)] pub enum SshCertCommand { Request { #[arg(long)] public_key: PathBuf, #[arg(long, default_value = "user")] kind: String, #[arg(long = "principal", required = true)] principals: Vec, #[arg(long)] valid_for: Option, #[arg(long)] renewal_of: Option, #[arg(long)] reason: Option, #[arg(long)] subject: Option, }, Requests { #[arg(long)] subject: Option, }, Approve { request_id: String, #[arg(long)] ca_key: PathBuf, #[arg(long)] valid_for: Option, #[arg(long)] serial: Option, #[arg(long)] out: Option, #[arg(long)] sign: bool, #[arg(long)] subject: Option, }, Import { request_id: String, #[arg(long)] cert: PathBuf, #[arg(long)] subject: Option, }, List { #[arg(long)] subject: Option, }, Sync { node: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum SshRevocationCommand { Add { kind: String, target: String, #[arg(long)] reason: Option, #[arg(long)] subject: Option, }, List { #[arg(long)] subject: Option, }, Export { #[arg(long)] out: PathBuf, #[arg(long, default_value = "jsonl")] format: String, #[arg(long)] ca_public: Option, #[arg(long)] subject: Option, }, Import { path: PathBuf, #[arg(long, default_value = "jsonl")] format: String, #[arg(long)] subject: Option, }, Sync { node: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Args)] pub struct EmptyArgs {} pub async fn run() -> Result<()> { let cli = Cli::parse(); let paths = GethPaths::resolve().context("resolve geth paths")?; match cli.command { Command::Init { admin_key, signing_key, owner, node_name, capabilities, } => { let node = geth_node::init_owned_node( &paths, geth_node::InitOwnerOptions { admin_key_path: admin_key, signing_key_path: signing_key, owner_name: owner, node_name, capabilities, }, ) .context("initialize geth node")?; println!("initialized geth home: {}", node.paths.home().display()); println!("agent: {}", node.agent_id); println!("node: {}", node.node_id); } Command::Daemon { command: DaemonCommand::Run, } => { geth_node::run_daemon(paths) .await .context("run geth daemon")?; } Command::Daemon { command: DaemonCommand::Service { command }, } => { let report = run_service_command(&paths, command).context("manage geth user service")?; print_service_report(report, cli.json || cli.jsonl)?; } Command::Ssh { command: SshCommand::Proxy { node, bearer_secret, }, } if !cli.json && !cli.jsonl => { geth_node::stream_ssh_proxy(&paths, node, bearer_secret) .await .context("stream SSH proxy through geth daemon")?; } Command::Pipe { command: PipeCommand::ForwardTcp { listen, node, target, bearer_secret, }, } if !cli.json && !cli.jsonl => { println!("forwarding tcp {listen} -> {node}:{target}"); geth_node::run_tcp_forward(&paths, listen, node, target, bearer_secret) .await .context("run TCP forward through geth daemon")?; } Command::Pipe { command: PipeCommand::ForwardUnix { listen, node, target, bearer_secret, }, } if !cli.json && !cli.jsonl => { println!( "forwarding unix {} -> {node}:{}", listen.display(), target.display() ); geth_node::run_unix_forward(&paths, listen, node, target, bearer_secret) .await .context("run Unix socket forward through geth daemon")?; } command => { let request = request_for_command(command)?; let response = geth_node::send_control(&paths, request) .await .with_context(|| { format!("connect to daemon at {}", paths.socket_path().display()) })?; print_response(response, cli.json || cli.jsonl)?; } } Ok(()) } fn request_for_command(command: Command) -> Result { Ok(match command { Command::Status => ControlRequest::Status, Command::Sync { command: SyncCommand::Status, } => ControlRequest::SyncStatus, Command::Sync { command: SyncCommand::Now { node }, } => ControlRequest::SyncNow { node }, Command::Node { command: NodeCommand::Id, } => ControlRequest::NodeId, Command::Node { command: NodeCommand::Status, } => ControlRequest::Status, Command::Node { command: NodeCommand::List, } => ControlRequest::NodeList, Command::Node { command: NodeCommand::Enroll { command }, } => match command { NodeEnrollCommand::Request { node_name, capabilities, reason, out, } => ControlRequest::NodeEnrollRequest { node_name, capabilities, reason, out, }, NodeEnrollCommand::Submit { owner_node, request_id, path, } => ControlRequest::NodeEnrollSubmit { owner_node, request_id, path, }, NodeEnrollCommand::Import { path } => ControlRequest::NodeEnrollImport { path }, NodeEnrollCommand::List { status } => ControlRequest::NodeEnrollList { status }, NodeEnrollCommand::Approve { request_id, signing_key, admin_key, node_name, capabilities, } => ControlRequest::NodeEnrollApprove { request_id, signing_key_path: signing_key, admin_key_path: admin_key, node_name, capabilities, }, NodeEnrollCommand::Sync { owner_node } => ControlRequest::NodeEnrollSync { owner_node }, }, Command::Node { command: NodeCommand::Rename { node, name, signing_key, }, } => ControlRequest::NodeRename { node, name, signing_key_path: signing_key, }, Command::Node { command: NodeCommand::Revoke { node, signing_key }, } => ControlRequest::NodeRevoke { node, signing_key_path: signing_key, }, Command::Node { command: NodeCommand::Grant { node, resource, capability, grant_id, signing_key, admin_key, }, } => ControlRequest::NodeGrant { node, resource, capability, grant_id, signing_key_path: Some(signing_key), admin_key_path: admin_key, }, Command::Node { command: NodeCommand::RevokeGrant { resource, grant_id, signing_key, admin_key, }, } => ControlRequest::NodeRevokeGrant { resource, grant_id, signing_key_path: Some(signing_key), admin_key_path: admin_key, }, Command::Node { command: NodeCommand::EndpointAdd { node, endpoint, signing_key, }, } => ControlRequest::NodeEndpointAdd { node, endpoint, signing_key_path: Some(signing_key), }, Command::Node { command: NodeCommand::EndpointRevoke { node, endpoint, signing_key, }, } => ControlRequest::NodeEndpointRevoke { node, endpoint, signing_key_path: Some(signing_key), }, Command::Peer { command } => match command { PeerCommand::Export { out } => ControlRequest::PeerCardExport { out }, PeerCommand::Import { path } => ControlRequest::PeerCardImport { path }, PeerCommand::List => ControlRequest::PeerCardList, PeerCommand::Ping { node } => ControlRequest::PeerPing { node }, PeerCommand::AuthCheck { node, resource, capability, } => ControlRequest::PeerAuthCheck { node, resource, capability, }, }, Command::Resource { command: ResourceCommand::List, } => ControlRequest::ResourceList, Command::Resource { command: ResourceCommand::Create { kind, name }, } => ControlRequest::ResourceCreate { kind, name }, Command::Keychain { command: KeychainCommand::Init { admin_key, signing_key, }, } => ControlRequest::KeychainInit { admin_key_path: admin_key, signing_key_path: signing_key, }, Command::Keychain { command: KeychainCommand::Status, } => ControlRequest::KeychainStatus, Command::Keychain { command: KeychainCommand::Sync { node }, } => ControlRequest::KeychainSync { node }, Command::Auth { command: AuthCommand::Sync { node }, } => ControlRequest::AuthSync { node }, Command::Auth { command: AuthCommand::Explain { subject, resource, capability, }, } => ControlRequest::AuthExplain { subject, resource, capability, }, Command::Auth { command: AuthCommand::Grant { subject, resource, capability, grant_id, signing_key, admin_key, }, } => ControlRequest::AuthGrant { subject, resource, capability, grant_id, signing_key_path: Some(signing_key), admin_key_path: admin_key, }, Command::Auth { command: AuthCommand::Revoke { resource, grant_id, signing_key, admin_key, }, } => ControlRequest::AuthRevoke { resource, grant_id, signing_key_path: Some(signing_key), admin_key_path: admin_key, }, Command::Secret { command } => match command { SecretCommand::Status => ControlRequest::SecretStatus, SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource }, SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource }, SecretCommand::Bearer { command } => match command { SecretBearerCommand::Create { resource, capabilities, expires_at_ms, } => ControlRequest::SecretBearerCreate { resource, capabilities, expires_at_ms, }, SecretBearerCommand::List => ControlRequest::SecretBearerList, SecretBearerCommand::Challenge { resource, capabilities, } => ControlRequest::SecretBearerChallenge { resource, capabilities, }, SecretBearerCommand::Prove { token, resource, capabilities, nonce, } => ControlRequest::SecretBearerProve { secret: token, resource, capabilities, nonce, }, SecretBearerCommand::Verify { token, resource, capabilities, nonce, response, } => ControlRequest::SecretBearerVerify { secret: token, resource, capabilities, nonce, response, }, SecretBearerCommand::Revoke { resource, bearer_id, } => ControlRequest::SecretBearerRevoke { resource, secret: bearer_id, }, }, }, Command::Cas { command } => match command { CasCommand::Add { path } => ControlRequest::CasAdd { path }, CasCommand::AddPrivate { resource, path } => { ControlRequest::CasAddPrivate { resource, path } } CasCommand::Get { hash, out } => ControlRequest::CasGet { hash: hash.into(), out, }, CasCommand::GetPrivate { resource, hash, out, } => ControlRequest::CasGetPrivate { resource, hash: hash.into(), out, }, CasCommand::Fetch { node, hash, bearer_secret, } => ControlRequest::CasFetch { node, hash: hash.into(), bearer_secret, }, CasCommand::Hash { path } => ControlRequest::CasHash { path }, CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() }, CasCommand::Pin { hash } => ControlRequest::CasPin { hash: hash.into() }, CasCommand::Unpin { hash } => ControlRequest::CasUnpin { hash: hash.into() }, CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run }, CasCommand::Providers { hash } => ControlRequest::CasProviders { hash: hash.into() }, CasCommand::List => ControlRequest::CasList, CasCommand::Root { command } => match command { CasRootCommand::Add { name, path } => ControlRequest::CasRootAdd { name, path }, CasRootCommand::List => ControlRequest::CasRootList, CasRootCommand::Scan { name } => ControlRequest::CasRootScan { name }, CasRootCommand::Sync { node, name, bearer_secret, } => ControlRequest::CasRootSync { node, name, bearer_secret, }, CasRootCommand::Apply { source, to, dry_run, } => ControlRequest::CasRootApply { source, target: to, dry_run, }, }, CasCommand::Conflict { command } => match command { CasConflictCommand::Record { root, path, kind, detail, base_tree, local_tree, remote_tree, } => ControlRequest::CasConflictRecord { root, path, kind, detail, base_tree: base_tree.map(Into::into), local_tree: local_tree.map(Into::into), remote_tree: remote_tree.map(Into::into), }, CasConflictCommand::List { root } => ControlRequest::CasConflictList { root }, CasConflictCommand::Resolve { conflict_id, resolution, note, } => ControlRequest::CasConflictResolve { conflict_id, resolution, note, }, }, }, Command::Kv { command } => match command { KvCommand::Create { name } => ControlRequest::KvCreate { name }, KvCommand::Set { name, key, value, subject, } => ControlRequest::KvSet { name, key, value, subject, }, KvCommand::Get { name, key } => ControlRequest::KvGet { name, key }, KvCommand::Sync { node, name, bearer_secret, } => ControlRequest::KvSync { node, name, bearer_secret, }, }, Command::Pubsub { command } => match command { PubsubCommand::Pub { topic, message, node, bearer_secret, } => ControlRequest::PubsubPub { topic, message, node, bearer_secret, }, PubsubCommand::Sub { topic, node, bearer_secret, } => ControlRequest::PubsubSub { topic, node, bearer_secret, }, }, Command::Pipe { command } => match command { PipeCommand::Listen { name, node, bearer_secret, } => ControlRequest::PipeListen { name, node, bearer_secret, }, PipeCommand::Connect { target, node, bearer_secret, } => ControlRequest::PipeConnect { target, node, bearer_secret, }, PipeCommand::ForwardTcp { listen, node, target, bearer_secret, } => ControlRequest::PipeTcpForward { listen_addr: listen, node, target_addr: target, bearer_secret, }, PipeCommand::ForwardUnix { listen, node, target, bearer_secret, } => ControlRequest::PipeUnixForward { listen_path: listen, node, target_path: target, bearer_secret, }, PipeCommand::Send { target, message, input, node, bearer_secret, } => ControlRequest::PipeSend { target, data_base64: pipe_send_payload_base64(message, input)?, node, bearer_secret, }, PipeCommand::Recv { name, peek } => ControlRequest::PipeRecv { name, peek }, }, Command::Db { command } => match command { DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path }, DbCommand::Status { name } => ControlRequest::DbStatus { name }, DbCommand::Changes { name, after_db_version, limit, } => ControlRequest::DbChanges { name, after_db_version, limit, }, DbCommand::Sync { node, name, limit, bearer_secret, } => ControlRequest::DbSync { node, name, limit, bearer_secret, }, }, Command::Document { command } => match command { DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name }, DocumentCommand::Status { name } => ControlRequest::DocumentStatus { name }, DocumentCommand::Set { name, state_json } => { ControlRequest::DocumentSet { name, state_json } } DocumentCommand::Get { name } => ControlRequest::DocumentGet { name }, DocumentCommand::Sync { node, name, bearer_secret, } => ControlRequest::DocumentSync { node, name, bearer_secret, }, }, Command::Ssh { command } => match command { SshCommand::Proxy { node, bearer_secret, } => ControlRequest::SshProxyConnect { node, bearer_secret, }, SshCommand::AdminShell { node, command, bearer_secret, } => ControlRequest::SshAdminShell { node, command, bearer_secret, }, SshCommand::Cert { command } => match command { SshCertCommand::Request { public_key, kind, principals, valid_for, renewal_of, reason, subject, } => ControlRequest::SshCertRequest { public_key_path: public_key, cert_kind: kind, principals, requested_validity: valid_for, renewal_of, reason, subject, }, SshCertCommand::Requests { subject } => ControlRequest::SshCertRequests { subject }, SshCertCommand::Approve { request_id, ca_key, valid_for, serial, out, sign, subject, } => ControlRequest::SshCertApprove { request_id, ca_key_path: ca_key, valid_for, serial, out, sign, subject, }, SshCertCommand::Import { request_id, cert, subject, } => ControlRequest::SshCertImport { request_id, cert_path: cert, subject, }, SshCertCommand::List { subject } => ControlRequest::SshCertList { subject }, SshCertCommand::Sync { node, bearer_secret, } => ControlRequest::SshCertSync { node, bearer_secret, }, }, SshCommand::Revocation { command } => match command { SshRevocationCommand::Add { kind, target, reason, subject, } => ControlRequest::SshRevocationAdd { kind, target, reason, subject, }, SshRevocationCommand::List { subject } => { ControlRequest::SshRevocationList { subject } } SshRevocationCommand::Export { out, format, ca_public, subject, } => ControlRequest::SshRevocationExport { out, format, ca_public, subject, }, SshRevocationCommand::Import { path, format, subject, } => ControlRequest::SshRevocationImport { path, format, subject, }, SshRevocationCommand::Sync { node, bearer_secret, } => ControlRequest::SshRevocationSync { node, bearer_secret, }, }, }, Command::Init { .. } | Command::Daemon { .. } => bail!("command is handled directly"), }) } fn run_service_command(paths: &GethPaths, command: ServiceCommand) -> Result { Ok(match command { ServiceCommand::Install { manager, bin, start, } => { geth_node::init_node(paths).context("initialize geth home before service install")?; let manager = manager.parse::()?; let executable = service_executable(bin)?; geth_node::service::install_user_service( paths, ServiceInstallOptions { manager, executable, start, }, )? } ServiceCommand::Uninstall { manager } => { geth_node::service::uninstall_user_service(manager.parse::()?)? } ServiceCommand::Start { manager } => { geth_node::service::start_user_service(manager.parse::()?)? } ServiceCommand::Stop { manager } => { geth_node::service::stop_user_service(manager.parse::()?)? } ServiceCommand::Status { manager } => { geth_node::service::status_user_service(manager.parse::()?)? } ServiceCommand::Print { manager, bin } => { let executable = service_executable(bin)?; geth_node::service::print_user_service( paths, manager.parse::()?, &executable, )? } }) } fn service_executable(bin: Option) -> Result { bin.map(Ok) .unwrap_or_else(std::env::current_exe) .context("resolve current geth executable") } fn print_response(response: ControlResponse, json: bool) -> Result<()> { if json { println!("{}", serde_json::to_string_pretty(&response)?); return Ok(()); } match response { ControlResponse::Status(status) => { println!("geth daemon: running"); println!("home: {}", status.home.display()); println!("socket: {}", status.socket.display()); println!("agent: {}", status.agent_id); println!("node: {}", status.node_id); println!( "endpoint: {}", status.endpoint_id.as_deref().unwrap_or("not started") ); println!("iroh relay: {}", status.iroh_relay_mode); println!( "iroh discovery: {}", if status.iroh_local_discovery { "local-network enabled" } else { "local-network disabled" } ); println!("iroh: {}", status.iroh); } ControlResponse::NodeId(node) => { println!("agent: {}", node.agent_id); println!("node: {}", node.node_id); println!( "endpoint: {}", node.endpoint_id .as_deref() .unwrap_or("not started in bootstrap") ); } ControlResponse::PeerCardExported { card, out, note } => { println!("peer card: {}", card.node_id); println!("agent: {}", card.agent_id); println!("endpoints: {}", card.endpoints.len()); if let Some(path) = out { println!("wrote: {}", path.display()); } else { println!("{}", serde_json::to_string_pretty(&card)?); } println!("note: {note}"); } ControlResponse::PeerCardImported { peer, note } => { println!("imported peer: {}", peer.card.node_id); println!("agent: {}", peer.card.agent_id); println!("trust: candidate-only"); println!("note: {note}"); } ControlResponse::PeerCardList { peers, note } => { if peers.is_empty() { println!("no peer candidates"); } else { for peer in peers { println!( "{}\t{}\t{} endpoints\tcandidate-only", peer.card.node_id, peer.card.agent_id, peer.card.endpoints.len() ); } } println!("note: {note}"); } ControlResponse::PeerPinged { peer_node_id, peer_agent_id, endpoint_id, alpn, note, } => { println!("peer pong: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("alpn: {alpn}"); println!("note: {note}"); } ControlResponse::PeerAuthChecked { peer_node_id, peer_agent_id, endpoint_id, resource, capability, allowed, reason, evaluated_ops, note, } => { println!("peer auth: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("resource: {resource}"); println!("capability: {capability}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("evaluated_ops: {evaluated_ops}"); println!("note: {note}"); } ControlResponse::ResourceList { resources } => { if resources.is_empty() { println!("no resources"); } else { for resource in resources { println!("{}\t{}\t{}", resource.kind, resource.name, resource.id); } } } ControlResponse::ResourceCreated { resource } => { println!( "created resource: {} {} ({})", resource.kind, resource.name, resource.id ); } ControlResponse::CasAdded { hash, size_bytes } => { println!("{hash} {size_bytes} bytes"); } ControlResponse::CasPrivateAdded { resource, epoch, plaintext_hash, encrypted_hash, size_bytes, note, } => { println!("encrypted_hash: {encrypted_hash}"); println!("plaintext_hash: {plaintext_hash}"); println!("resource: {resource}"); println!("epoch: {epoch}"); println!("size_bytes: {size_bytes}"); println!("note: {note}"); } ControlResponse::CasGot { hash, out, size_bytes, } => { println!("wrote {hash} to {} ({size_bytes} bytes)", out.display()); } ControlResponse::CasPrivateGot { resource, hash, plaintext_hash, out, size_bytes, note, } => { println!( "decrypted {hash} for {resource} to {} ({size_bytes} bytes)", out.display() ); println!("plaintext_hash: {plaintext_hash}"); println!("note: {note}"); } ControlResponse::CasFetched { peer_node_id, peer_agent_id, endpoint_id, hash, size_bytes, allowed, reason, note, } => { if allowed { println!("fetched {hash} from {peer_node_id} ({size_bytes} bytes)"); } else { println!("fetch denied for {hash} from {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::CasHash { hash } => println!("{hash}"), ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"), ControlResponse::CasPinned { hash, pinned } => { println!("{hash}: pinned={pinned}"); } ControlResponse::CasCleanup { removed, retained_pinned, dry_run, } => { let action = if dry_run { "would remove" } else { "removed" }; println!("{action}: {}", removed.len()); for hash in removed { println!(" {hash}"); } println!("retained_pinned: {}", retained_pinned.len()); for hash in retained_pinned { println!(" {hash}"); } } ControlResponse::CasList { blobs } => { for blob in blobs { let pin = if blob.pinned { "pinned" } else { "unpinned" }; println!("{}\t{} bytes\t{}", blob.hash, blob.size_bytes, pin); } } ControlResponse::CasProviders { hash, providers } => { println!("hash: {hash}"); println!("providers: {}", providers.len()); for provider in providers { println!( "{}\t{}\t{}", provider.peer_node_id, provider.endpoint_id, provider.last_seen_ms ); } } ControlResponse::CasRootAdded { root } => { println!("added file root: {}", root.name); println!("id: {}", root.id); println!("resource: {}", root.resource); println!("path: {}", root.path); } ControlResponse::CasRootList { roots } => { if roots.is_empty() { println!("no file roots"); } else { for root in roots { println!( "{}\t{}\t{}", root.name, root.path, root.latest_tree .map(|hash| hash.to_string()) .unwrap_or_else(|| "unscanned".to_owned()) ); } } } ControlResponse::CasRootScanned { scan } => { println!("file root: {}", scan.root.name); println!("tree: {}", scan.tree.hash); println!("tree_bytes: {}", scan.tree.size_bytes); println!("changes: {}", scan.changes.len()); for change in scan.changes { println!("{change:?}"); } println!("note: {}", scan.note); } ControlResponse::CasRootSynced { peer_node_id, peer_agent_id, endpoint_id, name, root, tree_bytes_imported, sync_conflicts, allowed, reason, note, } => { if let Some(root) = root { println!("synced file root: {name}"); println!("peer: {peer_node_id}"); println!("path: {}", root.path); println!( "tree: {}", root.latest_tree .map(|hash| hash.to_string()) .unwrap_or_else(|| "unscanned".to_owned()) ); println!("tree_bytes_imported: {tree_bytes_imported}"); println!("sync_conflicts: {}", sync_conflicts.len()); for conflict in sync_conflicts { println!( "{}\t{}\t{}\t{}", conflict.id, conflict.path, conflict.kind.as_str(), conflict.status.as_str() ); } } else { println!("file root sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::CasRootApplied { source, target, files_written, dirs_created, conflicts, dry_run, note, } => { println!("applied file root: {source}"); println!("target: {}", target.display()); println!("dry_run: {dry_run}"); println!("files_written: {files_written}"); println!("dirs_created: {dirs_created}"); println!("conflicts: {}", conflicts.len()); for conflict in conflicts { println!( "{}\t{}\t{}\t{}", conflict.id, conflict.path, conflict.kind.as_str(), conflict.status.as_str() ); } println!("note: {note}"); } ControlResponse::CasConflictRecorded { conflict } => { println!("recorded conflict: {}", conflict.id); print_file_conflict(&conflict); } ControlResponse::CasConflictList { conflicts } => { if conflicts.is_empty() { println!("no file conflicts"); } else { for conflict in conflicts { println!( "{}\t{}\t{}\t{}\t{}", conflict.id, conflict.root, conflict.path, conflict.kind.as_str(), conflict.status.as_str() ); } } } ControlResponse::CasConflictResolved { conflict } => { println!("resolved conflict: {}", conflict.id); print_file_conflict(&conflict); } ControlResponse::KeychainStatus(status) => { println!("initialized: {}", status.initialized); println!("admin_keys: {}", status.admin_keys); println!("signatures: {}", status.signatures); println!("verified_signatures: {}", status.verified_signatures); println!("failed_signatures: {}", status.failed_signatures); println!("users: {}", status.users); println!("devices: {}", status.devices); println!("nodes: {}", status.nodes); } ControlResponse::KeychainInitialized { ops, signatures } => { println!("initialized keychain"); for op in ops { println!("recorded keychain op: {}", op.id); } for signature in signatures { println!( "signed keychain op: {} by {} ({})", signature.op_id, signature.signer, signature.namespace ); } } ControlResponse::KeychainSynced { peer_node_id, peer_agent_id, endpoint_id, ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, note, } => { println!("synced keychain from: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("ops_imported: {ops_imported}"); println!("signatures_imported: {signatures_imported}"); println!("invalid_ops_rejected: {invalid_ops_rejected}"); println!("high_water_ms: {high_water_ms}"); println!("note: {note}"); } ControlResponse::AuthSynced { peer_node_id, peer_agent_id, endpoint_id, ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, note, } => { println!("synced auth from: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("ops_imported: {ops_imported}"); println!("signatures_imported: {signatures_imported}"); println!("invalid_ops_rejected: {invalid_ops_rejected}"); println!("high_water_ms: {high_water_ms}"); println!("note: {note}"); } ControlResponse::SyncStatus { peers, note } => { if peers.is_empty() { println!("no sync peers"); } else { for peer in peers { println!("peer: {}", peer.peer_node_id); if peer.streams.is_empty() { println!(" no sync attempts recorded"); } for stream in peer.streams { println!( " {}\tcursor={}\tlast_attempt={}\tlast_success={}\timported={}\trejected={}\terror={}", stream.stream, stream.cursor_ms, stream .last_attempt_ms .map(|value| value.to_string()) .unwrap_or_else(|| "never".to_owned()), stream .last_success_ms .map(|value| value.to_string()) .unwrap_or_else(|| "never".to_owned()), stream.last_imported, stream.last_rejected, stream.last_error.unwrap_or_else(|| "-".to_owned()) ); } } } println!("note: {note}"); } ControlResponse::SyncRan { peers, note } => { if peers.is_empty() { println!("no sync peers"); } else { for peer in peers { println!("peer: {}", peer.peer_node_id); for stream in peer.streams { let state = if !stream.attempted { "skipped" } else if stream.success { "ok" } else { "failed" }; println!( " {}\t{}\tcursor={}\timported={}\trejected={}\terror={}", stream.stream, state, stream.cursor_ms, stream.imported, stream.rejected, stream.error.unwrap_or_else(|| "-".to_owned()) ); } } } println!("note: {note}"); } ControlResponse::SecretStatus { secrets } => { if secrets.is_empty() { println!("no resource secrets"); } else { for secret in secrets { println!("{}\t{}\tepoch {}", secret.id, secret.resource, secret.epoch); } } } ControlResponse::SecretCreated { secret } => { println!("resource secret: {}", secret.id); println!("resource: {}", secret.resource); println!("epoch: {}", secret.epoch); } ControlResponse::SecretBearerCreated { access } => { println!("bearer id: {}", access.secret); if let Some(token) = access.token { println!("bearer token: {token}"); } println!("resource: {}", access.resource); println!( "capabilities: {}", access .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); if let Some(expires_at) = access.expires_at { println!("expires_at_ms: {}", expires_at.0); } println!("may_delegate: {}", access.may_delegate); } ControlResponse::SecretBearerList { access } => { if access.is_empty() { println!("no bearer access"); } else { for item in access { println!( "{}\t{}\t{}\tmay_delegate={}", item.secret, item.resource, item.capabilities .iter() .map(ToString::to_string) .collect::>() .join(","), item.may_delegate ); } } } ControlResponse::SecretBearerChallenge { challenge } => { println!("bearer challenge"); println!("resource: {}", challenge.resource); println!("nonce: {}", challenge.nonce); println!("issued_at_ms: {}", challenge.issued_at.0); println!( "capabilities: {}", challenge .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); } ControlResponse::SecretBearerProof { proof } => { println!("bearer proof"); println!("secret: {}", proof.secret); println!("resource: {}", proof.resource); println!("nonce: {}", proof.nonce); println!("response: {}", proof.response); println!( "capabilities: {}", proof .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); } ControlResponse::SecretBearerVerified { secret, resource, capabilities, verified, reason, } => { println!("bearer verified: {verified}"); println!("secret: {secret}"); println!("resource: {resource}"); println!("capabilities: {}", capabilities.join(",")); println!("reason: {reason}"); } ControlResponse::SecretBearerRevoked { resource, secret } => { println!("revoked bearer secret: {secret}"); println!("resource: {resource}"); } ControlResponse::AuthExplain(explain) => { println!("allowed: {}", explain.allowed); println!("subject: {}", explain.subject); println!("resource: {}", explain.resource); println!("capability: {}", explain.capability); println!("reason: {}", explain.reason); println!("evaluated_ops: {}", explain.evaluated_ops); if !explain.diagnostics.is_empty() { println!("diagnostics: {}", explain.diagnostics.join(",")); } } ControlResponse::AuthOpRecorded { op, signatures } => { println!("recorded auth op: {}", op.id); println!("resource: {}", op.resource); for signature in signatures { println!( "signed auth op: {} by {} ({})", signature.op_id, signature.signer, signature.namespace ); } } ControlResponse::NodeList { nodes, note } => { if nodes.is_empty() { println!("no enrolled nodes"); } else { for node in nodes { println!( "{}\t{}\tdevice {}\t{} endpoints", node.name, node.id, node.device, node.endpoints.len() ); } } println!("note: {note}"); } ControlResponse::NodeKeychainUpdated { ops, signatures, note, } => { for op in ops { println!("recorded keychain op: {}", op.id); } for signature in signatures { println!( "signed keychain op: {} by {} ({})", signature.op_id, signature.signer, signature.namespace ); } println!("note: {note}"); } ControlResponse::NodeGrantUpdated { op, signatures, note, } => { println!("recorded auth op: {}", op.id); println!("resource: {}", op.resource); for signature in signatures { println!( "signed auth op: {} by {} ({})", signature.op_id, signature.signer, signature.namespace ); } println!("note: {note}"); } ControlResponse::NodeEnrollmentRequested { request, out, note } => { println!("node enrollment request: {}", request.id); println!("node: {}", request.requester_node); println!("requested_name: {}", request.requested_node_name); println!("status: {}", request.status); if let Some(out) = out { println!("written: {}", out.display()); } println!("note: {note}"); } ControlResponse::NodeEnrollmentSubmitted { request_id, owner_node_id, accepted, note, } => { println!("submitted enrollment request: {request_id}"); println!("owner_node: {owner_node_id}"); println!("accepted: {accepted}"); println!("note: {note}"); } ControlResponse::NodeEnrollmentImported { request, note } => { println!("imported enrollment request: {}", request.id); println!("node: {}", request.requester_node); println!("requested_name: {}", request.requested_node_name); println!("status: {}", request.status); println!("note: {note}"); } ControlResponse::NodeEnrollmentList { requests, note } => { if requests.is_empty() { println!("no node enrollment requests"); } else { for request in requests { println!( "{}\t{}\t{}\t{} capabilities", request.id, request.status, request.requested_node_name, request.requested_capabilities.len() ); } } println!("note: {note}"); } ControlResponse::NodeEnrollmentApproved { request, keychain_ops, keychain_signatures, auth_ops, auth_signatures, note, } => { println!("approved enrollment request: {}", request.id); println!("node: {}", request.requester_node); println!("keychain_ops: {}", keychain_ops.len()); println!("keychain_signatures: {}", keychain_signatures.len()); println!("auth_ops: {}", auth_ops.len()); println!("auth_signatures: {}", auth_signatures.len()); println!("note: {note}"); } ControlResponse::NodeEnrollmentSynced { owner_node, keychain_ops_imported, keychain_signatures_imported, auth_ops_imported, auth_signatures_imported, invalid_ops_rejected, note, } => { println!("synced enrollment from: {owner_node}"); println!("keychain_ops_imported: {keychain_ops_imported}"); println!("keychain_signatures_imported: {keychain_signatures_imported}"); println!("auth_ops_imported: {auth_ops_imported}"); println!("auth_signatures_imported: {auth_signatures_imported}"); println!("invalid_ops_rejected: {invalid_ops_rejected}"); println!("note: {note}"); } ControlResponse::SshCertRequested { request } => { println!("ssh cert request: {}", request.id); println!("status: {}", request.status); println!("kind: {}", request.cert_kind); println!("principals: {}", request.principals.join(",")); println!("public_key_fingerprint: {}", request.public_key_fingerprint); } ControlResponse::SshCertRequests { requests } => { if requests.is_empty() { println!("no ssh certificate requests"); } else { for request in requests { println!( "{}\t{}\t{}\t{}\t{}", request.id, request.status, request.cert_kind, request.principals.join(","), request.public_key_fingerprint ); } } } ControlResponse::SshCertApproved { approval } => { println!("approved ssh cert request: {}", approval.request_id); println!("valid_for: {}", approval.valid_for); if let Some(serial) = approval.serial { println!("serial: {serial}"); } if let Some(output_path) = approval.output_path { println!("expected_certificate: {output_path}"); } println!("signing_command:"); println!("{}", shell_quote_command(&approval.signing_command)); println!("signed: {}", approval.signed); if let Some(certificate_id) = approval.certificate_id { println!("certificate_id: {certificate_id}"); } println!("note: {}", approval.note); } ControlResponse::SshCertImported { certificate } => { println!("imported ssh certificate: {}", certificate.id); println!("request: {}", certificate.request_id); println!("fingerprint: {}", certificate.certificate_fingerprint); } ControlResponse::SshCertList { requests, certificates, } => { println!("requests:"); if requests.is_empty() { println!(" none"); } else { for request in requests { println!( " {}\t{}\t{}\t{}", request.id, request.status, request.cert_kind, request.principals.join(",") ); } } println!("certificates:"); if certificates.is_empty() { println!(" none"); } else { for certificate in certificates { println!( " {}\t{}\t{}", certificate.id, certificate.request_id, certificate.certificate_fingerprint ); } } } ControlResponse::SshCertSynced { peer_node_id, peer_agent_id, endpoint_id, requests_imported, certificates_imported, allowed, reason, note, } => { if allowed { println!( "synced ssh cert metadata from {peer_node_id}: {requests_imported} requests, {certificates_imported} certificates" ); } else { println!("ssh cert metadata sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::SshRevocationAdded { revocation } => { println!("added ssh revocation: {}", revocation.id); println!("kind: {}", revocation.kind); println!("target: {}", revocation.target); if let Some(reason) = revocation.reason { println!("reason: {reason}"); } } ControlResponse::SshRevocationList { revocations } => { if revocations.is_empty() { println!("no ssh revocations"); } else { for revocation in revocations { println!( "{}\t{}\t{}\t{}", revocation.id, revocation.kind, revocation.target, revocation.reason.unwrap_or_default() ); } } } ControlResponse::SshRevocationExported { out, format, count, note, } => { println!("exported {count} ssh revocations to {}", out.display()); println!("format: {format}"); println!("note: {note}"); } ControlResponse::SshRevocationImported { revocations, format, count, note, } => { println!("imported {count} ssh revocations"); println!("format: {format}"); for revocation in revocations { println!( "{}\t{}\t{}", revocation.id, revocation.kind, revocation.target ); } println!("note: {note}"); } ControlResponse::SshRevocationSynced { peer_node_id, peer_agent_id, endpoint_id, revocations_imported, allowed, reason, note, } => { if allowed { println!("synced {revocations_imported} ssh revocations from {peer_node_id}"); } else { println!("ssh revocation sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::DbAdded { db } => { println!("registered db: {}", db.name); println!("id: {}", db.id); println!("resource: {}", db.resource); println!("path: {}", db.path); println!("sync_status: {}", db.sync_status); } ControlResponse::DbStatus { db } => { println!("db: {}", db.name); println!("id: {}", db.id); println!("resource: {}", db.resource); println!("path: {}", db.path); println!("path_exists: {}", db.path_exists); println!( "size_bytes: {}", db.size_bytes .map(|size| size.to_string()) .unwrap_or_else(|| "unknown".to_owned()) ); println!("schema_metadata: {}", db.schema_metadata); println!( "crsqlite_changes_available: {}", db.crsqlite_changes.available ); if let Some(count) = db.crsqlite_changes.change_count { println!("crsqlite_change_count: {count}"); } if let Some(version) = db.crsqlite_changes.max_db_version { println!("crsqlite_max_db_version: {version}"); } if let Some(error) = db.crsqlite_changes.error { println!("crsqlite_changes_error: {error}"); } println!("sync_status: {}", db.sync_status); } ControlResponse::DbChanges { db, batch } => { println!("db: {}", db.name); println!("changes: {}", batch.changes.len()); println!( "max_db_version: {}", batch .max_db_version .map(|version| version.to_string()) .unwrap_or_else(|| "none".to_owned()) ); println!("schema_metadata: {}", batch.schema_metadata); for change in batch.changes { println!( "{}\t{}\t{}", change.db_version, change.table_name, change.column_id ); } } ControlResponse::DbSynced { peer_node_id, peer_agent_id, endpoint_id, name, changes_received, changes_applied, max_db_version, schema_match, allowed, reason, note, } => { if allowed { println!("synced db changes for {name} from {peer_node_id}"); } else { println!("db sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("changes_received: {changes_received}"); println!("changes_applied: {changes_applied}"); println!( "max_db_version: {}", max_db_version .map(|version| version.to_string()) .unwrap_or_else(|| "none".to_owned()) ); println!("schema_match: {schema_match}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::KvCreated { kv } => { println!("created kv: {}", kv.name); println!("id: {}", kv.id); println!("resource: {}", kv.resource); println!("sync_status: {}", kv.sync_status); } ControlResponse::KvSet { entry } => { println!("set {} {}", entry.store, entry.key); } ControlResponse::KvGet { entry } => { if let Some(entry) = entry { println!("{}", entry.value); } else { println!("not found"); } } ControlResponse::KvSynced { peer_node_id, peer_agent_id, endpoint_id, name, entries_imported, allowed, reason, note, } => { if allowed { println!("synced kv {name} from {peer_node_id}: {entries_imported} entries"); } else { println!("kv sync denied for {name} by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::DocumentCreated { document } => { println!("created document: {}", document.name); println!("id: {}", document.id); println!("resource: {}", document.resource); println!("sync_status: {}", document.sync_status); println!("state_bytes: {}", document.state_bytes); } ControlResponse::DocumentStatus { document } => { println!("document: {}", document.name); println!("id: {}", document.id); println!("resource: {}", document.resource); println!("sync_status: {}", document.sync_status); println!("state_bytes: {}", document.state_bytes); } ControlResponse::DocumentSet { state } => { println!("updated document: {}", state.document.name); println!("state_bytes: {}", state.document.state_bytes); println!("updated_at_ms: {}", state.updated_at.0); } ControlResponse::DocumentGet { state } => { println!("{}", state.state_json); } ControlResponse::DocumentSynced { peer_node_id, peer_agent_id, endpoint_id, name, updated, allowed, reason, note, } => { if allowed { println!("synced document {name} from {peer_node_id}: updated={updated}"); } else { println!("document sync denied for {name} by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PubsubPublished { message } => { println!("published: {}", message.topic); println!("published_at_ms: {}", message.published_at.0); } ControlResponse::PubsubRemotePublished { peer_node_id, peer_agent_id, endpoint_id, message, allowed, reason, note, } => { if allowed { println!("published: {}", message.topic); println!("peer: {peer_node_id}"); println!("published_at_ms: {}", message.published_at.0); } else { println!("pubsub publish denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PubsubMessages { topic, messages, note, } => { println!("topic: {topic}"); println!("messages: {}", messages.len()); for message in messages { println!("{}\t{}", message.published_at.0, message.message); } println!("note: {note}"); } ControlResponse::PubsubRemoteMessages { peer_node_id, peer_agent_id, endpoint_id, topic, messages, allowed, reason, note, } => { if allowed { println!("topic: {topic}"); println!("peer: {peer_node_id}"); println!("messages: {}", messages.len()); for message in messages { println!("{}\t{}", message.published_at.0, message.message); } } else { println!("pubsub subscribe denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeListening { listener } => { println!("listening pipe: {}", listener.name); println!("id: {}", listener.id); println!("listened_at_ms: {}", listener.listened_at.0); println!("note: {}", listener.note); } ControlResponse::PipeConnected { connection } => { println!("pipe target: {}", connection.target); println!("local_listener_found: {}", connection.local_listener_found); println!("connected_at_ms: {}", connection.connected_at.0); println!("note: {}", connection.note); } ControlResponse::PipeRemoteListening { peer_node_id, peer_agent_id, endpoint_id, listener, allowed, reason, note, } => { if let Some(listener) = listener { println!("listening pipe: {}", listener.name); println!("peer: {peer_node_id}"); println!("id: {}", listener.id); println!("listened_at_ms: {}", listener.listened_at.0); } else { println!("pipe listen denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeRemoteConnected { peer_node_id, peer_agent_id, endpoint_id, connection, allowed, reason, note, } => { if allowed { println!("pipe target: {}", connection.target); println!("peer: {peer_node_id}"); println!("remote_listener_found: {}", connection.local_listener_found); println!("connected_at_ms: {}", connection.connected_at.0); } else { println!("pipe connect denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeSent { message, listener_found, note, } => { println!("listener_found: {listener_found}"); if let Some(message) = message { println!("pipe: {}", message.pipe); println!("received_at_ms: {}", message.received_at.0); print_pipe_message_data(&message)?; } println!("note: {note}"); } ControlResponse::PipeRemoteSent { peer_node_id, peer_agent_id, endpoint_id, message, listener_found, allowed, reason, note, } => { println!("peer: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("listener_found: {listener_found}"); if let Some(message) = message { println!("pipe: {}", message.pipe); println!("received_at_ms: {}", message.received_at.0); } println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeMessages { name, messages, drained, note, } => { println!("pipe: {name}"); println!("messages: {}", messages.len()); println!("drained: {drained}"); for message in messages { print_pipe_message_data(&message)?; } println!("note: {note}"); } ControlResponse::SshProxyConnected { peer_node_id, peer_agent_id, endpoint_id, connection, allowed, reason, note, } => { if allowed { println!("ssh proxy target: {peer_node_id}"); if let Some(connection) = connection { println!("connected_at_ms: {}", connection.connected_at.0); if let Some(local_sshd_target) = connection.local_sshd_target { println!("remote_sshd_target: {local_sshd_target}"); } println!( "admin_shell_available: {}", connection.admin_shell_available ); println!("connection_note: {}", connection.note); } } else { println!("ssh proxy denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::SshAdminShellOutput { peer_node_id, peer_agent_id, endpoint_id, command, output, allowed, reason, note, } => { if allowed { println!("{output}"); } else { println!("ssh admin shell denied by {peer_node_id}"); } println!("command: {command}"); println!("peer: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::NotImplemented { module, command } => { println!("{module} {command}: not implemented yet"); } ControlResponse::Error { message } => bail!(message), } Ok(()) } fn print_service_report(report: ServiceReport, json: bool) -> Result<()> { if json { println!( "{}", serde_json::json!({ "manager": report.manager.to_string(), "action": format!("{:?}", report.action), "service_name": report.service_name, "definition_path": report.definition_path, "definition": report.definition, "commands": report.commands, "note": report.note, }) ); return Ok(()); } println!("service: {}", report.service_name); println!("manager: {}", report.manager); println!("action: {:?}", report.action); if let Some(path) = report.definition_path { println!("definition: {}", path.display()); } if !report.commands.is_empty() { println!("commands:"); for command in report.commands { println!(" {}", shell_quote_command(&command)); } } if let Some(definition) = report.definition { println!("definition_body:"); print!("{definition}"); } println!("note: {}", report.note); Ok(()) } fn print_file_conflict(conflict: &geth_cas::FileConflict) { println!("root: {}", conflict.root); println!("resource: {}", conflict.resource); println!("path: {}", conflict.path); println!("kind: {}", conflict.kind.as_str()); println!("status: {}", conflict.status.as_str()); if let Some(hash) = &conflict.base_tree { println!("base_tree: {hash}"); } if let Some(hash) = &conflict.local_tree { println!("local_tree: {hash}"); } if let Some(hash) = &conflict.remote_tree { println!("remote_tree: {hash}"); } println!("detail: {}", conflict.detail); if let Some(resolution) = &conflict.resolution { println!("resolution: {}", resolution.as_str()); } if let Some(note) = &conflict.resolution_note { println!("resolution_note: {note}"); } } fn print_pipe_message_data(message: &geth_pipe::PipeMessage) -> Result<()> { let bytes = base64::engine::general_purpose::STANDARD .decode(&message.data_base64) .context("decode pipe message")?; match String::from_utf8(bytes) { Ok(text) => println!("{text}"), Err(error) => println!( "base64:{}", base64::engine::general_purpose::STANDARD.encode(error.into_bytes()) ), } Ok(()) } fn pipe_send_payload_base64(message: Option, input: Option) -> Result { match (message, input) { (Some(message), None) => Ok(base64::engine::general_purpose::STANDARD.encode(message)), (None, Some(path)) if path.as_os_str() == "-" => { let mut bytes = Vec::new(); std::io::stdin() .read_to_end(&mut bytes) .context("read pipe payload from stdin")?; Ok(base64::engine::general_purpose::STANDARD.encode(bytes)) } (None, Some(path)) => { let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; Ok(base64::engine::general_purpose::STANDARD.encode(bytes)) } (Some(_), Some(_)) => bail!("pipe send accepts either MESSAGE or --in, not both"), (None, None) => bail!("pipe send requires MESSAGE or --in ; use --in - for stdin"), } } fn shell_quote_command(command: &[String]) -> String { command .iter() .map(|arg| { if arg .bytes() .all(|byte| byte.is_ascii_alphanumeric() || b"-_./:=+@,".contains(&byte)) { arg.clone() } else { format!("'{}'", arg.replace('\'', "'\\''")) } }) .collect::>() .join(" ") }