#!/usr/bin/env bash
# MAX FRP 一体管理器
# 服务端 FRPS + 客户端 FRPC 安装、配置、运行与诊断
# 用法: bash <(curl -fsSL https://dl.unvmax.com/linux/scripts/frp/latest.sh)
set -Eeuo pipefail

ACTION="${1:-menu}"
FRP_VERSION="${FRP_VERSION:-}"
FRP_REPO="fatedier/frp"
INSTALL_DIR="/usr/local/frp"
BIN_DIR="/usr/local/bin"
FRPS_BIN="${BIN_DIR}/frps"
FRPC_BIN="${BIN_DIR}/frpc"
SERVER_DIR="/etc/frp"
CLIENT_DIR="/etc/frp-client"
FRPS_CONFIG="${SERVER_DIR}/frps.toml"
FRPC_CONFIG="${CLIENT_DIR}/frpc.toml"
FRPS_SERVICE="/etc/systemd/system/frps.service"
FRPC_SERVICE="/etc/systemd/system/frpc.service"
BACKUP_ROOT="/root/max-backup/frp"
TMP_DIR=""

BC='\033[38;5;220m'; BG='\033[38;5;220m'; Y='\033[1;33m'; R='\033[0;31m'
BW='\033[38;5;255m'; W='\033[38;5;250m'; GR='\033[38;5;242m'; N='\033[0m'
red(){ printf '%b%s%b\n' "$R" "$*" "$N"; }; green(){ printf '%b%s%b\n' "$BG" "$*" "$N"; }
yellow(){ printf '%b%s%b\n' "$Y" "$*" "$N"; }; blue(){ printf '%b%s%b\n' "$BC" "$*" "$N"; }
die(){ red "错误：$*"; exit 1; }
cleanup(){ if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then rm -rf "$TMP_DIR"; fi; return 0; }
trap cleanup EXIT
need_root(){ [[ $EUID -eq 0 ]] || die "请使用 root 运行。"; }
pause_menu(){ echo; echo -ne "  ${GR}按 Enter 返回菜单...${N}"; read -r _ || true; }
header(){ clear 2>/dev/null || true; echo; echo -e "  ${BC}┌──────────────────────────────────────────────────┐${N}"; echo -e "  ${BG}│     🔀  MAX FRP 一体管理器${N}"; echo -e "  ${BC}└──────────────────────────────────────────────────┘${N}"; echo; }
section(){ echo -e "  ${BC}── $1 ───────────────────────────────────${N}"; }
item(){ printf '  %b%-4s%b  %b%-12s%b %b%s%b\n' "$BC" "$1" "$N" "$BW" "$2" "$N" "$GR" "$3" "$N"; }
arch(){ case "$(uname -m)" in x86_64|amd64) echo amd64;; aarch64|arm64) echo arm64;; armv7l|armv7) echo arm;; *) die "暂不支持架构：$(uname -m)";; esac; }
backup(){ local label="$1" d; d="$BACKUP_ROOT/$label-$(date +%Y%m%d-%H%M%S)"; install -d -m 700 "$d"; [[ -f "$FRPS_CONFIG" ]] && cp -a "$FRPS_CONFIG" "$d/"; [[ -f "$FRPC_CONFIG" ]] && cp -a "$FRPC_CONFIG" "$d/"; [[ -f "$FRPS_SERVICE" ]] && cp -a "$FRPS_SERVICE" "$d/"; [[ -f "$FRPC_SERVICE" ]] && cp -a "$FRPC_SERVICE" "$d/"; echo "$d"; }
latest_version(){ curl -fsSL --connect-timeout 12 "https://api.github.com/repos/$FRP_REPO/releases/latest" | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^\"]+)".*/\1/p' | head -1; }
download_frp(){
  need_root; local ver a url tgz sumurl expected actual; a="$(arch)"; ver="${FRP_VERSION:-$(latest_version)}"; [[ -n "$ver" ]] || die "获取 FRP 最新版本失败，请设置 FRP_VERSION 后重试。"
  TMP_DIR="$(mktemp -d)"; tgz="$TMP_DIR/frp.tar.gz"; url="https://github.com/$FRP_REPO/releases/download/v${ver}/frp_${ver}_linux_${a}.tar.gz"; sumurl="https://github.com/$FRP_REPO/releases/download/v${ver}/frp_${ver}_checksums.txt"
  blue "下载 FRP v${ver}（${a}）..."; curl -fL --retry 2 --connect-timeout 15 -o "$tgz" "$url" || die "下载失败：$url"
  curl -fsSL --connect-timeout 15 "$sumurl" -o "$TMP_DIR/checksums.txt" || die "无法下载 SHA256 校验文件"
  expected="$(awk -v f="frp_${ver}_linux_${a}.tar.gz" '$2 ~ f {print $1; exit}' "$TMP_DIR/checksums.txt")"; actual="$(sha256sum "$tgz" | awk '{print $1}')"
  [[ -n "$expected" && "$expected" == "$actual" ]] || die "SHA256 校验失败，已拒绝安装。"
  tar -xzf "$tgz" -C "$TMP_DIR"; install -d -m 755 "$INSTALL_DIR"; rm -rf "$INSTALL_DIR"/*; cp -a "$TMP_DIR/frp_${ver}_linux_${a}/." "$INSTALL_DIR/"
  install -m 755 "$INSTALL_DIR/frps" "$FRPS_BIN"; install -m 755 "$INSTALL_DIR/frpc" "$FRPC_BIN"; green "FRP v${ver} 已安装并完成 SHA256 校验。"
}
write_frps_service(){ cat >"$FRPS_SERVICE" <<EOF
[Unit]
Description=MAX FRP Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$FRPS_BIN -c $FRPS_CONFIG
Restart=always
RestartSec=5
LimitNOFILE=1048576
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
}
write_frpc_service(){ cat >"$FRPC_SERVICE" <<EOF
[Unit]
Description=MAX FRP Client
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$FRPC_BIN -c $FRPC_CONFIG
Restart=always
RestartSec=5
LimitNOFILE=1048576
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
}
valid_port(){ [[ "$1" =~ ^[0-9]+$ ]] && (( $1>=1 && $1<=65535 )); }
port_free(){ ! ss -ltnH "sport = :$1" 2>/dev/null | grep -q .; }
random_token(){ tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32; }
ask_port(){ local p="$1" prompt="$2"; read -rp "$prompt [$p]: " v; v="${v:-$p}"; valid_port "$v" || die "端口无效：$v"; echo "$v"; }
manage_ufw(){ local p="$1"; command -v ufw >/dev/null 2>&1 || return 0; ufw allow "$p/tcp" comment 'MAX-FRP' >/dev/null 2>&1 || true; }
install_server(){
  need_root; header; section "安装 FRPS 服务端"; local bind dashboard token; bind="$(ask_port 7000 'FRPS 控制端口')"; dashboard="$(ask_port 7500 '仪表盘端口（仅本机监听）')"; port_free "$bind" || die "端口 $bind 已被占用。"; backup server-install >/dev/null; download_frp; install -d -m 700 "$SERVER_DIR"; token="$(random_token)"
  cat >"$FRPS_CONFIG" <<EOF
bindPort = $bind
transport.maxPoolCount = 5
auth.method = "token"
auth.token = "$token"
webServer.addr = "127.0.0.1"
webServer.port = $dashboard
webServer.user = "admin"
webServer.password = "$token"
log.to = "/var/log/frps.log"
log.level = "info"
EOF
  write_frps_service; systemctl daemon-reload; systemctl enable --now frps; manage_ufw "$bind"; sleep 1; systemctl is-active --quiet frps || { journalctl -u frps -n 30 --no-pager; die "FRPS 未能启动。"; }
  green "FRPS 服务端已启动。"; echo -e "  ${GR}控制端口：${W}$bind${N}"; echo -e "  ${GR}认证 Token：${W}$token${N}"; echo -e "  ${Y}请立即保存 Token；它同时是本地仪表盘密码。${N}"; pause_menu
}
service_summary(){
  local service="$1"
  if ! systemctl list-unit-files "${service}.service" --no-legend 2>/dev/null | grep -q "${service}.service" && ! systemctl status "$service" >/dev/null 2>&1; then
    printf '%b○ 未安装%b' "$GR" "$N"
  elif systemctl is-active --quiet "$service"; then
    printf '%b● 运行中%b' "$BG" "$N"
  else
    printf '%b● 已停止%b' "$Y" "$N"
  fi
}

autostart_summary(){
  local service="$1"
  if systemctl is-enabled --quiet "$service" 2>/dev/null; then
    printf '%b✅ 已启用%b' "$BG" "$N"
  else
    printf '%b❌ 未启用%b' "$GR" "$N"
  fi
}

show_status_summary(){
  local frp_ver server_port dashboard_port client_host client_port rule_count
  frp_ver="$($FRPS_BIN --version 2>/dev/null || $FRPC_BIN --version 2>/dev/null || true)"
  frp_ver="${frp_ver%%$'\n'*}"; frp_ver="${frp_ver:-未安装}"
  server_port="$(sed -nE 's/^bindPort[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' "$FRPS_CONFIG" 2>/dev/null | head -1 || true)"
  dashboard_port="$(sed -nE 's/^webServer\.port[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' "$FRPS_CONFIG" 2>/dev/null | head -1 || true)"
  client_host="$(sed -nE 's/^serverAddr[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$FRPC_CONFIG" 2>/dev/null | head -1 || true)"
  client_port="$(sed -nE 's/^serverPort[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' "$FRPC_CONFIG" 2>/dev/null | head -1 || true)"
  rule_count="$(grep -c '^\[\[proxies\]\]' "$FRPC_CONFIG" 2>/dev/null || true)"
  echo -e "  ${BC}┌── 实时服务状态 ──────────────────────────────────┐${N}"
  echo -e "  ${GR}│${N}  核心 ${W}${frp_ver}${N}"
  echo -e "  ${GR}│${N}  FRPS 服务端  $(service_summary frps)  $(autostart_summary frps)"
  if [[ -n "$server_port" ]]; then
    echo -e "  ${GR}│${N}    控制端口 ${W}${server_port}${N}   仪表盘 ${W}127.0.0.1:${dashboard_port:-未配置}${N}"
  fi
  echo -e "  ${GR}│${N}  FRPC 客户端  $(service_summary frpc)  $(autostart_summary frpc)"
  if [[ -n "$client_host" ]]; then
    echo -e "  ${GR}│${N}    中心 ${W}${client_host}:${client_port:-未配置}${N}   规则 ${W}${rule_count} 条${N}"
  fi
  echo -e "  ${BC}└──────────────────────────────────────────────────┘${N}"
  echo
}

server_info(){ header; section "FRPS 服务端状态"; if [[ ! -f "$FRPS_CONFIG" ]]; then yellow "未安装 FRPS 服务端。"; pause_menu; return; fi; printf '  服务：'; systemctl is-active frps 2>/dev/null || true; echo -e "  ${GR}配置：${W}$FRPS_CONFIG${N}"; sed -nE 's/^(bindPort|webServer\.addr|webServer\.port)[[:space:]]*=[[:space:]]*(.*)/  \1 = \2/p' "$FRPS_CONFIG"; echo; ss -ltnp | grep -E "frps|:(7000|7500)" || true; pause_menu; }
server_restart(){ need_root; systemctl restart frps; systemctl is-active --quiet frps && green "FRPS 已重启。" || die "重启失败。"; pause_menu; }
server_logs(){ header; journalctl -u frps -n 80 --no-pager || true; pause_menu; }
server_edit(){ need_root; [[ -f "$FRPS_CONFIG" ]] || die "未安装服务端。"; backup server-edit >/dev/null; ${EDITOR:-nano} "$FRPS_CONFIG"; "$FRPS_BIN" verify -c "$FRPS_CONFIG" || die "配置校验失败，备份位于 $BACKUP_ROOT"; systemctl restart frps; green "配置已校验并重启。"; pause_menu; }
install_client(){
  need_root; header; section "安装 FRPC 客户端"; local server port token; read -rp 'FRPS 地址（IP/域名）: ' server; [[ -n "$server" ]] || die "FRPS 地址不能为空。"; port="$(ask_port 7000 'FRPS 控制端口')"; read -rsp 'FRPS Token: ' token; echo; [[ -n "$token" ]] || die "Token 不能为空。"; backup client-install >/dev/null; download_frp; install -d -m 700 "$CLIENT_DIR"
  cat >"$FRPC_CONFIG" <<EOF
serverAddr = "$server"
serverPort = $port
auth.method = "token"
auth.token = "$token"
transport.protocol = "tcp"

# 在客户端菜单中添加穿透条目；也可手动编辑本文件。
EOF
  write_frpc_service; systemctl daemon-reload; systemctl enable frpc; green "FRPC 已安装；请先添加至少一条穿透规则。"; pause_menu
}
add_tcp_proxy(){
  need_root; [[ -f "$FRPC_CONFIG" ]] || die "请先安装 FRPC 客户端。"; header; section "新增 TCP 穿透规则"; local name lip lport rport; read -rp '规则名称（英文/数字/连字符）: ' name; [[ "$name" =~ ^[A-Za-z0-9_-]+$ ]] || die "名称只能包含英文、数字、_ 或 -。"; read -rp '本地目标地址 [127.0.0.1]: ' lip; lip="${lip:-127.0.0.1}"; lport="$(ask_port 80 '本地目标端口')"; rport="$(ask_port 8080 'FRPS 公网端口')"; backup client-proxy >/dev/null
  cat >>"$FRPC_CONFIG" <<EOF

[[proxies]]
name = "$name"
type = "tcp"
localIP = "$lip"
localPort = $lport
remotePort = $rport
EOF
  "$FRPC_BIN" verify -c "$FRPC_CONFIG" || die "配置校验失败，未重启服务。"; systemctl enable --now frpc; systemctl restart frpc; systemctl is-active --quiet frpc || die "FRPC 启动失败。"; green "规则 $name 已添加：公网 $rport → $lip:$lport"; pause_menu
}
client_info(){ header; section "FRPC 客户端状态"; if [[ ! -f "$FRPC_CONFIG" ]]; then yellow "未安装 FRPC 客户端。"; pause_menu; return; fi; printf '  服务：'; systemctl is-active frpc 2>/dev/null || true; echo -e "  ${GR}配置：${W}$FRPC_CONFIG${N}"; sed -nE 's/^(serverAddr|serverPort)[[:space:]]*=[[:space:]]*(.*)/  \1 = \2/p' "$FRPC_CONFIG"; echo -e "  ${GR}规则：${N}"; grep -E '^name[[:space:]]*=' "$FRPC_CONFIG" | sed 's/^/    /' || true; pause_menu; }
client_restart(){ need_root; systemctl restart frpc; systemctl is-active --quiet frpc && green "FRPC 已重启。" || die "重启失败。"; pause_menu; }
client_logs(){ header; journalctl -u frpc -n 80 --no-pager || true; pause_menu; }
client_edit(){ need_root; [[ -f "$FRPC_CONFIG" ]] || die "未安装客户端。"; backup client-edit >/dev/null; ${EDITOR:-nano} "$FRPC_CONFIG"; "$FRPC_BIN" verify -c "$FRPC_CONFIG" || die "配置校验失败，未重启。"; systemctl restart frpc; green "配置已校验并重启。"; pause_menu; }
uninstall_role(){ need_root; local role="$1" service config; if [[ "$role" == server ]]; then service=frps; config="$SERVER_DIR"; else service=frpc; config="$CLIENT_DIR"; fi; read -rp "确认卸载 $service（保留备份）？输入 YES: " ok; [[ "$ok" == YES ]] || return; backup "${role}-uninstall" >/dev/null; systemctl disable --now "$service" 2>/dev/null || true; rm -f "/etc/systemd/system/${service}.service"; rm -rf "$config"; systemctl daemon-reload; green "$service 已卸载，备份保留在 $BACKUP_ROOT。"; pause_menu; }
server_menu(){ while true; do header; section "FRPS 服务端"; item S1 "安装/重装" "下载校验、生成 Token、创建 systemd"; item S2 "查看状态" "监听端口、服务状态、配置摘要"; item S3 "重启服务" "重启 frps"; item S4 "查看日志" "最近 80 行 journal"; item S5 "编辑配置" "校验 TOML 后重启"; item S0 "卸载服务端" "备份后清理服务与配置"; item 0 "返回" "返回主菜单"; echo; read -rp '  输入编号 → ' c || c=0; case "${c^^}" in S1) install_server;;S2) server_info;;S3) server_restart;;S4) server_logs;;S5) server_edit;;S0) uninstall_role server;;0) return;;*) yellow '无效选项。'; sleep 1;;esac; done; }
client_menu(){ while true; do header; section "FRPC 客户端"; item C1 "安装/导入" "设置 FRPS 地址、端口和 Token"; item C2 "新增 TCP规则" "公网端口 → 本地 IP:端口"; item C3 "查看状态" "服务与规则摘要"; item C4 "重启服务" "重启 frpc"; item C5 "查看日志" "最近 80 行 journal"; item C6 "编辑配置" "校验 TOML 后重启"; item C0 "卸载客户端" "备份后清理服务与配置"; item 0 "返回" "返回主菜单"; echo; read -rp '  输入编号 → ' c || c=0; case "${c^^}" in C1) install_client;;C2) add_tcp_proxy;;C3) client_info;;C4) client_restart;;C5) client_logs;;C6) client_edit;;C0) uninstall_role client;;0) return;;*) yellow '无效选项。'; sleep 1;;esac; done; }
main(){ while true; do header; show_status_summary; section "FRP 服务端 + 客户端一体管理"; item 1 "FRPS 服务端" "穿透中心、Token、仪表盘与端口管理"; item 2 "FRPC 客户端" "连接中心、新增 TCP 穿透规则"; item 3 "版本信息" "显示本机 FRP 版本"; item 0 "退出" "输入 0 立即结束脚本"; echo; read -rp '  输入编号 → ' c || c=0; case "$c" in 1) server_menu;;2) client_menu;;3) header; "$FRPS_BIN" --version 2>/dev/null || "$FRPC_BIN" --version 2>/dev/null || yellow '未安装 FRP。'; pause_menu;;0) exit 0;;*) yellow '无效选项。'; sleep 1;;esac; done; }
case "$ACTION" in menu|"") main;; server) server_menu;; client) client_menu;; *) die "用法：$0 [menu|server|client]";; esac
