use anyhow::{Result, ensure}; use clap::{Parser, Subcommand}; use roentgen::{ bus::Client, controller::Operation, model::{Status, SubscriptionSummary}, }; use std::io::{self, BufRead, IsTerminal, Read, Write}; #[derive(Parser)] #[command( version, about = "Control local-machine proxying through the Roentgen service" )] struct Cli { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Command { /// Open the terminal interface. Tui, /// Read sanitized requested state, routing, and proxy health. Status { #[arg(long)] json: bool, }, #[command(subcommand)] Subscription(SubscriptionCommand), #[command(subcommand)] Configuration(ConfigurationCommand), Enable, Disable, /// Measure HTTPS delay through a configuration without enabling host proxying. Ping { id: Option, }, /// Run a bounded manual download through a configuration. Speed { id: Option, }, Logs { #[arg(long, required = true)] editor: bool, }, /// Install systemwide service assets. Requires root. Install, Uninstall { #[arg(long)] purge: bool, }, /// Explicitly persist disabled intent and release only owned networking. Recover { #[arg(long, required = true)] disable: bool, }, #[command(hide = true)] Daemon, #[command(hide = true)] Guard, #[command(hide = true)] XrayPreflight, } #[derive(Subcommand)] enum SubscriptionCommand { /// Read the secret URL from a prompt or one stdin line. Add { #[arg(long)] name: Option, }, List, Edit { name: String, }, Delete { name: String, }, Update { #[arg(required_unless_present = "all", conflicts_with = "all")] name: Option, #[arg(long)] all: bool, }, } #[derive(Subcommand)] enum ConfigurationCommand { List { #[arg(long)] subscription: Option, }, Select { id: String, }, Unset, } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_ansi(false) .init(); if let Err(error) = run(Cli::parse()).await { eprintln!("roentgen: {error}"); std::process::exit(1); } } async fn run(cli: Cli) -> Result<()> { let command = cli.command.unwrap_or(Command::Tui); match command { Command::Install => return roentgen::installation::install().await, Command::Uninstall { purge } => return roentgen::installation::uninstall(purge).await, Command::Recover { .. } => return roentgen::installation::recover().await, Command::Daemon => return roentgen::daemon::run().await, Command::Guard => return roentgen::daemon::guard().await, Command::XrayPreflight => return roentgen::xray::preflight().await, _ => {} } let client = Client::system().await?; let interactive = io::stdin().is_terminal() && io::stderr().is_terminal(); let mutation = matches!( command, Command::Enable | Command::Disable | Command::Ping { .. } | Command::Speed { .. } | Command::Subscription( SubscriptionCommand::Add { .. } | SubscriptionCommand::Edit { .. } | SubscriptionCommand::Delete { .. } | SubscriptionCommand::Update { .. } ) | Command::Configuration( ConfigurationCommand::Select { .. } | ConfigurationCommand::Unset ) ); let _agent = roentgen::authentication::TerminalAgent::start(interactive && mutation)?; match command { Command::Tui => roentgen::tui::run(client).await?, Command::Status { json } => { let status: Status = client.read("Status").await?; if json { println!("{}", serde_json::to_string(&status)?); } else { println!( "Requested: {}\nSelected: {}\nRouting: {}\nHealth: {}", if status.requested_enabled { "enabled" } else { "disabled" }, status.selected_configuration.as_deref().unwrap_or("unset"), status.routing, status.health ); if let Some(error) = status.last_error { println!("Error: {error}"); } } } Command::Subscription(SubscriptionCommand::List) => { for subscription in client .read::>("Subscriptions") .await? { println!("{}\t{}", subscription.id, subscription.name); } } Command::Subscription(SubscriptionCommand::Add { name }) => { let name = name.unwrap_or_else(|| { format!("Subscription-{}", &uuid::Uuid::new_v4().to_string()[..8]) }); let url = secret_line("Subscription URL: ")?; client .change(Operation::AddSubscription { name, url }, interactive) .await?; } Command::Subscription(SubscriptionCommand::Edit { name }) => { let url = secret_line("New subscription URL: ")?; client .change(Operation::EditSubscription { name, url }, interactive) .await?; } Command::Subscription(SubscriptionCommand::Delete { name }) => { client .change(Operation::DeleteSubscription { name }, interactive) .await? } Command::Subscription(SubscriptionCommand::Update { name, .. }) => { client .change(Operation::Refresh { name }, interactive) .await? } Command::Configuration(ConfigurationCommand::List { subscription }) => { for entry in client.configurations(subscription.as_deref()).await? { println!( "{}\t{}\t{}\t{}\t{}", if entry.selected { "*" } else { " " }, entry.id, entry.name, entry.status, entry.explanation.as_deref().unwrap_or("") ); } } Command::Configuration(ConfigurationCommand::Select { id }) => { client.change(Operation::Select { id }, interactive).await? } Command::Configuration(ConfigurationCommand::Unset) => { client.change(Operation::Unset, interactive).await? } Command::Enable => client.change(Operation::Enable, interactive).await?, Command::Disable => client.change(Operation::Disable, interactive).await?, Command::Ping { id } => println!( "{}", client.diagnostic(id.as_deref(), false, interactive).await? ), Command::Speed { id } => println!( "{}", client.diagnostic(id.as_deref(), true, interactive).await? ), Command::Logs { .. } => roentgen::editor::open(&client.read::>("Logs").await?)?, _ => unreachable!(), } Ok(()) } fn secret_line(prompt: &str) -> Result { if io::stdin().is_terminal() { eprint!("{prompt}"); io::stderr().flush()?; } let mut line = String::new(); let count = io::stdin() .lock() .take(16 * 1024 + 1) .read_line(&mut line)?; ensure!( count > 0 && count <= 16 * 1024, "Provide one subscription URL on stdin, up to 16 KiB" ); Ok(line.trim_end_matches(['\r', '\n']).to_owned()) }