#!/bin/sh # Distro Roulette: verify an install. # # Reads what this machine says about itself and asks the server to sign it. # Read it before you run it. It is short on purpose. # # curl -fsSL https://distro.grivyntech.com/verify.sh | sh # wget -qO- https://distro.grivyntech.com/verify.sh | sh # # To put your GitHub login on the proof page, pass it as the first argument: # # curl -fsSL https://distro.grivyntech.com/verify.sh | sh -s -- your-login # # What it sends, and nothing else: # # ID and VERSION_ID from /etc/os-release # the kernel release uname -r # a package count from the package manager's own database # the install date when / was created, if the filesystem records it # your GitHub login only if you pass one # # No root. Nothing is written to disk. POSIX sh, so it runs on a minimal # install before you have put bash on it. # # Everything lives in main(), called on the last line. Piped into sh, a script # starts running while it is still downloading; if the connection dropped # halfway, sh would run half of it. Wrapped like this, nothing runs until the # whole file has arrived. # # DR_API points it at another server, which is how CI tests it. set -eu fail() { printf 'verify: %s\n' "$*" >&2 exit 1 } # Parsed, never sourced: sourcing it would run whatever the file contains. os_release() { sed -n "s/^$1=//p" "$OS_RELEASE" | head -n 1 | tr -d "\"'" } # From each package manager's own database, so it counts what is installed # rather than what is available. count_packages() { case "$ID" in debian | linuxmint | pop) dpkg-query -f '.\n' -W ;; fedora | rocky | opensuse-tumbleweed) rpm -qa ;; arch | manjaro | artix) pacman -Qq ;; void) xbps-query -l ;; alpine) apk info ;; gentoo) find /var/db/pkg -mindepth 2 -maxdepth 2 -type d ;; slackware) find /var/log/packages -mindepth 1 -maxdepth 1 ;; nixos) nix-store -q --requisites /run/current-system ;; *) # Not on the wheel. The server will say so; this only has to # produce a number on the way there. dpkg-query -f '.\n' -W || rpm -qa || pacman -Qq ;; esac 2>/dev/null | wc -l | tr -dc '0-9' } json_string() { if [ -z "$1" ]; then printf 'null' else printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')" fi } main() { # "sh verify.sh -- login" as well as "| sh -s -- login". if [ "${1:-}" = "--" ]; then shift fi API="${DR_API:-https://distro.grivyntech.com}" GITHUB="${1:-}" # --- what this machine says about itself -------------------------------- OS_RELEASE="" for candidate in /etc/os-release /usr/lib/os-release; do if [ -r "$candidate" ]; then OS_RELEASE="$candidate" break fi done [ -n "$OS_RELEASE" ] || fail "no /etc/os-release, so there is no way to tell which distro this is." ID="$(os_release ID)" VERSION_ID="$(os_release VERSION_ID)" KERNEL="$(uname -r)" PACKAGES="$(count_packages)" [ -n "$PACKAGES" ] || PACKAGES=0 # When the root directory was created: close to when the filesystem was # made, which is close to when the install happened. 0 where the # filesystem does not record it; the server simply leaves it off. INSTALLED="$(stat -c %W / 2>/dev/null | tr -dc '0-9')" [ -n "$INSTALLED" ] || INSTALLED=0 # --- the request --------------------------------------------------------- BODY="$(printf '{"id":%s,"versionId":%s,"kernel":%s,"packages":%s,"installedAt":%s,"github":%s}' \ "$(json_string "$ID")" \ "$(json_string "$VERSION_ID")" \ "$(json_string "$KERNEL")" \ "$PACKAGES" \ "$INSTALLED" \ "$(json_string "$GITHUB")")" printf 'Sending: %s %s, kernel %s, %s packages%s\n\n' \ "$ID" "${VERSION_ID:-(rolling)}" "$KERNEL" "$PACKAGES" "${GITHUB:+, as @$GITHUB}" # --- asking the server --------------------------------------------------- if command -v curl >/dev/null 2>&1; then RESPONSE="$(curl -sS -X POST \ -H 'Content-Type: application/json' \ -H 'Accept: text/plain' \ --data "$BODY" \ -w '\n%{http_code}' \ "$API/api/verify")" || fail "could not reach $API." CODE="$(printf '%s\n' "$RESPONSE" | tail -n 1)" TEXT="$(printf '%s\n' "$RESPONSE" | sed '$d')" elif command -v wget >/dev/null 2>&1 && wget --help 2>&1 | grep -q BusyBox; then # BusyBox wget, all a fresh Alpine has, cannot keep the body of an # error response: a refusal arrives without its reason. It does still # tell "the server said no" apart from "no server", on stderr. set +e TEXT="$(wget -qO- \ --header='Content-Type: application/json' \ --header='Accept: text/plain' \ --post-data="$BODY" \ "$API/api/verify" 2>&1)" STATUS=$? set -e if [ "$STATUS" -eq 0 ]; then CODE=200 elif printf '%s' "$TEXT" | grep -q 'server returned error'; then CODE=error TEXT="Not verified, and BusyBox wget does not show the server's reason. Install curl and run this again to see it." else fail "could not reach $API ($TEXT)." fi elif command -v wget >/dev/null 2>&1; then # GNU wget exits 8 when the server answers with an error, and # --content-on-error keeps the server's reason instead of dropping it. set +e TEXT="$(wget -qO- --content-on-error \ --header='Content-Type: application/json' \ --header='Accept: text/plain' \ --post-data="$BODY" \ "$API/api/verify" 2>/dev/null)" STATUS=$? set -e case "$STATUS" in 0) CODE=200 ;; 8) CODE=error ;; *) fail "could not reach $API (wget exited $STATUS)." ;; esac else fail "needs curl or wget to talk to the server. Installing one counts as part of the weekend." fi printf '%s\n' "$TEXT" [ "$CODE" = 200 ] } main "$@"