#!/usr/bin/env sh set -eu umask 077 REPO="kazeyukiro/3m-ui" BASE="/usr/local/lib/3m-ui" APP_BIN="$BASE/3m-ui-bin" VERSION_FILE="$BASE/VERSION" MODE_FILE="$BASE/BUILD_MODE" ENTRY="/usr/local/bin/3m-ui" CONFIG_DIR="/etc/3m-ui" DATA_DIR="/var/lib/3m-ui" LOG_DIR="/var/log/3m-ui" MIHOMO_BIN="/usr/local/bin/mihomo" SERVICE_NAME="3m-ui" YES=0 INSTALL_MIHOMO=1 # Official releases are pure-Go static binaries (portable on glibc + musl). STATIC="${THREE_M_UI_STATIC:-1}" REQUESTED_VERSION="" RELEASE_TAG="" TMP_FILES="" say(){ printf '%s\n' "$*"; } err(){ say "Error: $*" >&2; exit 1; } command_exists(){ command -v "$1" >/dev/null 2>&1; } need_root(){ [ "$(id -u)" -eq 0 ] || err "Please run this script as root."; } cleanup_tmp(){ # shellcheck disable=SC2086 [ -n "$TMP_FILES" ] && rm -f $TMP_FILES 2>/dev/null || true } trap cleanup_tmp EXIT INT TERM track_tmp(){ TMP_FILES="$TMP_FILES $1" } usage(){ cat </dev/null | sed 's#.*/##; s/[[:space:][:cntrl:]]*$//')" || true case "$tag" in v[0-9]*|manual-[0-9]*) printf '%s' "$tag";; *) tag="$(curl -fsSL "${1}/releases/latest" 2>/dev/null | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" || true printf '%s' "$tag" ;; esac } random_hex(){ dd if=/dev/urandom bs=1 count="${1:-32}" 2>/dev/null | od -An -tx1 | tr -d ' \n'; } # Compute SHA-256 of a file; empty string if no hasher available. file_sha256(){ f="$1" if command_exists sha256sum; then sha256sum "$f" | awk '{print $1}' elif command_exists shasum; then shasum -a 256 "$f" | awk '{print $1}' elif command_exists openssl; then openssl dgst -sha256 "$f" | awk '{print $NF}' else printf '' fi } # Verify file against release SHA256SUMS when present (soft-fail if sums missing). verify_release_sha256(){ tag="$1" asset="$2" path="$3" sums_tmp="$(mktemp)" track_tmp "$sums_tmp" if ! download "https://github.com/$REPO/releases/download/${tag}/SHA256SUMS" "$sums_tmp" 2>/dev/null; then say "Note: SHA256SUMS not published for $tag; skipping checksum verification." return 0 fi # SHA256SUMS may list "./3m-ui-linux-amd64" or "3m-ui-linux-amd64" expected="$(awk -v a="$asset" '{ n=$2; sub(/^\.\//,"",n); if (n==a) { print $1; exit } }' "$sums_tmp")" if [ -z "$expected" ]; then say "Note: $asset not listed in SHA256SUMS; skipping checksum verification." return 0 fi actual="$(file_sha256 "$path")" if [ -z "$actual" ]; then say "Warning: no sha256 tool available; cannot verify $asset." return 0 fi if [ "$actual" != "$expected" ]; then err "Checksum mismatch for $asset (expected $expected, got $actual)" fi say "Checksum OK: $asset" } write_config(){ mkdir -p "$CONFIG_DIR" "$DATA_DIR/mihomo" "$LOG_DIR" [ -f "$CONFIG_DIR/config.yaml" ] && { chmod 0600 "$CONFIG_DIR/config.yaml"; return; } # NAT-friendly: PANEL_PORT / THREE_M_UI_PORT (default 8080), optional PANEL_LISTEN / PUBLIC_URL panel_port="${PANEL_PORT:-${THREE_M_UI_PORT:-8080}}" case "$panel_port" in ''|*[!0-9]*) panel_port=8080;; esac if [ "$panel_port" -lt 1 ] || [ "$panel_port" -gt 65535 ]; then panel_port=8080; fi panel_listen="${PANEL_LISTEN:-${THREE_M_UI_LISTEN:-}}" public_url="${PUBLIC_URL:-${THREE_M_UI_PUBLIC_URL:-}}" cat > "$CONFIG_DIR/config.yaml" </dev/null 2>&1; then say "Existing Mihomo detected: $MIHOMO_BIN"; return; fi asset="$(mihomo_asset)" if [ -z "$asset" ]; then say "Warning: no official Mihomo binary for $(uname -m). Install Mihomo manually or re-run with --no-mihomo." return 0 fi tag="$(latest_tag https://github.com/MetaCubeX/mihomo)"; [ -n "$tag" ] || err "Unable to determine latest Mihomo release." tmp="$(mktemp)"; gz="$tmp.gz" track_tmp "$tmp"; track_tmp "$gz" url="https://github.com/MetaCubeX/mihomo/releases/download/${tag}/${asset}-${tag}.gz" say "Downloading Mihomo $tag..."; download "$url" "$gz"; gzip -dc "$gz" > "$tmp"; chmod 0755 "$tmp" "$tmp" -v >/dev/null 2>&1 || err "Downloaded Mihomo failed validation." install -m 0755 "$tmp" "$MIHOMO_BIN" rm -f "$tmp" "$gz" } install_release_asset(){ tag="$1"; file="$2"; destination="$3" tmp="$(mktemp)"; track_tmp "$tmp" if download "https://github.com/$REPO/releases/download/${tag}/${file}" "$tmp" 2>/dev/null; then :; else say "Release $tag does not contain $file; using the matching main-branch management script." rm -f "$tmp" download "https://raw.githubusercontent.com/$REPO/main/scripts/$file" "$tmp" fi install -m 0755 "$tmp" "$destination" rm -f "$tmp" } install_helpers(){ tag="$1" mkdir -p "$BASE" install_release_asset "$tag" 3m-ui.sh "$BASE/3m-ui.sh" install_release_asset "$tag" install.sh "$BASE/install.sh" install_release_asset "$tag" update.sh "$BASE/update.sh" install_release_asset "$tag" uninstall.sh "$BASE/uninstall.sh" install_release_asset "$tag" 3m-ui "$ENTRY" } install_panel(){ RELEASE_TAG="${REQUESTED_VERSION:-$(latest_tag https://github.com/$REPO)}" [ -n "$RELEASE_TAG" ] || err "Unable to determine latest 3m-ui release." # Official releases are pure-Go static binaries named 3m-ui-linux-. # Fall back to legacy *-static name for older tags if needed. cpu="$(arch)" asset="3m-ui-linux-${cpu}" tmp="$(mktemp)"; track_tmp "$tmp" url="https://github.com/$REPO/releases/download/${RELEASE_TAG}/${asset}" say "Downloading 3m-ui $RELEASE_TAG ($asset)..." if ! download "$url" "$tmp" 2>/dev/null; then asset="3m-ui-linux-${cpu}-static" url="https://github.com/$REPO/releases/download/${RELEASE_TAG}/${asset}" say "Retrying legacy asset name: $asset" download "$url" "$tmp" fi verify_release_sha256 "$RELEASE_TAG" "$asset" "$tmp" chmod 0755 "$tmp" "$tmp" --version >/dev/null 2>&1 || err "Downloaded 3m-ui failed executable validation." install -m 0755 "$tmp" "$APP_BIN" printf '%s\n' "$RELEASE_TAG" > "$VERSION_FILE" printf '%s\n' "$([ "$STATIC" = "1" ] && echo static || echo dynamic)" > "$MODE_FILE" chmod 0600 "$VERSION_FILE" "$MODE_FILE" rm -f "$tmp" } write_systemd(){ cat > /etc/systemd/system/$SERVICE_NAME.service </dev/null; systemctl restart "$SERVICE_NAME"; } write_openrc(){ cat > /etc/init.d/$SERVICE_NAME </dev/null 2>&1 || true; rc-service "$SERVICE_NAME" restart; } install_service(){ case "$(init_system)" in systemd) write_systemd;; openrc) write_openrc;; *) err "Unsupported init system. Supported: systemd and OpenRC.";; esac; } main(){ need_root; install_deps mkdir -p "$BASE" "$CONFIG_DIR" "$DATA_DIR/mihomo" "$LOG_DIR" if [ -t 0 ] && [ "$YES" -ne 1 ]; then say "3m-ui Installer"; say "OS: $(os_id) Architecture: $(arch) Init: $(init_system)" printf 'Install Mihomo Core too? [Y/n] '; read -r answer || true case "$answer" in n|N|no|NO) INSTALL_MIHOMO=0;; esac fi write_config if [ -x "$APP_BIN" ]; then case "$(init_system)" in systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true;; openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true;; esac fi install_panel install_mihomo install_helpers "$RELEASE_TAG" install_service say "" say "3m-ui installed successfully." say "Command: 3m-ui" panel_port="${PANEL_PORT:-${THREE_M_UI_PORT:-8080}}" say "Panel: http://SERVER_IP:${panel_port}/" say "Custom port: PANEL_PORT=8443 curl ... | bash or edit server.port in $CONFIG_DIR/config.yaml" say "Default administrator credentials are unchanged; first login requires a password change." } main "$@"