diff options
| author | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:10:41 +0400 |
|---|---|---|
| committer | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:10:41 +0400 |
| commit | d608ffeeda1a479c9dc0e5f6ad269ba07a510d7f (patch) | |
| tree | 3824ec9da5ca0adac913cc47e3e20054c3dc843a /src/main.rs | |
| parent | 40cf3276cdc74bf6f1795990ed600891a637d1fe (diff) | |
Implement Linux proxy controller, subscription state, and CLI/TUI
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 244 |
1 files changed, 242 insertions, 2 deletions
diff --git a/src/main.rs b/src/main.rs index e7a11a9..7ad2d81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,243 @@ -fn main() { - println!("Hello, world!"); +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<Command>, +} + +#[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<String>, + }, + /// Run a bounded manual download through a configuration. + Speed { + id: Option<String>, + }, + 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, +} + +#[derive(Subcommand)] +enum SubscriptionCommand { + /// Read the secret URL from a prompt or one stdin line. + Add { + #[arg(long)] + name: Option<String>, + }, + List, + Edit { + name: String, + }, + Delete { + name: String, + }, + Update { + #[arg(required_unless_present = "all", conflicts_with = "all")] + name: Option<String>, + #[arg(long)] + all: bool, + }, +} +#[derive(Subcommand)] +enum ConfigurationCommand { + List { + #[arg(long)] + subscription: Option<String>, + }, + 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, + _ => {} + } + 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::<Vec<SubscriptionSummary>>("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::<Vec<String>>("Logs").await?)?, + _ => unreachable!(), + } + Ok(()) +} + +fn secret_line(prompt: &str) -> Result<String> { + 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()) } |