Skip to content

Commit

Permalink
feat: improve Linux support (#130)
Browse files Browse the repository at this point in the history
There has been some assumptions on the Linux environment that weren't
correct and needed fixes.
This PR should address some of them and hopefully make it more
straightforward to work with it on Linux.
  • Loading branch information
augustoccesar authored Jan 3, 2025
1 parent d65f7a1 commit da032b6
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 98 deletions.
76 changes: 45 additions & 31 deletions linkup-cli/install.sh
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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
9 changes: 9 additions & 0 deletions linkup-cli/src/commands/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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!(
Expand Down
43 changes: 21 additions & 22 deletions linkup-cli/src/commands/local_dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -50,6 +51,8 @@ pub fn install(config_arg: &Option<String>) -> Result<()> {
println!(" - Ensure there is a folder /etc/resolvers");
println!(" - Create file(s) for /etc/resolver/<domain>");
println!(" - Flush DNS cache");

sudo_su()?;
}

ensure_resolver_dir()?;
Expand Down Expand Up @@ -119,6 +122,8 @@ fn install_resolvers(resolve_domains: &[String]) -> Result<()> {
}

flush_dns_cache()?;

#[cfg(target_os = "macos")]
kill_dns_responder()?;

Ok(())
Expand All @@ -141,6 +146,8 @@ fn uninstall_resolvers(resolve_domains: &[String]) -> Result<()> {
}

flush_dns_cache()?;

#[cfg(target_os = "macos")]
kill_dns_responder()?;

Ok(())
Expand All @@ -164,22 +171,30 @@ pub fn list_resolvers() -> std::result::Result<Vec<String>, 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"])
Expand All @@ -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
}
19 changes: 19 additions & 0 deletions linkup-cli/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JoinHandle<()>> = None;
let display_channel = sync::mpsc::channel::<bool>();

Expand Down
4 changes: 2 additions & 2 deletions linkup-cli/src/commands/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,10 @@ pub fn format_state_domains(session_name: &str, domains: &[StorableDomain]) -> V
.map(|d| d.domain.clone())
.collect::<Vec<String>>();

return filtered_domains
filtered_domains
.iter()
.map(|domain| format!("https://{}.{}", session_name, domain.clone()))
.collect();
.collect()
}

fn linkup_services(state: &LocalState) -> Vec<LocalService> {
Expand Down
17 changes: 14 additions & 3 deletions linkup-cli/src/commands/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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!");
Expand Down
32 changes: 31 additions & 1 deletion linkup-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<T> = std::result::Result<T, CliError>;

#[derive(Error, Debug)]
Expand Down
Loading

0 comments on commit da032b6

Please sign in to comment.