diff --git a/linkup-cli/install.sh b/linkup-cli/install.sh index 98cfe27..17ebad7 100755 --- a/linkup-cli/install.sh +++ b/linkup-cli/install.sh @@ -1,20 +1,22 @@ -if command -v linkup &>/dev/null; then - echo "Linkup is already installed. To update it, run 'linkup update'." +#!/bin/sh + +if command -v -- "linkup" >/dev/null 2>&1; then + printf '%s\n' "Linkup is already installed. To update it, run 'linkup update'." 1>&2 exit 0 fi # region: Dependencies # TODO: Maybe we want this script to be able to install the dependencies as well? -if ! command -v cloudflared &>/dev/null; then - echo "WARN: 'cloudflared' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" +if ! command -v -- "cloudflared" >/dev/null 2>&1; then + printf '%s\n' "WARN: 'cloudflared' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" 1>&2 fi -if ! command -v caddy &>/dev/null; then - echo "WARN: 'caddy' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://caddyserver.com/docs/install" +if ! command -v -- "caddy" >/dev/null 2>&1; then + printf '%s\n' "WARN: 'caddy' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://caddyserver.com/docs/install" 1>&2 fi -if ! command -v dnsmasq &>/dev/null; then - echo "WARN: 'dnsmasq' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://thekelleys.org.uk/dnsmasq/doc.html" +if ! command -v -- "dnsmasq" >/dev/null 2>&1; then + printf '%s\n' "WARN: 'dnsmasq' is not installed. Some features will not work as expected. Please install it.\nFor more info check: https://thekelleys.org.uk/dnsmasq/doc.html" 1>&2 fi # endregion: Dependencies @@ -23,26 +25,33 @@ ARCH=$(uname -m) FETCH_OS='' FETCH_ARCH='' -if [[ "$OS" == "Darwin"* ]]; then +case "$OS" in +Darwin*) FETCH_OS='apple-darwin' - - if [[ "$ARCH" == "arm64" ]]; then + case "$ARCH" in + arm64 | aarch64) FETCH_ARCH='aarch64' - elif [[ "$arch" == "x86_64" ]]; then + ;; + x86_64) FETCH_ARCH='x86_64' - fi -elif [[ "$OS" == "Linux"* ]]; then - FETCH_OS='unknown-linux' - - if [[ "$ARCH" == "arm64" ]]; then + ;; + esac + ;; +Linux*) + FETCH_OS='unknown-linux-gnu' + case "$ARCH" in + arm64 | aarch64) FETCH_ARCH='aarch64' - elif [[ "$arch" == "x86_64" ]]; then + ;; + x86_64) FETCH_ARCH='x86_64' - fi -fi + ;; + esac + ;; +esac -if [[ -z "$FETCH_OS" || -z "$FETCH_ARCH" ]]; then - echo "Unsupported OS/Arch combination: $OS/$ARCH" +if [ -z "$FETCH_OS" ] || [ -z "$FETCH_ARCH" ]; then + printf '%s\n' "Unsupported OS/Arch combination: $OS/$ARCH" 1>&2 exit 1 fi @@ -56,25 +65,29 @@ FILE_DOWNLOAD_URL=$( ) if [ -z "$FILE_DOWNLOAD_URL" ]; then - echo "Could not find file with pattern '$LOOKUP_FILE_DOWNLOAD_URL' on the latest GitHub release." + printf '%s\n' "Could not find file with pattern '$LOOKUP_FILE_DOWNLOAD_URL' on the latest GitHub release." 1>&2 exit 1 fi -echo "Downloading: $FILE_DOWNLOAD_URL" +printf '%s\n' "Downloading: $FILE_DOWNLOAD_URL" 1>&2 curl -sLO --output-dir "/tmp" $FILE_DOWNLOAD_URL LOCAL_FILE_PATH="/tmp/$(basename $FILE_DOWNLOAD_URL)" -echo "Decompressing $LOCAL_FILE_PATH" +printf '%s\n' "Decompressing $LOCAL_FILE_PATH" 1>&2 tar -xzf $LOCAL_FILE_PATH -C /tmp mkdir -p $HOME/.linkup/bin mv /tmp/linkup $HOME/.linkup/bin/ -echo "Linkup installed on $HOME/.linkup/bin/linkup" +printf '%s\n' "Linkup installed on $HOME/.linkup/bin/linkup" 1>&2 rm "$LOCAL_FILE_PATH" -if [[ ":$PATH:" != *":$HOME/.linkup/bin:"* ]]; then +case ":$PATH:" in +*":$HOME/.linkup/bin:"*) + # PATH already contains the directory + ;; +*) SHELL_NAME=$(basename "$SHELL") case "$SHELL_NAME" in bash) @@ -91,7 +104,8 @@ if [[ ":$PATH:" != *":$HOME/.linkup/bin:"* ]]; then ;; esac - echo "Adding Linkup bin to PATH in $PROFILE_FILE" - echo "\n# Linkup bin\nexport PATH=\$PATH:\$HOME/.linkup/bin" >>"$PROFILE_FILE" - echo "Please source your profile file or restart your terminal to apply the changes." -fi + printf '%s\n' "Adding Linkup bin to PATH in $PROFILE_FILE" 1>&2 + printf "\n# Linkup bin\nexport PATH=\$PATH:\$HOME/.linkup/bin" >>"$PROFILE_FILE" + printf '%s\n' "Please source your profile file or restart your terminal to apply the changes." 1>&2 + ;; +esac diff --git a/linkup-cli/src/commands/health.rs b/linkup-cli/src/commands/health.rs index 37acb57..2017a4a 100644 --- a/linkup-cli/src/commands/health.rs +++ b/linkup-cli/src/commands/health.rs @@ -76,6 +76,7 @@ struct EnvironmentVariables { cf_api_token: bool, cf_zone_id: bool, cf_account_id: bool, + cert_storage_redis_url: bool, } impl EnvironmentVariables { @@ -84,6 +85,7 @@ impl EnvironmentVariables { cf_api_token: env::var("LINKUP_CF_API_TOKEN").is_ok(), cf_zone_id: env::var("LINKUP_CLOUDFLARE_ZONE_ID").is_ok(), cf_account_id: env::var("LINKUP_CLOUDFLARE_ACCOUNT_ID").is_ok(), + cert_storage_redis_url: env::var("LINKUP_CERT_STORAGE_REDIS_URL").is_ok(), } } } @@ -290,6 +292,13 @@ impl Display for Health { writeln!(f, "{}", "MISSING".yellow())?; } + write!(f, " - LINKUP_CERT_STORAGE_REDIS_URL ")?; + if self.environment_variables.cert_storage_redis_url { + writeln!(f, "{}", "OK".blue())?; + } else { + writeln!(f, "{}", "MISSING".yellow())?; + } + writeln!(f, "{}", "Linkup:".bold().italic())?; writeln!(f, " Version: {}", self.linkup.version)?; writeln!( diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index 3817642..6c40eff 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -6,8 +6,9 @@ use std::{ use clap::Subcommand; use crate::{ + is_sudo, local_config::{config_path, get_config}, - services, CliError, Result, LINKUP_CF_TLS_API_ENV_VAR, + services, sudo_su, CliError, Result, LINKUP_CF_TLS_API_ENV_VAR, }; #[derive(clap::Args)] @@ -50,6 +51,8 @@ pub fn install(config_arg: &Option) -> Result<()> { println!(" - Ensure there is a folder /etc/resolvers"); println!(" - Create file(s) for /etc/resolver/"); println!(" - Flush DNS cache"); + + sudo_su()?; } ensure_resolver_dir()?; @@ -119,6 +122,8 @@ fn install_resolvers(resolve_domains: &[String]) -> Result<()> { } flush_dns_cache()?; + + #[cfg(target_os = "macos")] kill_dns_responder()?; Ok(()) @@ -141,6 +146,8 @@ fn uninstall_resolvers(resolve_domains: &[String]) -> Result<()> { } flush_dns_cache()?; + + #[cfg(target_os = "macos")] kill_dns_responder()?; Ok(()) @@ -164,22 +171,30 @@ pub fn list_resolvers() -> std::result::Result, std::io::Error> { } fn flush_dns_cache() -> Result<()> { - let status_flush = Command::new("sudo") - .args(["dscacheutil", "-flushcache"]) + #[cfg(target_os = "linux")] + let status_flush = Command::new("resolvectl") + .args(["flush-caches"]) + .status() + .map_err(|_err| { + CliError::LocalDNSInstall("Failed to run resolvectl flush-caches".into()) + })?; + + #[cfg(target_os = "macos")] + let status_flush = Command::new("dscacheutil") + .args(["-flushcache"]) .status() .map_err(|_err| { CliError::LocalDNSInstall("Failed to run dscacheutil -flushcache".into()) })?; if !status_flush.success() { - return Err(CliError::LocalDNSInstall( - "Failed to run dscacheutil -flushcache".into(), - )); + return Err(CliError::LocalDNSInstall("Failed flush DNS cache".into())); } Ok(()) } +#[cfg(target_os = "macos")] fn kill_dns_responder() -> Result<()> { let status_kill_responder = Command::new("sudo") .args(["killall", "-HUP", "mDNSResponder"]) @@ -196,19 +211,3 @@ fn kill_dns_responder() -> Result<()> { Ok(()) } - -fn is_sudo() -> bool { - let sudo_check = Command::new("sudo") - .arg("-n") - .arg("true") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - if let Ok(exit_status) = sudo_check { - return exit_status.success(); - } - - false -} diff --git a/linkup-cli/src/commands/start.rs b/linkup-cli/src/commands/start.rs index 08cdef4..9936958 100644 --- a/linkup-cli/src/commands/start.rs +++ b/linkup-cli/src/commands/start.rs @@ -55,6 +55,25 @@ pub async fn start<'a>( let caddy = services::Caddy::new(); let dnsmasq = services::Dnsmasq::new(); + #[cfg(target_os = "linux")] + { + use crate::{is_sudo, sudo_su}; + match (caddy.should_start(&state.domain_strings()), is_sudo()) { + // Should start Caddy and is not sudo + (Ok(true), false) => { + println!( + "On linux binding port 443 and 80 requires sudo. And this is necessary to start caddy." + ); + + sudo_su()?; + } + // Should not start Caddy or should start Caddy but is already sudo + (Ok(false), _) | (Ok(true), true) => (), + // Can't check if should start Caddy + (Err(error), _) => log::error!("Failed to check if should start Caddy: {}", error), + } + } + let mut display_thread: Option> = None; let display_channel = sync::mpsc::channel::(); diff --git a/linkup-cli/src/commands/status.rs b/linkup-cli/src/commands/status.rs index 9abc919..bf3a68a 100644 --- a/linkup-cli/src/commands/status.rs +++ b/linkup-cli/src/commands/status.rs @@ -275,10 +275,10 @@ pub fn format_state_domains(session_name: &str, domains: &[StorableDomain]) -> V .map(|d| d.domain.clone()) .collect::>(); - return filtered_domains + filtered_domains .iter() .map(|domain| format!("https://{}.{}", session_name, domain.clone())) - .collect(); + .collect() } fn linkup_services(state: &LocalState) -> Vec { diff --git a/linkup-cli/src/commands/uninstall.rs b/linkup-cli/src/commands/uninstall.rs index edc393a..6f723a8 100644 --- a/linkup-cli/src/commands/uninstall.rs +++ b/linkup-cli/src/commands/uninstall.rs @@ -13,10 +13,12 @@ pub fn uninstall(_args: &Args) -> Result<(), CliError> { log::debug!("Removing linkup folder: {}", linkup_dir.display()); fs::remove_dir_all(linkup_dir)?; - let exe_path = fs::canonicalize(std::env::current_exe()?)?; + let exe_path = fs::canonicalize(std::env::current_exe()?)? + .display() + .to_string(); - log::debug!("Linkup exe path: {}", exe_path.display()); - if exe_path.display().to_string().contains("homebrew") { + log::debug!("Linkup exe path: {}", &exe_path); + if exe_path.contains("homebrew") { log::debug!("Uninstalling linkup from Homebrew"); process::Command::new("brew") @@ -25,6 +27,15 @@ pub fn uninstall(_args: &Args) -> Result<(), CliError> { .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .status()?; + } else if exe_path.contains(".cargo") { + log::debug!("Uninstalling linkup from Cargo"); + + process::Command::new("cargo") + .args(["uninstall", "linkup-cli"]) + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status()?; } println!("linkup uninstalled!"); diff --git a/linkup-cli/src/main.rs b/linkup-cli/src/main.rs index 1bee09c..e0c4b2d 100644 --- a/linkup-cli/src/main.rs +++ b/linkup-cli/src/main.rs @@ -1,4 +1,4 @@ -use std::{env, fs, io::ErrorKind, path::PathBuf}; +use std::{env, fs, io::ErrorKind, path::PathBuf, process}; use clap::{Parser, Subcommand}; use colored::Colorize; @@ -52,6 +52,36 @@ fn ensure_linkup_dir() -> Result<()> { } } +fn is_sudo() -> bool { + let sudo_check = process::Command::new("sudo") + .arg("-n") + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .arg("true") + .status(); + + if let Ok(exit_status) = sudo_check { + return exit_status.success(); + } + + false +} + +fn sudo_su() -> Result<()> { + let status = process::Command::new("sudo") + .arg("su") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status()?; + + if !status.success() { + return Err(CliError::StartErr("failed to sudo".to_string())); + } + + Ok(()) +} + pub type Result = std::result::Result; #[derive(Error, Debug)] diff --git a/linkup-cli/src/services/caddy.rs b/linkup-cli/src/services/caddy.rs index 6d76ba3..348b64a 100644 --- a/linkup-cli/src/services/caddy.rs +++ b/linkup-cli/src/services/caddy.rs @@ -13,17 +13,22 @@ use super::{local_server::LINKUP_LOCAL_SERVER_PORT, BackgroundService}; #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Failed to start the Caddy service")] + Starting, #[error("Failed while handing file: {0}")] FileHandling(#[from] std::io::Error), #[error("Cloudflare TLS API token is required for local-dns Cloudflare TLS certificates.")] MissingTlsApiTokenEnv, #[error("Redis shared storage is a new feature! You need to uninstall and reinstall local-dns to use it.")] MissingRedisInstalation, + #[error("Failed to stop pid: {0}")] + StoppingPid(#[from] signal::PidError), } pub struct Caddy { caddyfile_path: PathBuf, - logfile_path: PathBuf, + stdout_file_path: PathBuf, + stderr_file_path: PathBuf, pidfile_path: PathBuf, } @@ -31,21 +36,26 @@ impl Caddy { pub fn new() -> Self { Self { caddyfile_path: linkup_file_path("Caddyfile"), - logfile_path: linkup_file_path("caddy-log"), + stdout_file_path: linkup_file_path("caddy-stdout"), + stderr_file_path: linkup_file_path("caddy-stderr"), pidfile_path: linkup_file_path("caddy-pid"), } } pub fn install_extra_packages() { - Command::new("caddy") - .args(["add-package", "github.com/caddy-dns/cloudflare"]) + Command::new("sudo") + .args(["caddy", "add-package", "github.com/caddy-dns/cloudflare"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .unwrap(); - Command::new("caddy") - .args(["add-package", "github.com/pberkel/caddy-storage-redis"]) + Command::new("sudo") + .args([ + "caddy", + "add-package", + "github.com/pberkel/caddy-storage-redis", + ]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() @@ -66,30 +76,48 @@ impl Caddy { self.write_caddyfile(&domains_and_subdomains)?; - // Clear previous log file on startup - fs::write(&self.logfile_path, "")?; + let stdout_file = fs::File::create(&self.stdout_file_path)?; + let stderr_file = fs::File::create(&self.stderr_file_path)?; - Command::new("caddy") + #[cfg(target_os = "macos")] + let status = Command::new("caddy") .current_dir(linkup_dir_path()) .arg("start") .arg("--pidfile") .arg(&self.pidfile_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stdout(stdout_file) + .stderr(stderr_file) .status()?; + #[cfg(target_os = "linux")] + let status = { + // To make sure that the local user is the owner of the pidfile and not root, + // we create it before running the caddy command. + let _ = fs::File::create(&self.pidfile_path)?; + + Command::new("sudo") + .current_dir(linkup_dir_path()) + .arg("caddy") + .arg("start") + .arg("--pidfile") + .arg(&self.pidfile_path) + .stdin(Stdio::null()) + .stdout(stdout_file) + .stderr(stderr_file) + .status()? + }; + + if !status.success() { + return Err(Error::Starting); + } + Ok(()) } pub fn stop(&self) -> Result<(), Error> { log::debug!("Stopping {}", Self::NAME); - Command::new("caddy") - .current_dir(linkup_dir_path()) - .arg("stop") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status()?; + signal::stop_pid_file(&self.pidfile_path, signal::Signal::SIGTERM)?; Ok(()) } @@ -139,7 +167,7 @@ impl Caddy { }} }} ", - self.logfile_path.display(), + self.stdout_file_path.display(), redis_storage, domains.join(", "), LINKUP_LOCAL_SERVER_PORT, @@ -159,7 +187,7 @@ impl Caddy { output_str.contains("redis") } - fn should_start(&self, domains: &[String]) -> Result { + pub fn should_start(&self, domains: &[String]) -> Result { let resolvers = local_dns::list_resolvers()?; Ok(domains.iter().any(|domain| resolvers.contains(domain))) @@ -233,8 +261,8 @@ impl BackgroundService for Caddy { } pub fn is_installed() -> bool { - let res = Command::new("command") - .args(["-v", "caddy"]) + let res = Command::new("which") + .args(["caddy"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()) diff --git a/linkup-cli/src/services/cloudflare_tunnel/mod.rs b/linkup-cli/src/services/cloudflare_tunnel/mod.rs index c614212..59cf6d3 100644 --- a/linkup-cli/src/services/cloudflare_tunnel/mod.rs +++ b/linkup-cli/src/services/cloudflare_tunnel/mod.rs @@ -167,10 +167,10 @@ impl CloudflareTunnel { fn url(&self, linkup_session_name: &str) -> Result { if Self::use_paid_tunnels() { - return Ok(Url::parse( + Ok(Url::parse( format!("https://tunnel-{}.mentimeter.dev", linkup_session_name).as_str(), ) - .expect("Failed to parse tunnel URL")); + .expect("Failed to parse tunnel URL")) } else { let tunnel_url_re = Regex::new(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com") .expect("Failed to compile regex"); @@ -374,8 +374,8 @@ impl BackgroundService for CloudflareTunnel { } pub fn is_installed() -> bool { - let res = Command::new("command") - .args(["-v", "cloudflared"]) + let res = Command::new("which") + .args(["cloudflared"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()) diff --git a/linkup-cli/src/services/dnsmasq.rs b/linkup-cli/src/services/dnsmasq.rs index e788206..3df76d1 100644 --- a/linkup-cli/src/services/dnsmasq.rs +++ b/linkup-cli/src/services/dnsmasq.rs @@ -170,8 +170,8 @@ impl BackgroundService for Dnsmasq { } pub fn is_installed() -> bool { - let res = Command::new("command") - .args(["-v", "dnsmasq"]) + let res = Command::new("which") + .args(["dnsmasq"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()) diff --git a/linkup-cli/src/signal.rs b/linkup-cli/src/signal.rs index 44bfa6e..e4264f0 100644 --- a/linkup-cli/src/signal.rs +++ b/linkup-cli/src/signal.rs @@ -2,6 +2,7 @@ use nix::sys::signal::kill; use nix::unistd::Pid; use std::fs::{self, File}; use std::path::Path; +use std::process; use std::str::FromStr; use thiserror::Error; @@ -52,18 +53,16 @@ pub fn get_running_pid(file_path: &Path) -> Option { Err(_) => return None, }; - let pid = match u32::from_str(&pid) { - Ok(pid) => pid, - Err(_) => return None, // TODO: Do we want to be loud about this? - }; - - let system = sysinfo::System::new_with_specifics( - sysinfo::RefreshKind::new().with_processes(sysinfo::ProcessRefreshKind::everything()), - ); - - system - .process(sysinfo::Pid::from_u32(pid)) - .map(|_| pid.to_string()) + match process::Command::new("ps") + .args(["-p", &pid, "-o", "comm="]) + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + { + Ok(status) if status.success() => Some(pid), + _ => None, + } } pub fn stop_pid_file(pid_file: &Path, signal: Signal) -> Result<(), PidError> {