summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs244
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())
}