#!/usr/bin/env bash
# MyanPing for Linux: sing-box with the shared game profile, run as a systemd service.
#
#   sudo ./myanping install    # from the unpacked folder: installs to /opt/ziki-route, asks for your MyanPing email + password
#   myanping on | off | status | logs | dashboard | login | autostart on|off | doctor | uninstall
#
# The tunnel (TUN) needs root, so commands re-run themselves with sudo.
set -euo pipefail

# The folder, service and tunnel keep their first names so existing installs update in place;
# the command players type is myanping (ziki-route still works for older installs).
APP=/opt/ziki-route
UNIT=/etc/systemd/system/ziki-route.service
BIN=/usr/local/bin/myanping
OLD_BIN=/usr/local/bin/ziki-route
LOGIN_RE='^[a-z0-9_-]+:[A-Za-z0-9_-]+$' # relay login "username:secret" from /api/launcher/login
IFACE=ziki-route # tun interface_name in the profile (scripts/lib.ts)

as_root() { [ "$(id -u)" -eq 0 ] || exec sudo -- "$0" "$@"; }

ui_port() { sed -n 's/.*"external_controller": "127\.0\.0\.1:\([0-9]*\)".*/\1/p' "$APP/profile.json" | head -n1; }

dashboard_url() { echo "http://127.0.0.1:$(ui_port)/ui/#secret=$(cat "$APP/ui-secret")"; }

# Replies from the relay and from direct connections come back IN through the tun interface.
# Firewalls that reject unknown interfaces (firewalld on Bazzite / Fedora, ufw) would drop them
# all and the whole internet stops while the booster is on, so trust the interface.
firewall_allow() {
  if command -v firewall-cmd >/dev/null && firewall-cmd --state >/dev/null 2>&1; then
    firewall-cmd --quiet --permanent --zone=trusted --add-interface="$IFACE" 2>/dev/null || true
    firewall-cmd --quiet --zone=trusted --change-interface="$IFACE" 2>/dev/null || true
  fi
  if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
    ufw allow in on "$IFACE" >/dev/null || true
  fi
}

firewall_remove() {
  if command -v firewall-cmd >/dev/null && firewall-cmd --state >/dev/null 2>&1; then
    firewall-cmd --quiet --permanent --zone=trusted --remove-interface="$IFACE" 2>/dev/null || true
    firewall-cmd --quiet --zone=trusted --remove-interface="$IFACE" 2>/dev/null || true
  fi
  if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
    ufw delete allow in on "$IFACE" >/dev/null 2>&1 || true
  fi
}

# What to send MyanPing support when the booster misbehaves. Turns the booster on for a moment to test.
doctor() {
  echo "== system"; (. /etc/os-release && echo "$PRETTY_NAME"); uname -r
  echo "== booster"; cat "$APP/VERSION" 2>/dev/null || echo "not installed"; systemctl is-active ziki-route || true
  echo "== firewall"
  if command -v firewall-cmd >/dev/null && firewall-cmd --state >/dev/null 2>&1; then
    echo "firewalld on; $IFACE zone: $(firewall-cmd --get-zone-of-interface="$IFACE" 2>/dev/null || echo none)"
  else echo "firewalld off/absent"; fi
  if command -v ufw >/dev/null; then ufw status 2>/dev/null | head -1; fi
  if command -v nft >/dev/null; then echo "nft rules mentioning reject/drop: $(nft list ruleset 2>/dev/null | grep -cE 'reject|drop')"; fi
  echo "== dns"; grep -E '^nameserver' /etc/resolv.conf || true
  local was_on=0
  systemctl is-active --quiet ziki-route && was_on=1
  [ "$was_on" = 1 ] || systemctl start ziki-route
  sleep 3
  echo "== with booster on"
  ip rule | grep -c 2022 | sed 's/^/booster route rules: /'
  ip route get 1.1.1.1 | head -1
  echo "web by IP:  $(curl -s -o /dev/null -m 8 -w '%{http_code}' http://1.1.1.1/ || echo FAIL)"
  echo "web by name: $(curl -s -o /dev/null -m 8 -w '%{http_code}' https://www.google.com/ || echo FAIL)"
  [ "$was_on" = 1 ] || systemctl stop ziki-route
  echo "== last log lines"; journalctl -u ziki-route -n 15 --no-pager -o cat 2>/dev/null | sed -E 's/(password|secret)[^,]*/\1 <hidden>/g'
}

json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' <<<"$1"; }
json_field() { sed -n "s/.*\"$1\": *\"\([^\"]*\)\".*/\1/p" <<<"$2" | head -n1; }

# This PC for "one free trial per PC": a hash of /etc/machine-id (the website never sees the real id).
device_field() {
  local id; id="$(tr -d '[:space:]' </etc/machine-id 2>/dev/null || true)"
  [ -n "$id" ] && printf ',"device":"%s"' "$(printf 'myanping-device:%s' "$id" | sha256sum | cut -d' ' -f1)"
}

# Signs in with the MyanPing website email + password; prints the relay login "username:secret".
sign_in() {
  local site email pass reply error
  site="$(cat "$APP/DASHBOARD" 2>/dev/null || true)"
  [ -n "$site" ] || { echo "This build has no MyanPing website to sign in to." >&2; exit 1; }
  while true; do
    read -rp "MyanPing email: " email </dev/tty
    read -rsp "Password: " pass </dev/tty; echo >&2
    reply="$(curl -sS -m 20 -H 'content-type: application/json' \
      --data-binary "{\"email\":\"$(json_escape "$email")\",\"password\":\"$(json_escape "$pass")\"$(device_field)}" \
      "$site/api/launcher/login" 2>/dev/null || true)"
    local login; login="$(json_field login "$reply")"
    if [[ "$login" =~ $LOGIN_RE ]]; then echo "$login"; return; fi
    error="$(json_field error "$reply")"
    case "$error" in
      invalid_credentials) echo "Wrong email or password." >&2 ;;
      email_not_confirmed) echo "Confirm your email first: open the link MyanPing emailed you." >&2 ;;
      no_plan) echo "No active plan yet. Buy one on your MyanPing account page." >&2; exit 1 ;;
      expired) echo "Your plan has ended. Add time on your MyanPing account page." >&2; exit 1 ;;
      banned | disabled) echo "This account is disabled. Contact MyanPing support." >&2; exit 1 ;;
      not_a_player) echo "This is a staff account. Sign in with a player account." >&2 ;;
      rate_limited) echo "Too many tries. Wait 15 minutes, then try again." >&2; exit 1 ;;
      trial_used) echo "This PC already used a free trial. Buy a plan on your MyanPing account page." >&2; exit 1 ;;
      update_required) echo "Please download the latest MyanPing for Linux, then sign in." >&2; exit 1 ;;
      "") echo "Can't reach $site. Check your internet." >&2; exit 1 ;;
      *) echo "Can't sign in right now ($error). Try again in a minute." >&2; exit 1 ;;
    esac
  done
}

# Fills the shared profile with this login + this PC's dashboard secret (never published).
# With no argument it asks for the MyanPing email + password; "username:secret" reuses a saved login.
login() {
  local pair="${1:-}"
  [[ "$pair" =~ $LOGIN_RE ]] || pair="$(sign_in)"
  local user="${pair%%:*}" pass="${pair#*:}"
  umask 077
  if [ ! -s "$APP/ui-secret" ]; then
    head -c 18 /dev/urandom | base64 | tr '+/' '-_' >"$APP/ui-secret"
  fi
  sed -e "s|{{login}}|$user:$pass|g" \
      -e "s|\"secret\": \"\"|\"secret\": \"$(cat "$APP/ui-secret")\"|" \
      "$APP/profile.json" >"$APP/config.json"
  if ! "$APP/sing-box" check -c "$APP/config.json" -D "$APP"; then
    echo "sing-box rejected the config." >&2; exit 1
  fi
  echo "Signed in."
  if systemctl is-active --quiet ziki-route; then systemctl restart ziki-route; echo "Restarted with the new login."; fi
}

install() {
  local src
  src="$(cd "$(dirname "$0")" && pwd)"
  [ -f "$src/profile.json" ] || { echo "Run install from the unpacked folder." >&2; exit 1; }
  command -v systemctl >/dev/null || { echo "Needs systemd." >&2; exit 1; }
  systemctl stop ziki-route 2>/dev/null || true
  mkdir -p "$APP"
  if [ "$src" != "$APP" ]; then
    rm -rf "$APP/ui"
    cp -r "$src/sing-box" "$src/libcronet.so" "$src/LICENSE" "$src/profile.json" "$src/ui" "$src/myanping" "$src/VERSION" "$src/DASHBOARD" "$APP/"
  fi
  rm -f "$APP/ziki-route" # the script's name before 1.1
  chmod 755 "$APP" "$APP/sing-box" "$APP/myanping"
  ln -sf "$APP/myanping" "$BIN"
  ln -sf "$APP/myanping" "$OLD_BIN"
  cat >"$UNIT" <<EOF
[Unit]
Description=MyanPing game booster
After=network-online.target
Wants=network-online.target

[Service]
WorkingDirectory=$APP
# A tunnel left by a killed or older sing-box would make this start fail; clear it first.
ExecStartPre=-/bin/sh -c 'ip link delete $IFACE 2>/dev/null || true'
ExecStart=$APP/sing-box run -c config.json -D $APP
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  firewall_allow
  echo "Installed $(cat "$APP/VERSION") to $APP."
  if [ -s "$APP/config.json" ]; then
    # Updating: keep the saved login, re-fill it into the new profile.
    local login_pair
    login_pair="$(sed -n 's/.*"password": "\([^"]*\)".*/\1/p' "$APP/config.json" | head -n1)"
    login "$login_pair"
  else
    login
  fi
  echo
  echo "Turn on:  myanping on        Turn off: myanping off"
  echo "Start with the PC: myanping autostart on"
}

cmd="${1:-help}"
case "$cmd" in
  install) as_root "$@"; install ;;
  login) as_root "$@"; login "${2:-}" ;;
  on)
    as_root "$@"
    [ -s "$APP/config.json" ] || login
    firewall_allow
    systemctl start ziki-route
    sleep 1
    systemctl is-active --quiet ziki-route || { journalctl -u ziki-route -n 20 --no-pager; exit 1; }
    echo "On. Dashboard: $(dashboard_url)"
    ;;
  off) as_root "$@"; systemctl stop ziki-route; echo "Off." ;;
  status) systemctl status ziki-route --no-pager || true ;;
  logs) as_root "$@"; journalctl -u ziki-route -f ;;
  dashboard) as_root "$@"; dashboard_url ;;
  doctor) as_root "$@"; doctor ;;
  autostart)
    as_root "$@"
    case "${2:-}" in
      on) systemctl enable ziki-route ;;
      off) systemctl disable ziki-route ;;
      *) echo "Usage: myanping autostart on|off" >&2; exit 2 ;;
    esac
    ;;
  uninstall)
    as_root "$@"
    systemctl disable --now ziki-route 2>/dev/null || true
    firewall_remove
    rm -f "$UNIT" "$BIN" "$OLD_BIN"
    systemctl daemon-reload
    rm -rf "$APP"
    echo "Removed."
    ;;
  version) cat "$APP/VERSION" 2>/dev/null || cat "$(dirname "$0")/VERSION" ;;
  *)
    sed -n '2,7p' "$0" | sed 's/^# \{0,1\}//'
    ;;
esac
