#!/bin/sh set -e SAYT_VERSION="${SAYT_VERSION:-v0.2.1}" if [ "${SAYT_VERSION#v}" = "$SAYT_VERSION" ] && [ "$SAYT_VERSION" != "latest" ]; then SAYT_VERSION="v$SAYT_VERSION" fi export SAYT_VERSION SAYT_INSECURE="${SAYT_INSECURE:-}" SAYT_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/sayt" # Detect OS and Arch OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$OS" in linux) case "$ARCH" in x86_64) BIN_NAME="sayt-linux-x64" ;; aarch64) BIN_NAME="sayt-linux-arm64" ;; armv7l) BIN_NAME="sayt-linux-armv7" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac ;; darwin) case "$ARCH" in x86_64) BIN_NAME="sayt-macos-x64" ;; arm64) BIN_NAME="sayt-macos-arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac ;; *) echo "Unsupported OS: $OS" >&2 exit 1 ;; esac SAYT_RELEASE_BASE_DOWNLOAD="" SAYT_RELEASE_BASE_CHILD="" SAYT_URL_HTTP="" if [ -n "${SAYT_RELEASE_BASE:-}" ]; then SAYT_RELEASE_BASE_DOWNLOAD="${SAYT_RELEASE_BASE%/}" SAYT_RELEASE_BASE_CHILD="$SAYT_RELEASE_BASE_DOWNLOAD" if [ -n "$SAYT_INSECURE" ]; then case "$SAYT_RELEASE_BASE_CHILD" in https://*) SAYT_RELEASE_BASE_CHILD="http://${SAYT_RELEASE_BASE_CHILD#https://}" ;; esac case "$SAYT_RELEASE_BASE_CHILD" in *:8443/*) base_prefix=${SAYT_RELEASE_BASE_CHILD%:8443/*} base_suffix=${SAYT_RELEASE_BASE_CHILD#*:8443/} SAYT_RELEASE_BASE_CHILD="${base_prefix}:8080/${base_suffix}" ;; esac fi SAYT_URL="${SAYT_RELEASE_BASE_DOWNLOAD}/${BIN_NAME}" if [ -n "$SAYT_INSECURE" ] && [ "$SAYT_RELEASE_BASE_CHILD" != "$SAYT_RELEASE_BASE_DOWNLOAD" ]; then SAYT_URL_HTTP="${SAYT_RELEASE_BASE_CHILD}/${BIN_NAME}" fi elif [ "$SAYT_VERSION" = "latest" ]; then SAYT_URL="https://github.com/bonisoft3/sayt/releases/latest/download/${BIN_NAME}" else SAYT_URL="https://github.com/bonisoft3/sayt/releases/download/${SAYT_VERSION}/${BIN_NAME}" fi case "$SAYT_URL" in http://*) [ -z "$SAYT_URL_HTTP" ] && SAYT_URL_HTTP="$SAYT_URL" ;; *) : ;; esac SAYT_BIN="$SAYT_CACHE_DIR/${SAYT_VERSION}/$BIN_NAME" # Symlink for easier usage SAYT_LINK="$SAYT_CACHE_DIR/sayt" mkdir -p "$SAYT_CACHE_DIR/${SAYT_VERSION}" # Download a file using available tools (no root required) download_file() { URL="$1" OUTPUT="$2" # Try wget (alpine has busybox wget with SSL) if command -v wget >/dev/null 2>&1; then WGET_FLAGS="-q" if [ -n "$SAYT_INSECURE" ]; then WGET_FLAGS="$WGET_FLAGS --no-check-certificate" fi wget $WGET_FLAGS -O "$OUTPUT" "$URL" && return 0 fi # Try curl if command -v curl >/dev/null 2>&1; then CURL_FLAGS="-fsSL" if [ -n "$SAYT_INSECURE" ]; then CURL_FLAGS="$CURL_FLAGS --insecure" fi curl $CURL_FLAGS -o "$OUTPUT" "$URL" && return 0 fi if [ -n "$SAYT_INSECURE" ]; then case "$URL" in https://*) return 1 ;; esac fi # Try python if command -v python3 >/dev/null 2>&1; then python3 -c "import urllib.request; urllib.request.urlretrieve('$URL', '$OUTPUT')" && return 0 fi if command -v python >/dev/null 2>&1; then python -c "import urllib.request; urllib.request.urlretrieve('$URL', '$OUTPUT')" 2>/dev/null && return 0 python -c "import urllib2; open('$OUTPUT','wb').write(urllib2.urlopen('$URL').read())" 2>/dev/null && return 0 fi # Try go if command -v go >/dev/null 2>&1; then go run -mod=readonly - "$URL" "$OUTPUT" <<'GOEOF' && return 0 package main import ("io";"net/http";"os") func main() { r,_ := http.Get(os.Args[1]); defer r.Body.Close(); f,_ := os.Create(os.Args[2]); defer f.Close(); io.Copy(f, r.Body) } GOEOF fi # Try node if command -v node >/dev/null 2>&1; then node -e " const https = require('https'), fs = require('fs'); const file = fs.createWriteStream(process.argv[2]); https.get(process.argv[1], r => { r.pipe(file); file.on('finish', () => process.exit(0)); }); " "$URL" "$OUTPUT" && return 0 fi # Try java if command -v java >/dev/null 2>&1 && command -v javac >/dev/null 2>&1; then TMP_DIR=$(mktemp -d) cat > "$TMP_DIR/Download.java" <<'JAVAEOF' import java.io.*; import java.net.*; import java.nio.channels.*; public class Download { public static void main(String[] a) throws Exception { ReadableByteChannel c = Channels.newChannel(new URL(a[0]).openStream()); new FileOutputStream(a[1]).getChannel().transferFrom(c, 0, Long.MAX_VALUE); }} JAVAEOF javac "$TMP_DIR/Download.java" && java -cp "$TMP_DIR" Download "$URL" "$OUTPUT" STATUS=$? rm -rf "$TMP_DIR" [ $STATUS -eq 0 ] && return 0 fi # Try php if command -v php >/dev/null 2>&1; then php -r "file_put_contents('$OUTPUT', file_get_contents('$URL'));" && return 0 fi return 1 } # Download via bash /dev/tcp (HTTP only, no TLS) download_file_bash_tcp() { URL="$1" OUTPUT="$2" # /dev/tcp is bash-specific, fork to bash for this download command -v bash >/dev/null 2>&1 || return 1 bash -c ' URL="$1" OUTPUT="$2" TMPFILE="${OUTPUT}.tmp" # Parse URL: http://host:port/path URL_NO_SCHEME="${URL#http://}" HOST_PORT="${URL_NO_SCHEME%%/*}" URL_PATH="/${URL_NO_SCHEME#*/}" # Extract host and port case "$HOST_PORT" in *:*) HOST="${HOST_PORT%:*}"; PORT="${HOST_PORT#*:}" ;; *) HOST="$HOST_PORT"; PORT=80 ;; esac # Open TCP connection exec 3<>/dev/tcp/"$HOST"/"$PORT" 2>/dev/null || exit 1 # Send HTTP request printf "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" "$URL_PATH" "$HOST" >&3 # Read entire response into temp file (avoids bash read buffering issues) cat <&3 > "$TMPFILE" exec 3>&- # Extract body: remove everything up to and including the first blank line (CRLF) # The blank line between headers and body is just a CR character after stripping LF sed -n "1,/^\r$/!p" "$TMPFILE" > "$OUTPUT" rm -f "$TMPFILE" # Verify we got something [ -s "$OUTPUT" ] || exit 1 ' _ "$URL" "$OUTPUT" } # Install wget/curl via apk without root (works on alpine/wolfi) install_downloader_apk() { APK_ROOT="$SAYT_CACHE_DIR/apk" mkdir -p "$APK_ROOT/etc/apk/keys" "$APK_ROOT/var/cache/apk" "$APK_ROOT/lib/apk/db" # Copy apk config cat /etc/apk/repositories > "$APK_ROOT/etc/apk/repositories" 2>/dev/null || return 1 cp /etc/apk/keys/* "$APK_ROOT/etc/apk/keys/" 2>/dev/null || true touch "$APK_ROOT/lib/apk/db/installed" # Try with --usermode (alpine), fall back without (wolfi) # Note: apk may return non-zero even on partial success (e.g., wolfi-baselayout errors) # so we check for the wget binary instead of relying on exit code apk add --root "$APK_ROOT" --usermode --initdb --no-scripts --no-cache wget 2>/dev/null \ || apk add --root "$APK_ROOT" --initdb --no-scripts --no-cache wget 2>/dev/null \ || true # Verify wget binary exists [ -x "$APK_ROOT/usr/bin/wget" ] || return 1 # Create wrapper script - use the apk-installed ld-linux with --library-path for # glibc systems (wolfi), and LD_LIBRARY_PATH for musl systems (alpine). # Must use the apk-installed ld-linux (not the host's) to avoid glibc version mismatch. mkdir -p "$SAYT_CACHE_DIR/bin" APK_LD="" for ld in "$APK_ROOT/usr/lib/ld-linux-aarch64.so.1" "$APK_ROOT/usr/lib/ld-linux-x86-64.so.2"; do [ -x "$ld" ] && APK_LD="$ld" && break done if [ -n "$APK_LD" ]; then # glibc (wolfi) cat > "$SAYT_CACHE_DIR/bin/wget" < "$SAYT_CACHE_DIR/bin/wget" </dev/null 2>&1 || return 1 return 0 } # Install curl/wget using package manager (requires root) # Cleans up package manager caches to keep Docker layers small install_downloader_root() { if command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* && return 0 elif command -v apk >/dev/null 2>&1; then apk add --no-cache curl && return 0 elif command -v yum >/dev/null 2>&1; then yum install -y -q curl && yum clean all && rm -rf /var/cache/yum && return 0 elif command -v dnf >/dev/null 2>&1; then dnf install -y -q curl && dnf clean all && rm -rf /var/cache/dnf && return 0 fi return 1 } # Main download logic ensure_downloader() { # Already have a downloader? if command -v wget >/dev/null 2>&1 || command -v curl >/dev/null 2>&1; then return 0 fi # Try non-root apk install (alpine/wolfi) - skip if already root if [ "$(id -u)" != "0" ] && command -v apk >/dev/null 2>&1; then echo "Installing wget via apk (non-root)..." >&2 if install_downloader_apk; then return 0 fi fi # Try as root if [ "$(id -u)" = "0" ]; then echo "Installing curl as root..." >&2 if install_downloader_root; then return 0 fi fi # Try with sudo if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then echo "Installing curl with sudo..." >&2 if sudo sh -c ' if command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/* elif command -v apk >/dev/null 2>&1; then apk add --no-cache curl elif command -v yum >/dev/null 2>&1; then yum install -y -q curl && yum clean all && rm -rf /var/cache/yum elif command -v dnf >/dev/null 2>&1; then dnf install -y -q curl && dnf clean all && rm -rf /var/cache/dnf else exit 1 fi '; then return 0 fi fi return 1 } if [ ! -x "$SAYT_BIN" ]; then echo "Downloading sayt ${SAYT_VERSION} ($BIN_NAME)..." >&2 DOWNLOADED=0 # Try HTTPS download with standard tools if download_file "$SAYT_URL" "$SAYT_BIN"; then DOWNLOADED=1 # Try HTTP download with standard tools when available elif [ -n "$SAYT_URL_HTTP" ] && [ "$SAYT_URL_HTTP" != "$SAYT_URL" ] && download_file "$SAYT_URL_HTTP" "$SAYT_BIN"; then DOWNLOADED=1 # Try after installing a downloader (HTTPS first, then HTTP fallback) elif ensure_downloader && { download_file "$SAYT_URL" "$SAYT_BIN" || { [ -n "$SAYT_URL_HTTP" ] && download_file "$SAYT_URL_HTTP" "$SAYT_BIN"; }; }; then DOWNLOADED=1 # Try HTTP download via bash /dev/tcp (works in Ubuntu without curl/wget) elif [ -n "$SAYT_URL_HTTP" ] && download_file_bash_tcp "$SAYT_URL_HTTP" "$SAYT_BIN"; then DOWNLOADED=1 fi if [ "$DOWNLOADED" = "1" ]; then chmod +x "$SAYT_BIN" ln -sf "$SAYT_BIN" "$SAYT_LINK" else echo "Error: Could not download sayt from $SAYT_URL" >&2 echo "" >&2 echo "No download tool available. Please either:" >&2 echo " 1. Run as root: sudo $0 $*" >&2 echo " 2. Install curl/wget manually" >&2 echo " 3. Download manually:" >&2 echo " curl -fsSL -o '$SAYT_BIN' '$SAYT_URL'" >&2 echo " chmod +x '$SAYT_BIN'" >&2 exit 1 fi fi if [ -n "$SAYT_RELEASE_BASE_CHILD" ]; then export SAYT_RELEASE_BASE="$SAYT_RELEASE_BASE_CHILD" fi exec "$SAYT_BIN" "$@"