geth/crates/geth-cli/src/lib.rs

2895 lines
91 KiB
Rust

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<PathBuf>,
#[arg(long)]
signing_key: Option<PathBuf>,
#[arg(long, default_value = "owner")]
owner: String,
#[arg(long, default_value = "local")]
node_name: String,
#[arg(long = "capability")]
capabilities: Vec<String>,
},
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<PathBuf>,
#[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<PathBuf>,
},
}
#[derive(Debug, Subcommand)]
pub enum SyncCommand {
Status,
Now { node: Option<String> },
}
#[derive(Debug, Subcommand)]
pub enum NodeCommand {
Id,
Status,
List,
Enroll {
#[command(subcommand)]
command: NodeEnrollCommand,
},
Rename {
node: String,
name: String,
#[arg(long)]
signing_key: Option<PathBuf>,
},
Revoke {
node: String,
#[arg(long)]
signing_key: Option<PathBuf>,
},
Grant {
node: String,
resource: String,
capability: String,
#[arg(long)]
grant_id: Option<String>,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
},
RevokeGrant {
resource: String,
grant_id: String,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
},
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<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
out: Option<PathBuf>,
},
Submit {
owner_node: String,
#[arg(long)]
request_id: Option<String>,
#[arg(long)]
path: Option<PathBuf>,
},
Import {
path: PathBuf,
},
List {
#[arg(long)]
status: Option<String>,
},
Approve {
request_id: String,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
#[arg(long)]
node_name: Option<String>,
#[arg(long = "capability")]
capabilities: Vec<String>,
},
Sync {
owner_node: String,
},
}
#[derive(Debug, Subcommand)]
pub enum PeerCommand {
Export {
#[arg(long)]
out: Option<PathBuf>,
},
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<PathBuf>,
#[arg(long)]
signing_key: Option<PathBuf>,
},
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<String>,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
},
Revoke {
resource: String,
grant_id: String,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
},
}
#[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<String>,
#[arg(long)]
expires_at_ms: Option<i64>,
},
List,
Challenge {
resource: String,
#[arg(long = "capability", required = true)]
capabilities: Vec<String>,
},
Prove {
token: String,
resource: String,
#[arg(long)]
nonce: String,
#[arg(long = "capability", required = true)]
capabilities: Vec<String>,
},
Verify {
token: String,
resource: String,
#[arg(long)]
nonce: String,
#[arg(long)]
response: String,
#[arg(long = "capability", required = true)]
capabilities: Vec<String>,
},
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<String>,
},
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<String>,
},
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<String>,
#[arg(long)]
local_tree: Option<String>,
#[arg(long)]
remote_tree: Option<String>,
},
List {
#[arg(long)]
root: Option<String>,
},
Resolve {
conflict_id: String,
resolution: String,
#[arg(long)]
note: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum KvCommand {
Create {
name: String,
},
Set {
name: String,
key: String,
value: String,
#[arg(long)]
subject: Option<String>,
},
Get {
name: String,
key: String,
},
Sync {
node: String,
name: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum PubsubCommand {
Pub {
topic: String,
message: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
Sub {
topic: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum PipeCommand {
Listen {
name: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
Connect {
target: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
ForwardTcp {
#[arg(long)]
listen: String,
#[arg(long)]
node: String,
#[arg(long)]
target: String,
#[arg(long)]
bearer_secret: Option<String>,
},
ForwardUnix {
#[arg(long)]
listen: PathBuf,
#[arg(long)]
node: String,
#[arg(long)]
target: PathBuf,
#[arg(long)]
bearer_secret: Option<String>,
},
Send {
target: String,
message: Option<String>,
#[arg(long = "in", value_name = "PATH")]
input: Option<PathBuf>,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
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<i64>,
#[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<String>,
},
}
#[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<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum SshCommand {
Proxy {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
AdminShell {
node: String,
command: String,
#[arg(long)]
bearer_secret: Option<String>,
},
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<String>,
#[arg(long)]
valid_for: Option<String>,
#[arg(long)]
renewal_of: Option<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
subject: Option<String>,
},
Requests {
#[arg(long)]
subject: Option<String>,
},
Approve {
request_id: String,
#[arg(long)]
ca_key: PathBuf,
#[arg(long)]
valid_for: Option<String>,
#[arg(long)]
serial: Option<u64>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
sign: bool,
#[arg(long)]
subject: Option<String>,
},
Import {
request_id: String,
#[arg(long)]
cert: PathBuf,
#[arg(long)]
subject: Option<String>,
},
List {
#[arg(long)]
subject: Option<String>,
},
Sync {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum SshRevocationCommand {
Add {
kind: String,
target: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
subject: Option<String>,
},
List {
#[arg(long)]
subject: Option<String>,
},
Export {
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = "jsonl")]
format: String,
#[arg(long)]
ca_public: Option<PathBuf>,
#[arg(long)]
subject: Option<String>,
},
Import {
path: PathBuf,
#[arg(long, default_value = "jsonl")]
format: String,
#[arg(long)]
subject: Option<String>,
},
Sync {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[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<ControlRequest> {
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<ServiceReport> {
Ok(match command {
ServiceCommand::Install {
manager,
bin,
start,
} => {
geth_node::init_node(paths).context("initialize geth home before service install")?;
let manager = manager.parse::<ServiceManager>()?;
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::<ServiceManager>()?)?
}
ServiceCommand::Start { manager } => {
geth_node::service::start_user_service(manager.parse::<ServiceManager>()?)?
}
ServiceCommand::Stop { manager } => {
geth_node::service::stop_user_service(manager.parse::<ServiceManager>()?)?
}
ServiceCommand::Status { manager } => {
geth_node::service::status_user_service(manager.parse::<ServiceManager>()?)?
}
ServiceCommand::Print { manager, bin } => {
let executable = service_executable(bin)?;
geth_node::service::print_user_service(
paths,
manager.parse::<ServiceManager>()?,
&executable,
)?
}
})
}
fn service_executable(bin: Option<PathBuf>) -> Result<PathBuf> {
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);
for backend in status.native_backends {
println!(
"native backend {}: {} target {} {} ({})",
backend.module,
backend.current_backend,
backend.target_crate,
backend.target_version,
backend.status
);
println!(
"native backend {} blocker: {}",
backend.module, backend.blocker
);
}
}
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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<String>, input: Option<PathBuf>) -> Result<String> {
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 <path>; 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::<Vec<_>>()
.join(" ")
}