#!/bin/sh # NetLink — install netlinkctl. # # curl -fsSL https://get.netlinks.dev | sh # # One version: # # curl -fsSL https://get.netlinks.dev | NETLINK_VERSION=v1.2.0 sh # # Somewhere else: # # curl -fsSL https://get.netlinks.dev | NETLINK_BIN=~/bin sh # # ─── Why everything is inside a function ───────────────────────────────── # # `curl | sh` hands the shell a stream. If the connection drops halfway — and # these run on Yemeni connections — the shell executes the half it received and # then stops. A script written as a list of commands would download a partial # binary and install it, or delete the old one and never replace it. # # Wrapping the whole thing in main() and calling it on the very last line makes # a truncated download do nothing at all: without that final line, main is # defined and never runs. This is the only reason for the shape of this file. # # ─── sh, not bash ──────────────────────────────────────────────────────── # # POSIX sh, because macOS ships bash 3.2 from 2007 and some minimal Linux images # have no bash at all. Nothing here needs more than sh gives. set -eu BASE="${NETLINK_BASE:-https://get.netlinks.dev}" main() { say "NetLink — installing netlinkctl" need curl check_sha_tool platform=$(detect_platform) version="${NETLINK_VERSION:-}" if [ -z "$version" ]; then version=$(curl -fsSL "$BASE/VERSION" 2>/dev/null | tr -d '\r\n ') || true if [ -z "$version" ]; then die "cannot reach $BASE to ask which version is current. Check the connection, or name one yourself: curl -fsSL $BASE | NETLINK_VERSION=v1.0.0 sh" fi fi file="netlinkctl-$platform" case "$platform" in windows-*) file="$file.exe" ;; esac # One temporary directory, removed on any exit including a failure. Without # the trap, a failed verification leaves a rejected binary in /tmp, and the # next person to look finds a file that was deliberately not installed. tmp=$(mktemp -d 2>/dev/null || mktemp -d -t netlinkctl) trap 'rm -rf "$tmp"' EXIT INT TERM say " version $version" say " platform $platform" # Downloaded to the temporary directory, never over the installed tool. A # failure part-way through must leave the working copy alone — somebody # running this to upgrade still has a job to do afterwards. if ! curl -fsSL "$BASE/$version/$file" -o "$tmp/$file"; then die "no build for $platform at version $version. Available builds are listed at $BASE/$version/SHA256SUMS" fi verify "$tmp" "$file" "$version" dest=$(choose_dest) install_binary "$tmp/$file" "$dest" say "" say "netlinkctl $version installed in $dest" check_path "$dest" say "" say "Next:" say " export NETLINK_API=https://api.netlinks.dev" say " netlinkctl login -phone +967... -password ..." } # ─── The checksum ──────────────────────────────────────────────────────── # verify refuses anything whose hash is not the published one. # # This is not only about an attacker. A truncated download over a bad connection # is the common case, and a half-written binary either fails to run or — worse on # some systems — runs and behaves strangely. The published list turns both into # one clear refusal. verify() { dir="$1" file="$2" version="$3" if ! curl -fsSL "$BASE/$version/SHA256SUMS" -o "$dir/SHA256SUMS"; then die "cannot fetch the checksum list for $version. Refusing to install a binary that cannot be checked." fi # Parsed with awk rather than grep, because sha256sum has two output formats # and which one you get depends on the machine that generated the list: # # name text mode, two spaces — GNU coreutils on Linux # *name binary mode, one space and a star — Windows, and # shasum on macOS when asked for binary # # The first version of this matched two spaces and would have failed for # everyone the day a release was cut from a Mac. Comparing the name field # with any leading star removed works for both and cannot drift. expected=$(awk -v want="$file" ' { name = $2; sub(/^\*/, "", name); if (name == want) { print $1; exit } } ' "$dir/SHA256SUMS") if [ -z "$expected" ]; then die "$file is not in the checksum list for $version." fi actual=$(sha_of "$dir/$file") if [ "$expected" != "$actual" ]; then die "the download does not match its published checksum. expected $expected got $actual Nothing was installed. This is usually an interrupted download, so try again. If it happens twice, stop and tell whoever runs the server." fi say " checksum ok" } # sha_of prints the SHA-256 of one file. # # A function rather than a variable holding a command name, because macOS needs # `shasum -a 256` and Linux has `sha256sum`. Held in a variable, the multi-word # one only works if it is expanded unquoted — which works, and is also exactly # the pattern that breaks the day a path has a space in it. A function keeps the # words where they belong. sha_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 else shasum -a 256 "$1" | cut -d' ' -f1 fi } # check_sha_tool fails early if neither exists. # # Checked before anything is downloaded. Finding out after the download that the # result cannot be verified leaves only two options, and one of them is # installing something unchecked. check_sha_tool() { if command -v sha256sum >/dev/null 2>&1; then return; fi if command -v shasum >/dev/null 2>&1; then return; fi die "neither sha256sum nor shasum is installed, so a download cannot be verified. Refusing to install anything unchecked." } # ─── Where it goes ─────────────────────────────────────────────────────── detect_platform() { os=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(uname -m) case "$os" in linux | darwin) ;; mingw* | msys* | cygwin*) die "this looks like Git Bash on Windows. Use PowerShell instead: irm https://get.netlinks.dev/install.ps1 | iex" ;; *) die "unsupported system: $os" ;; esac # uname reports the same architecture under several names. x86_64 and amd64 # are one thing; aarch64 and arm64 are one thing. case "$arch" in x86_64 | amd64) arch="amd64" ;; aarch64 | arm64) arch="arm64" ;; armv7l | armv6l) die "32-bit ARM is not built. If you need it, say so — it is one line." ;; *) die "unsupported architecture: $arch" ;; esac echo "$os-$arch" } # choose_dest picks somewhere to put it, preferring a directory already on PATH. # # ~/.local/bin rather than sudo by default: installing a tool should not need # root, and a team where every install is done with sudo is a team that types # sudo without reading. /usr/local/bin is used only when it is already writable. choose_dest() { if [ -n "${NETLINK_BIN:-}" ]; then echo "$NETLINK_BIN" return fi if [ -w /usr/local/bin ]; then echo "/usr/local/bin" return fi echo "$HOME/.local/bin" } install_binary() { src="$1" dir="$2" mkdir -p "$dir" || die "cannot create $dir" # 0755 explicitly. A downloaded file has no execute bit, and the failure # without this is "netlinkctl: command not found" from a file that is # plainly there, which sends people looking at PATH for an hour. # # Two steps rather than `install`: busybox and some minimal images do not # have it, and this is the one place a missing tool would strand somebody. if ! cp "$src" "$dir/netlinkctl.new" 2>/dev/null; then die "cannot write to $dir. Either pick somewhere else: curl -fsSL $BASE | NETLINK_BIN=\$HOME/.local/bin sh or make it writable." fi chmod 0755 "$dir/netlinkctl.new" # Renamed into place at the end, because a rename is atomic. Copying over # the existing file directly means a failure halfway leaves a truncated # netlinkctl where a working one used to be — and on a laptop mid-enrolment # that is somebody standing in a shop with no tool. mv "$dir/netlinkctl.new" "$dir/netlinkctl" } # check_path says so if the tool was installed somewhere the shell will not # look. Without this the script ends with "installed" and the next command fails. check_path() { dir="$1" case ":$PATH:" in *":$dir:"*) return ;; esac say "" say "$dir is not on your PATH, so the shell will not find it yet." say "Add this to your ~/.profile, ~/.bashrc or ~/.zshrc:" say "" say " export PATH=\"$dir:\$PATH\"" say "" say "Until then, run it as $dir/netlinkctl" } # ─── Small helpers ─────────────────────────────────────────────────────── need() { command -v "$1" >/dev/null 2>&1 || die "$1 is needed and not installed." } # Everything to stderr, so a person reading the output and a script capturing it # see the same thing and neither gets the other's. say() { printf '%s\n' "$*" >&2; } die() { printf '\nnetlinkctl install failed: %s\n\n' "$*" >&2 exit 1 } # The last line, and the only line that does anything. See the note at the top: # a download cut short never reaches this, so it installs nothing rather than # half of something. main "$@"