#!/bin/sh # xin installer — https://xinproxy.com # # curl -fsSL https://install.xinproxy.com/ | sh # # Enrolls the signed xin repository with your system package manager (apt or # dnf/yum) and installs from it, so `apt-get upgrade` keeps working afterwards. # Falls back to a checksum-verified tarball where no package feed exists. # # Flags: # --version default: 0.2.1 (tarball path only) # --method default: auto # --flavor default: plain; FIPS is Linux amd64/arm64 only # --prefix tarball install prefix, default /usr/local # --start enable and start the service after install # --dry-run print what would run, change nothing # --help # # Environment variables, for unattended (non-tty) installs -- an interactive # run is asked instead, never assumed: # XIN_ADOPT_NGINX=1 copy an existing /etc/nginx configuration tree to # /etc/xinginx as xinginx's starting configuration, with no # prompt. # XIN_REPLACE_NGINX=1 implies XIN_ADOPT_NGINX=1, and additionally # disables nginx.service and enables/starts # xinginx.service, with no prompt. # # Copyright (c) 2026 Signeen Inc. All rights reserved. # Licensed under the Xin License: https://install.xinproxy.com/LICENSE set -eu XIN_VERSION="0.2.1" PRODUCT="xinginx" METHOD="auto" FLAVOR="plain" PREFIX="/usr/local" START=0 DRY=0 APT_HOST="http://apt.xinproxy.com" RPM_HOST="http://rpm.xinproxy.com" DL_HOST="https://dl.xinproxy.com" EMPTY_SHA="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" # Pin the archive keys because the package feeds use HTTP. APT_KEY_SHA="74b7c675aa078a760b0c52784b028780a025fb0a09d1da6e3694babb1c70883e" RPM_KEY_SHA="5a6d3285bc03b038d650cf50d1056ac777e3fd44ab12f616f19348b1d21fc2af" # Tarball installs write this starter when no /etc/xinginx/xinginx.conf exists. STARTER_XIN_CONF='worker_processes auto; error_log /var/log/xin/error.log; events { worker_connections 1024; } http { include /etc/xinginx/mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; server_tokens off; gzip on; access_log /var/log/xin/access.log; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /var/www/html; index index.html index.htm; location = /health { return 200 "ok\n"; } location / { try_files $uri $uri/ =404; } } }' RED=''; GRN=''; YEL=''; DIM=''; BLD=''; RST='' if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then RED=$(printf '\033[31m'); GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m') DIM=$(printf '\033[2m'); BLD=$(printf '\033[1m'); RST=$(printf '\033[0m') fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$GRN" "$RST" "$*"; } warn() { printf '%swarning:%s %s\n' "$YEL" "$RST" "$*" >&2; } die() { printf '%serror:%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; } usage() { awk 'NR == 1 { next } /^set -eu$/ { exit } { sub(/^# ?/, ""); print }' "$0"; exit 0; } while [ $# -gt 0 ]; do case "$1" in --product) PRODUCT="${2:-}"; shift 2 ;; --version) XIN_VERSION="${2:-}"; shift 2 ;; --method) METHOD="${2:-}"; shift 2 ;; --flavor) FLAVOR="${2:-}"; shift 2 ;; --prefix) PREFIX="${2:-}"; shift 2 ;; --start) START=1; shift ;; --dry-run) DRY=1; shift ;; -h|--help) usage ;; *) die "unknown option: $1 (try --help)" ;; esac done case "$METHOD" in auto|apt|rpm|tarball) ;; *) die "unknown install method: $METHOD" ;; esac case "$FLAVOR" in plain|fips) ;; *) die "unknown build flavor: $FLAVOR" ;; esac case "$PRODUCT" in xinginx) ;; *) die "unknown product: $PRODUCT (this installer supports xinginx only)" ;; esac say "Copyright (c) 2026 Signeen Inc. All rights reserved." say "Xin is licensed under the Xin License for non-commercial use or evaluation." say "Commercial use requires a separate written license." say "Full terms: https://install.xinproxy.com/LICENSE" say "By continuing with installation, you accept the Xin License." say "If you do not agree, cancel the installation now." say "" # ---------------------------------------------------------------- privileges SUDO="" if [ "$(id -u)" -ne 0 ]; then if command -v sudo >/dev/null 2>&1; then SUDO="sudo" else die "this needs root and sudo was not found; re-run as root" fi fi run() { if [ "$DRY" -eq 1 ]; then printf '%s %s%s\n' "$DIM" "$*" "$RST" else # shellcheck disable=SC2086 $SUDO "$@" fi } run_sh() { if [ "$DRY" -eq 1 ]; then printf '%s %s%s\n' "$DIM" "$1" "$RST" else if [ -n "$SUDO" ]; then $SUDO sh -c "$1"; else sh -c "$1"; fi fi } have() { command -v "$1" >/dev/null 2>&1; } # Read prompts from the terminal; stdin may contain this script under curl | sh. ask() { [ "$DRY" -eq 0 ] || return 1 [ -t 0 ] || [ -r /dev/tty ] || return 1 printf '%s' "$1" > /dev/tty 2>/dev/null || return 1 # shellcheck disable=SC2039 IFS= read -r ans < /dev/tty 2>/dev/null || return 1 case "$ans" in [Yy]|[Yy][Ee][Ss]) return 0 ;; *) return 1 ;; esac } fetch() { # Do not follow redirects to a different scheme. case "$1" in https://*) proto=https ;; http://*) proto=http ;; *) die "fetch: unsupported URL scheme: $1" ;; esac if have curl; then curl -fsSL --proto "=$proto" --tlsv1.2 -o "$2" "$1" elif have wget; then wget -qO "$2" "$1" else die "neither curl nor wget is available" fi } verify_archive_key() { if have sha256sum; then got=$(sha256sum "$1" | awk '{print $1}') elif have shasum; then got=$(shasum -a 256 "$1" | awk '{print $1}') else die "no sha256sum or shasum available to verify the archive key" fi [ "$got" = "$2" ] || die "archive signing key checksum mismatch (expected $2, got $got)" } # ---------------------------------------------------------------- detection UNAME_S=$(uname -s 2>/dev/null || echo unknown) UNAME_M=$(uname -m 2>/dev/null || echo unknown) # Darwin and FreeBSD use tarballs; Windows uses install.ps1. case "$UNAME_S" in Linux) ;; Darwin|FreeBSD) case "$METHOD" in auto|tarball) METHOD=tarball ;; *) die "$METHOD packages are unavailable for $UNAME_S" ;; esac ;; *) die "unsupported OS: $UNAME_S (Linux, Darwin, and FreeBSD are supported; Windows needs its own installer, not this script)" ;; esac case "$UNAME_M" in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; i386|i486|i586|i686) ARCH="i686" ;; armv7l|armv7) ARCH="armv7" ;; armv6l|armv6) ARCH="armv6" ;; ppc64le) ARCH="ppc64le" ;; s390x) ARCH="s390x" ;; riscv64) ARCH="riscv64" ;; *) die "unsupported architecture: $UNAME_M" ;; esac case "$UNAME_S:$ARCH" in Darwin:amd64|Darwin:arm64|FreeBSD:amd64|FreeBSD:arm64|FreeBSD:i686|Linux:*) ;; *) die "no $UNAME_S tarball is published for $ARCH" ;; esac if [ "$FLAVOR" = fips ]; then case "$UNAME_S:$ARCH" in Linux:amd64|Linux:arm64) ;; *) die "FIPS packages are published only for Linux amd64 and arm64" ;; esac fi case "$UNAME_S" in Linux) OS_NAME="linux-$ARCH" ;; Darwin) OS_NAME="darwin-$ARCH" ;; FreeBSD) OS_NAME="freebsd-$ARCH" ;; esac OS_ID=""; OS_LIKE=""; OS_VERSION="" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-}"; OS_LIKE="${ID_LIKE:-}"; OS_VERSION="${VERSION_ID:-}" fi APT_SUITE="stable" case "$OS_ID:$OS_VERSION" in ubuntu:24.04) APT_SUITE="noble" ;; esac if [ "$ARCH" = "armv6" ]; then APT_SUITE="armv6" fi pick_method() { case "$METHOD" in apt|rpm|tarball) echo "$METHOD"; return ;; esac case " $OS_ID $OS_LIKE " in *" debian "*|*" ubuntu "*) if have apt-get; then echo apt; return; fi ;; *" fedora "*|*" rhel "*|*" centos "*) case "$ARCH" in armv6|armv7) echo tarball; return ;; *) if have dnf || have yum; then echo rpm; return; fi ;; esac ;; esac if have apt-get; then echo apt; return; fi # No RPM packages are published for 32-bit ARM. case "$ARCH" in armv6|armv7) ;; *) if have dnf || have yum; then echo rpm; return; fi ;; esac echo tarball } METHOD=$(pick_method) PKG=$PRODUCT [ "$FLAVOR" = fips ] && PKG=$PRODUCT-fips OTHER_PKG=$PRODUCT-fips [ "$FLAVOR" = fips ] && OTHER_PKG=$PRODUCT XIN_CONF_PREEXISTED=0 if [ -e /etc/xinginx/xinginx.conf ] || [ -L /etc/xinginx/xinginx.conf ]; then XIN_CONF_PREEXISTED=1 fi case "$METHOD:$UNAME_S:$ARCH" in apt:Linux:*|rpm:Linux:*) ;; tarball:*) ;; *) die "$METHOD packages are unavailable for $UNAME_S/$ARCH" ;; esac if [ "$METHOD" = rpm ]; then case "$ARCH" in armv6|armv7) die "no RPM is published for $ARCH; use --method tarball" ;; esac fi say "" say "${BLD}xin installer${RST} ${DIM}$PKG · $ARCH · ${OS_ID:-unknown} · via $METHOD${RST}" say "" # ---------------------------------------------------------------- apt install_apt() { step "Enrolling $APT_HOST (suite: $APT_SUITE)" run_sh "install -d -m 0755 /usr/share/keyrings" if [ "$DRY" -eq 1 ]; then printf '%s fetch and verify pinned archive key from %s/keys/xin-archive-keyring.gpg > /usr/share/keyrings/xin.gpg%s\n' \ "$DIM" "$APT_HOST" "$RST" else tmpkey=$(mktemp) fetch "$APT_HOST/keys/xin-archive-keyring.gpg" "$tmpkey" \ || die "could not download the signing key from $APT_HOST" [ -s "$tmpkey" ] || die "the downloaded signing key is empty" verify_archive_key "$tmpkey" "$APT_KEY_SHA" $SUDO install -m 0644 "$tmpkey" /usr/share/keyrings/xin.gpg rm -f "$tmpkey" fi run_sh "printf 'deb [signed-by=/usr/share/keyrings/xin.gpg] %s %s main\n' '$APT_HOST' '$APT_SUITE' > /etc/apt/sources.list.d/xin.list" step "apt-get update" run env DEBIAN_FRONTEND=noninteractive apt-get update step "Installing $PKG" run env DEBIAN_FRONTEND=noninteractive apt-get install -y "$PKG" } # ---------------------------------------------------------------- rpm install_rpm() { step "Enrolling $RPM_HOST/el8" run_sh "install -d -m 0755 /etc/pki/rpm-gpg" if [ "$DRY" -eq 1 ]; then printf '%s fetch and verify pinned archive key from %s/keys/xin-archive-keyring.asc > /etc/pki/rpm-gpg/RPM-GPG-KEY-xin%s\n' \ "$DIM" "$RPM_HOST" "$RST" else tmpkey=$(mktemp) fetch "$RPM_HOST/keys/xin-archive-keyring.asc" "$tmpkey" \ || die "could not download the signing key from $RPM_HOST" [ -s "$tmpkey" ] || die "the downloaded signing key is empty" verify_archive_key "$tmpkey" "$RPM_KEY_SHA" $SUDO install -m 0644 "$tmpkey" /etc/pki/rpm-gpg/RPM-GPG-KEY-xin rm -f "$tmpkey" fi run_sh "cat > /etc/yum.repos.d/xin.repo <<'XINREPO' [xin] name=xin baseurl=$RPM_HOST/el8 enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-xin XINREPO" step "Installing $PKG" if have dnf; then if have rpm && rpm -q "$OTHER_PKG" >/dev/null 2>&1; then run dnf swap -y "$OTHER_PKG" "$PKG" else run dnf install -y "$PKG" fi else if have rpm && rpm -q "$OTHER_PKG" >/dev/null 2>&1; then run yum swap -y "$OTHER_PKG" "$PKG" else run yum install -y "$PKG" fi fi } # ---------------------------------------------------------------- tarball install_tarball() { # Prefer the static Linux archive when one is available. base="$PRODUCT-$XIN_VERSION-$OS_NAME" cands="$base" [ "$UNAME_S" = "Linux" ] && cands="$base-musl $base" if [ "$FLAVOR" = fips ]; then cands="$base-fips"; fi for cand in $cands; do url="$DL_HOST/$PRODUCT/$XIN_VERSION/$cand.tar.gz" if { have curl && curl -fsSLI --proto '=https' "$url" >/dev/null 2>&1; } || { have wget && wget -q --spider "$url"; }; then TARNAME="$cand.tar.gz"; TARURL="$url"; break fi done [ -n "${TARURL:-}" ] || die "no tarball published for $PRODUCT $XIN_VERSION on $OS_NAME" step "Downloading $TARNAME" tmpd=$(mktemp -d) trap 'rm -rf "$tmpd"' EXIT if [ "$DRY" -eq 1 ]; then printf '%s fetch %s%s\n' "$DIM" "$TARURL" "$RST" return fi fetch "$TARURL" "$tmpd/$TARNAME" || die "download failed: $TARURL" step "Verifying checksum" fetch "$DL_HOST/$PRODUCT/$XIN_VERSION/SHA256SUMS" "$tmpd/SHA256SUMS" || die "could not download SHA256SUMS" want=$(grep -E "[[:space:]]\*?$TARNAME\$" "$tmpd/SHA256SUMS" | awk '{print $1}' | head -n1) [ -n "$want" ] || die "$TARNAME is not listed in SHA256SUMS" if have sha256sum; then got=$(sha256sum "$tmpd/$TARNAME" | awk '{print $1}') elif have shasum; then got=$(shasum -a 256 "$tmpd/$TARNAME" | awk '{print $1}') else die "no sha256sum or shasum available to verify the download" fi [ "$want" = "$got" ] || die "checksum mismatch for $TARNAME (expected $want, got $got)" # Reject empty archives even when the manifest lists their checksum. if [ "$got" = "$EMPTY_SHA" ] || [ ! -s "$tmpd/$TARNAME" ]; then die "$TARNAME is published but empty — use apt/dnf or another architecture" fi step "Installing into $PREFIX" tar -xzf "$tmpd/$TARNAME" -C "$tmpd" bundle="$tmpd/$cand" [ -f "$bundle/install.sh" ] && [ -f "$bundle/sbin/xinginx" ] || die "$TARNAME does not contain the expected installer and binary" TARBALL_CONFIG="$bundle/share/doc/$PRODUCT/xin.conf" [ -f "$TARBALL_CONFIG" ] || die "$TARNAME is missing xin.conf" configure_xin_conf run env PREFIX="$PREFIX" sh "$bundle/install.sh" warn "A tarball install has no package upgrades." warn "Where a package feed exists, prefer --method apt or --method rpm." } # ---------------------------------------------------------- xinginx.conf itself # Keep an existing /etc/xinginx/xinginx.conf from before this install. configure_xin_conf() { [ "$PRODUCT" = "xinginx" ] || return 0 if [ "$DRY" -eq 1 ]; then printf '%s configure /etc/xinginx/xinginx.conf (adopt existing nginx config, or write a starter)%s\n' "$DIM" "$RST" return 0 fi [ "$XIN_CONF_PREEXISTED" -eq 1 ] && return 0 if [ -r /etc/nginx/nginx.conf ]; then adopt=1 if [ "${XIN_ADOPT_NGINX:-0}" = 1 ] || [ "${XIN_REPLACE_NGINX:-0}" = 1 ]; then : # explicit unattended consent, no prompt elif ask "Found /etc/nginx/nginx.conf. Copy nginx's configuration tree into /etc/xinginx? [y/N] "; then : # interactive consent else adopt=0 if [ -t 0 ] || [ -r /dev/tty ]; then say " Not copying it — leaving xinginx's starter config in place." else warn "not adopting nginx's configuration (no tty to ask) — set XIN_ADOPT_NGINX=1 to copy it unattended" fi fi if [ "$adopt" -eq 1 ]; then run_sh "$(cat <<'SH' set -eu mkdir -p /etc/xinginx staging=$(mktemp -d /etc/xinginx/.nginx-import.XXXXXX) trap 'rm -rf "$staging"' EXIT cp -RPp /etc/nginx/. "$staging/" chmod 700 "$staging" find "$staging" -type l | while IFS= read -r link; do target=$(readlink "$link") case "$target" in /etc/nginx/*) ln -sfn "/etc/xinginx/${target#/etc/nginx/}" "$link" ;; esac done find "$staging" -type f | while IFS= read -r file; do if grep -Iq -e '/etc/nginx/' -e '/var/log/nginx/' "$file"; then sed -e 's#/etc/nginx/#/etc/xinginx/#g' -e 's#/var/log/nginx/#/var/log/xin/#g' "$file" > "$file.xin-tmp" cat "$file.xin-tmp" > "$file" rm -f "$file.xin-tmp" fi done # Copy the entries, not "$staging/." itself: -p on "." would stamp the # staging directory's 0700 onto /etc/xinginx, and a reload re-reads the # configuration as the unprivileged user. find "$staging" -mindepth 1 -maxdepth 1 -exec cp -RPnp {} /etc/xinginx/ \; cp -p "$staging/nginx.conf" /etc/xinginx/xinginx.conf SH )" step "Copied nginx's configuration tree into /etc/xinginx" say " Paths under /etc/nginx now point to the copied files under /etc/xinginx," say " and logs under /var/log/nginx now go to /var/log/xin." say " Other absolute paths, such as certificates elsewhere, stay as written." return 0 fi fi { [ -e /etc/xinginx/xinginx.conf ] || [ -L /etc/xinginx/xinginx.conf ]; } && return 0 run_sh "mkdir -p /etc/xinginx" if [ -n "${TARBALL_CONFIG:-}" ]; then run install -m 0644 "$TARBALL_CONFIG" /etc/xinginx/xinginx.conf step "Installed the tarball's starter config at /etc/xinginx/xinginx.conf" return 0 fi run_sh "cat > /etc/xinginx/xinginx.conf <<'XINCONF' $STARTER_XIN_CONF XINCONF" step "Wrote a minimal starter config to /etc/xinginx/xinginx.conf" } # ------------------------------------------------- nginx.conf compat symlink # Keep /etc/xinginx/nginx.conf available for nginx-compatible tools. configure_nginx_conf_symlink() { [ "$PRODUCT" = "xinginx" ] || return 0 if [ "$DRY" -eq 1 ]; then printf '%s ensure /etc/xinginx/nginx.conf -> xinginx.conf (compat symlink)%s\n' "$DIM" "$RST" return 0 fi # Preserve an existing regular file. if [ -e /etc/xinginx/nginx.conf ] && [ ! -L /etc/xinginx/nginx.conf ]; then return 0 fi run_sh "ln -sf xinginx.conf /etc/xinginx/nginx.conf" step "Linked /etc/xinginx/nginx.conf -> xinginx.conf (nginx-tooling compat)" } # Switch services only with explicit consent. maybe_switch_from_nginx() { [ "$PRODUCT" = "xinginx" ] || return 0 [ "$DRY" -eq 1 ] && return 0 [ "$METHOD" = "tarball" ] && return 0 have systemctl || return 0 systemctl list-unit-files xinginx.service >/dev/null 2>&1 || return 0 nginx_state=$( { systemctl is-enabled nginx.service 2>/dev/null; systemctl is-active nginx.service 2>/dev/null; } | tr '\n' ' ') case "$nginx_state" in *enabled*|*active*) ;; *) return 0 ;; esac switch=0 if [ "${XIN_REPLACE_NGINX:-0}" = 1 ]; then switch=1 elif ask "Disable the nginx service and enable xinginx in its place? [y/N] "; then switch=1 fi if [ "$switch" -ne 1 ]; then say "" say " ${DIM}# when you are ready — stop nginx first, they share port 80${RST}" say " sudo systemctl disable --now nginx && sudo systemctl enable --now xinginx" if ! { [ -t 0 ] || [ -r /dev/tty ]; }; then warn "not switching services (no tty to ask) — set XIN_REPLACE_NGINX=1 to do so unattended" fi return 0 fi # Order matters: nginx has to give up the port before xinginx can bind it. step "Disabling nginx, enabling xinginx" run systemctl disable --now nginx.service run systemctl enable --now xinginx.service sleep 1 if $SUDO systemctl is-active --quiet xinginx.service 2>/dev/null; then step "xinginx is active" else warn "xinginx did not come up after the switch. It is not serving traffic." warn "Check it: systemctl status xinginx ; journalctl -u xinginx -n 40" warn "Roll back: sudo systemctl enable --now nginx" fi } case "$METHOD" in apt) install_apt ;; rpm) install_rpm ;; tarball) install_tarball ;; *) die "unknown method: $METHOD" ;; esac [ "$DRY" -eq 1 ] && { say ""; say "${DIM}dry run — nothing was changed${RST}"; exit 0; } # ---------------------------------------------------------------- verify say "" step "Verifying the install" BIN="xinginx" if have "$BIN"; then "$BIN" -v 2>/dev/null || "$BIN" --version 2>/dev/null || true else case "$METHOD" in tarball) warn "$BIN is not on PATH yet — open a new shell, or check $PREFIX/sbin" ;; *) warn "$BIN is not on PATH yet — open a new shell, or check /usr/sbin" ;; esac fi # ---------------------------------------------------------- xinginx.conf itself configure_xin_conf configure_nginx_conf_symlink # Check the installed configuration without starting Xin. if [ "$PRODUCT" = "xinginx" ] && [ -r /etc/xinginx/xinginx.conf ] && have xinginx; then say "" step "Checking /etc/xinginx/xinginx.conf (read-only)" say "" xinginx -t || true fi # ---------------------------------------------------------------- start if [ "$START" -eq 1 ]; then if have systemctl; then svc="xinginx" step "Enabling and starting $svc" run systemctl enable --now "$svc" run systemctl --no-pager --full status "$svc" || true else warn "--start needs systemd; start $BIN yourself" fi elif [ "$PRODUCT" = "xinginx" ]; then say "" say "${BLD}Installed.${RST} xinginx is not running yet — it never starts itself." maybe_switch_from_nginx elif have systemctl; then say "" say "${BLD}Installed.${RST} $PRODUCT is not running yet — it never starts itself." say "" say " sudo systemctl enable --now $PRODUCT" fi say "" say "Running in a container instead? ${DIM}docker pull ghcr.io/xinproxy/xin${RST}" say "Commercial licensing: licensing@xinproxy.com" case "$METHOD" in apt|rpm) say "Full license: /usr/share/doc/$PKG/LICENSE" ;; tarball) say "Full license: $PREFIX/share/doc/xin/LICENSE" ;; esac say "More information: ${BLD}https://xinproxy.com${RST}" say ""