Загрузка данных


#!/usr/bin/env bash
# ============================================================================
#  o3-claude-setup — настройка доступа к Claude Code из РФ через свой SSH-сервер.
#                    Только для macOS (нужен Homebrew).
#
#  Зачем это нужно:
#    Anthropic не пускает Claude Code с российских IP. Скрипт заворачивает
#    трафик claude через твой зарубежный VPS — для Anthropic запросы выглядят
#    приходящими из страны сервера. Проксируется ТОЛЬКО claude, остальные
#    программы (git, brew, браузер) ходят напрямую.
#
#  Как работает (путь одного запроса claude):
#    claude
#      -> по HTTPS_PROXY идёт на локальный HTTP-прокси privoxy (127.0.0.1:8118)
#      -> privoxy перекладывает запрос в локальный SOCKS5 (127.0.0.1:1080)
#      -> SOCKS5 = вход в SSH-туннель (ssh -D) до твоего VPS
#      -> из VPS запрос выходит в интернет уже с IP сервера
#      -> и доходит до api.anthropic.com
#    Зачем два прокси: claude понимает только HTTP-прокси, а туннель даёт
#    SOCKS5 — privoxy служит переходником HTTP -> SOCKS5.
#
#  Что делает скрипт (по шагам):
#    1. Ставит privoxy и autossh через Homebrew.
#    2. Генерит отдельный SSH-ключ ~/.ssh/o3-claude под этот туннель.
#    3. Авторизует ключ на сервере: сам через ssh-copy-id (если знаешь пароль),
#       либо печатает готовую строку для ручного добавления в authorized_keys.
#    4. Настраивает privoxy как переходник SOCKS5 -> HTTP.
#    5. Ставит туннель под launchd: автозапуск при логине и авто-переподключение
#       при обрыве или смене сети (autossh).
#    6. Кладёт команду o3-claude-health для проверки всей цепочки.
#    7. Добавляет в ~/.zshrc алиас o3-claude (claude с прокси-переменными).
#    8. Ставит сам Claude Code через туннель, если он ещё не установлен.
#    Все артефакты помечены именем o3-claude — легко найти и удалить.
#
#  После установки:
#    Запуск Claude:      o3-claude
#    Проверка цепочки:   o3-claude-health
#    Перезапуск прокси:  o3-claude-setup --restart
#
#  Смена VPS / полное удаление:
#    o3-claude-setup --uninstall            # снести всю настройку и откатить
#    o3-claude-setup new-user@new-host      # затем чистая установка на новый VPS
#
#  Использование:
#    o3-claude-setup user@host
#    o3-claude-setup --host 1.2.3.4 --user root --port 22
#    o3-claude-setup                 # спросит сервер интерактивно
#    o3-claude-setup --restart       # перезапустить прокси (:1080 и :8118)
#
#  Опции:
#    -H, --host HOST     адрес сервера
#    -u, --user USER     ssh-пользователь (по умолчанию root)
#    -p, --port PORT     ssh-порт (по умолчанию 22)
#    -k, --key  PATH     путь к ключу (по умолчанию ~/.ssh/o3-claude)
#        --socks PORT    локальный SOCKS5-порт (по умолчанию 1080)
#        --http  PORT    локальный HTTP-порт privoxy (по умолчанию 8118)
#    -r, --restart       перезапустить SOCKS-туннель и privoxy (без переустановки)
#        --uninstall     снести настройку и откатить все изменения
#    -h, --help
# ============================================================================
set -euo pipefail

info() { printf "\033[36m==>\033[0m %s\n" "$1"; }
ok()   { printf "\033[32mOK\033[0m  %s\n" "$1"; }
warn() { printf "\033[33m!!\033[0m  %s\n" "$1"; }
die()  { printf "\033[31mОШИБКА:\033[0m %s\n" "$1" >&2; exit 1; }

usage() { awk 'NR==1{next} /^#/{sub(/^# ?/,"");print;next} {exit}' "$0"; }

# Откат: удаляет все артефакты (историч. имена autossh-proxy / cc-proxy / o3-claude).
# Не трогает личный ключ ~/.ssh/id_*, пакеты privoxy/autossh и сам Claude Code.
uninstall() {
  [[ "$(uname)" == "Darwin" ]] || die "откат рассчитан на macOS."
  info "Откат настройки o3-claude — удаляю артефакты..."

  # launchd-агенты
  for l in autossh-proxy cc-proxy o3-claude; do
    launchctl unload "$HOME/Library/LaunchAgents/com.user.$l.plist" 2>/dev/null || true
    rm -f "$HOME/Library/LaunchAgents/com.user.$l.plist"
  done

  # живые туннели
  pkill -f 'autossh' 2>/dev/null || true
  pkill -f 'ssh .*-D ' 2>/dev/null || true

  # сгенерированные команды и логи
  rm -f "$HOME/.local/bin/proxy-health" "$HOME/.local/bin/cc-proxy-health" \
        "$HOME/.local/bin/o3-claude-health" "$HOME/.local/bin/cc-install"
  rm -f /tmp/autossh-proxy.err /tmp/autossh-proxy.out \
        /tmp/cc-proxy.err /tmp/cc-proxy.out /tmp/o3-claude.err /tmp/o3-claude.out

  # ключи, созданные скриптом (личные ~/.ssh/id_* НЕ трогаем)
  rm -f "$HOME/.ssh/cc-proxy" "$HOME/.ssh/cc-proxy.pub" \
        "$HOME/.ssh/o3-claude" "$HOME/.ssh/o3-claude.pub"

  # блоки алиасов в ~/.zshrc
  if [[ -f "$HOME/.zshrc" ]]; then
    for m in cc-proxy o3-claude; do
      sed -i '' "/# >>> $m alias >>>/,/# <<< $m alias <<</d" "$HOME/.zshrc"
    done
  fi

  # privoxy: убрать наши блоки, вернуть исходный socket-timeout, выключить сервис
  local CFG; CFG="$(brew --prefix 2>/dev/null)/etc/privoxy/config"
  if [[ -f "$CFG" ]]; then
    for m in cc-proxy o3-claude; do
      sed -i '' "/# >>> $m >>>/,/# <<< $m <<</d" "$CFG"
    done
    sed -i '' '/^forward-socks5t/d' "$CFG"
    sed -i '' '/Перенаправление всего трафика/d' "$CFG"
    sed -i '' -E 's/^# (socket-timeout[[:space:]]+.*)$/\1/' "$CFG"
    brew services stop privoxy >/dev/null 2>&1 || true
  fi

  ok "Откат завершён."
  echo "  Не тронуты: личный ключ ~/.ssh/id_*, пакеты privoxy/autossh, сам Claude Code."
  echo "  Старый VPS можно забыть — его authorized_keys больше не используется."
  echo "  Чистая установка на новый сервер:  o3-claude-setup new-user@new-host"
  echo "  (после отката перезапусти терминал или: source ~/.zshrc)"
}

# Ждёт, пока порт начнёт слушать (до tries*0.5 сек).
wait_port() {
  local port="$1" tries="${2:-24}" i
  for ((i = 0; i < tries; i++)); do
    lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
    sleep 0.5
  done
  return 1
}

# Перезапуск обеих половин цепочки без переустановки:
# SOCKS-туннель (autossh под launchd) и privoxy. Конфиги не трогает.
restart_proxies() {
  [[ "$(uname)" == "Darwin" ]] || die "перезапуск рассчитан на macOS."
  command -v brew >/dev/null 2>&1 || die "нет Homebrew. Поставь с https://brew.sh и запусти снова."
  local PLIST="$HOME/Library/LaunchAgents/com.user.o3-claude.plist"
  local FAILED=0

  info "Перезапускаю SOCKS-туннель (:$SOCKS_PORT)..."
  if [[ -f "$PLIST" ]]; then
    launchctl unload "$PLIST" 2>/dev/null || true
    pkill -f "autossh.*-D $SOCKS_PORT" 2>/dev/null || true
    pkill -f "ssh .*-D $SOCKS_PORT" 2>/dev/null || true
    sleep 1
    launchctl load "$PLIST" || die "не удалось загрузить $PLIST"
    if wait_port "$SOCKS_PORT"; then
      ok "Туннель слушает :$SOCKS_PORT"
    else
      FAILED=1
      warn "порт :$SOCKS_PORT не поднялся — смотри /tmp/o3-claude.err (ключ авторизован на сервере?)"
    fi
  else
    FAILED=1
    warn "нет $PLIST — туннель ещё не настроен. Установка: o3-claude-setup user@host"
  fi

  info "Перезапускаю privoxy (:$HTTP_PORT)..."
  if brew list privoxy >/dev/null 2>&1; then
    brew services restart privoxy >/dev/null || die "не удалось перезапустить privoxy"
    if wait_port "$HTTP_PORT"; then
      ok "Privoxy слушает :$HTTP_PORT"
    else
      FAILED=1
      warn "порт :$HTTP_PORT не поднялся — проверь $(brew --prefix)/etc/privoxy/config"
    fi
  else
    FAILED=1
    warn "privoxy не установлен. Установка: o3-claude-setup user@host"
  fi

  if [[ -x "$HOME/.local/bin/o3-claude-health" ]]; then
    echo
    SOCKS_PORT="$SOCKS_PORT" HTTP_PORT="$HTTP_PORT" "$HOME/.local/bin/o3-claude-health" || true
  fi

  return "$FAILED"
}

# --- параметры --------------------------------------------------------------
SSH_HOST=""; SSH_USER="root"; SSH_PORT="22"; DO_UNINSTALL=0; DO_RESTART=0
KEY="$HOME/.ssh/o3-claude"; SOCKS_PORT="1080"; HTTP_PORT="8118"
NO_PROXY_VAL="localhost,127.0.0.1,::1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.ru,.ozon.ru,.o3.ru,.o3t.ru"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -H|--host)  SSH_HOST="$2"; shift 2;;
    -u|--user)  SSH_USER="$2"; shift 2;;
    -p|--port)  SSH_PORT="$2"; shift 2;;
    -k|--key)   KEY="$2"; shift 2;;
    --socks)    SOCKS_PORT="$2"; shift 2;;
    --http)     HTTP_PORT="$2"; shift 2;;
    -r|--restart|--reload) DO_RESTART=1; shift;;
    --uninstall|--rollback) DO_UNINSTALL=1; shift;;
    -h|--help)  usage; exit 0;;
    -*)         die "неизвестный флаг: $1 (см. --help)";;
    *)          if [[ "$1" == *@* ]]; then SSH_USER="${1%@*}"; SSH_HOST="${1#*@}"; else SSH_HOST="$1"; fi; shift;;
  esac
done

if [[ "$DO_UNINSTALL" == 1 ]]; then uninstall; exit 0; fi
if [[ "$DO_RESTART" == 1 ]]; then restart_proxies || exit $?; exit 0; fi

if [[ -z "$SSH_HOST" ]]; then
  [[ -t 0 ]] || die "не задан сервер (user@host). См. --help"
  read -r -p "SSH-сервер (host или user@host): " ans
  [[ -n "$ans" ]] || die "сервер не указан"
  if [[ "$ans" == *@* ]]; then SSH_USER="${ans%@*}"; SSH_HOST="${ans#*@}"; else SSH_HOST="$ans"; fi
fi

TARGET="$SSH_USER@$SSH_HOST"

# --- окружение --------------------------------------------------------------
[[ "$(uname)" == "Darwin" ]] || die "скрипт рассчитан на macOS."
command -v brew >/dev/null 2>&1 || die "нет Homebrew. Поставь с https://brew.sh и запусти снова."
BREW_PREFIX="$(brew --prefix)"
info "Сервер: $TARGET:$SSH_PORT | SOCKS :$SOCKS_PORT | HTTP :$HTTP_PORT"

# --- 1. зависимости ---------------------------------------------------------
info "Ставлю privoxy и autossh (если ещё нет)..."
brew list privoxy >/dev/null 2>&1 || brew install privoxy
brew list autossh >/dev/null 2>&1 || brew install autossh
ok "Зависимости на месте"

# --- 2. SSH-ключ ------------------------------------------------------------
if [[ -f "$KEY" ]]; then
  ok "Ключ уже есть: $KEY"
else
  info "Генерирую SSH-ключ..."
  ssh-keygen -t ed25519 -f "$KEY" -N "" -C "o3-claude-$(whoami)@$(hostname -s)"
  ok "Ключ создан: $KEY"
fi

# --- 3. авторизация ключа на сервере ---------------------------------------
key_ok() { ssh -i "$KEY" -p "$SSH_PORT" -o BatchMode=yes -o ConnectTimeout=8 \
           -o StrictHostKeyChecking=accept-new "$TARGET" 'true' 2>/dev/null; }

if key_ok; then
  ok "Вход по ключу уже работает"
else
  warn "Ключ ещё не авторизован на сервере."
  if [[ -t 0 ]]; then
    read -r -p "Скопировать ключ на $TARGET сейчас? (нужен пароль от сервера) [Y/n]: " a
    if [[ ! "$a" =~ ^[Nn] ]]; then
      ssh-copy-id -i "$KEY.pub" -p "$SSH_PORT" "$TARGET" || warn "ssh-copy-id не удался"
    fi
  fi
  if ! key_ok; then
    echo
    echo "============================================================"
    printf "\033[1m  ДОБАВЬ ЭТОТ ПУБЛИЧНЫЙ КЛЮЧ НА СЕРВЕР (в ~/.ssh/authorized_keys юзера %s):\033[0m\n" "$SSH_USER"
    echo "============================================================"
    echo "restrict,port-forwarding $(cat "$KEY.pub")"
    echo "============================================================"
    echo "  (префикс restrict,port-forwarding = только туннель, без шелла)"
    echo "  Как только ключ добавят — туннель поднимется сам (~минута)."
    echo
  fi
fi

# --- 4. Privoxy (SOCKS5 -> HTTP) -------------------------------------------
CFG="$BREW_PREFIX/etc/privoxy/config"
info "Настраиваю Privoxy: $CFG"
if grep -q '# >>> o3-claude >>>' "$CFG"; then
  sed -i '' '/# >>> o3-claude >>>/,/# <<< o3-claude <<</d' "$CFG"
fi
sed -i '' -E 's/^([[:space:]]*socket-timeout[[:space:]]+.*)$/# \1/' "$CFG"
LISTEN_LINE=""
if ! grep -qE "^[[:space:]]*listen-address[[:space:]]+127\.0\.0\.1:$HTTP_PORT([[:space:]]|\$)" "$CFG"; then
  LISTEN_LINE="listen-address 127.0.0.1:$HTTP_PORT"
fi
cat >> "$CFG" <<EOF

# >>> o3-claude >>>
$LISTEN_LINE
socket-timeout 900
forward-socks5t / 127.0.0.1:$SOCKS_PORT .
# <<< o3-claude <<<
EOF
brew services restart privoxy >/dev/null
ok "Privoxy настроен и запущен (:$HTTP_PORT)"

# --- 5. автозапуск туннеля (launchd) ---------------------------------------
PLIST="$HOME/Library/LaunchAgents/com.user.o3-claude.plist"
info "Настраиваю автозапуск туннеля..."
launchctl unload "$PLIST" 2>/dev/null || true
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.user.o3-claude</string>
  <key>ProgramArguments</key>
  <array>
    <string>$BREW_PREFIX/bin/autossh</string>
    <string>-M</string><string>0</string>
    <string>-N</string>
    <string>-D</string><string>$SOCKS_PORT</string>
    <string>-i</string><string>$KEY</string>
    <string>-p</string><string>$SSH_PORT</string>
    <string>-o</string><string>ServerAliveInterval=15</string>
    <string>-o</string><string>ServerAliveCountMax=2</string>
    <string>-o</string><string>ExitOnForwardFailure=yes</string>
    <string>-o</string><string>StrictHostKeyChecking=accept-new</string>
    <string>$TARGET</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict><key>AUTOSSH_GATETIME</key><string>0</string></dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardErrorPath</key><string>/tmp/o3-claude.err</string>
  <key>StandardOutPath</key><string>/tmp/o3-claude.out</string>
</dict>
</plist>
EOF
launchctl load "$PLIST"
ok "Туннель под автозапуском (сам переподнимется при обрыве/смене сети)"

# --- 6. хелсчек ------------------------------------------------------------
BIN="$HOME/.local/bin"; mkdir -p "$BIN"

cat > "$BIN/o3-claude-health" <<'HEALTHEOF'
#!/bin/bash
SOCKS_PORT="${SOCKS_PORT:-__SOCKS__}"; HTTP_PORT="${HTTP_PORT:-__HTTP__}"
g(){ printf "\033[32m%s\033[0m" "$1"; }; r(){ printf "\033[31m%s\033[0m" "$1"; }
echo "=== Проверка o3-claude ==="
launchctl list 2>/dev/null | grep -q o3-claude && echo "autossh (launchd) : $(g OK)" || echo "autossh (launchd) : $(r FAIL)"
lsof -nP -iTCP:$SOCKS_PORT -sTCP:LISTEN >/dev/null 2>&1 && echo "туннель :$SOCKS_PORT     : $(g OK)" || echo "туннель :$SOCKS_PORT     : $(r FAIL — ключ авторизован на сервере?)"
lsof -nP -iTCP:$HTTP_PORT -sTCP:LISTEN >/dev/null 2>&1 && echo "privoxy :$HTTP_PORT     : $(g OK)" || echo "privoxy :$HTTP_PORT     : $(r FAIL)"
DIRECT=$(curl -s --max-time 8 https://api.ipify.org)
SOCKS_IP=$(curl -s --max-time 8 --socks5-hostname 127.0.0.1:$SOCKS_PORT https://api.ipify.org)
HTTP_IP=$(HTTPS_PROXY="http://127.0.0.1:$HTTP_PORT" curl -s --max-time 8 https://api.ipify.org)
echo "прямой IP         : ${DIRECT:-нет ответа}"
{ [ -n "$SOCKS_IP" ] && [ "$SOCKS_IP" != "$DIRECT" ]; } && echo "SOCKS egress      : $(g OK) ($SOCKS_IP)" || echo "SOCKS egress      : $(r FAIL) (${SOCKS_IP:-пусто})"
{ [ -n "$HTTP_IP" ] && [ "$HTTP_IP" = "$SOCKS_IP" ]; } && echo "Privoxy egress    : $(g OK) ($HTTP_IP)" || echo "Privoxy egress    : $(r FAIL) (${HTTP_IP:-пусто})"
echo "========================="
{ [ -n "$SOCKS_IP" ] && [ "$SOCKS_IP" != "$DIRECT" ] && [ "$HTTP_IP" = "$SOCKS_IP" ]; } \
  && { g "Цепочка работает."; echo; } || { r "Ещё не готово — смотри FAIL и /tmp/o3-claude.err"; echo; }
HEALTHEOF
sed -i '' "s/__SOCKS__/$SOCKS_PORT/; s/__HTTP__/$HTTP_PORT/" "$BIN/o3-claude-health"
chmod +x "$BIN/o3-claude-health"
ok "Команда o3-claude-health установлена"

# --- 7. PATH + алиас -------------------------------------------------------
ALIAS="o3-claude"

ZRC="$HOME/.zshrc"; touch "$ZRC"
if grep -q '# >>> o3-claude alias >>>' "$ZRC"; then
  sed -i '' '/# >>> o3-claude alias >>>/,/# <<< o3-claude alias <<</d' "$ZRC"
fi
cat >> "$ZRC" <<EOF

# >>> o3-claude alias >>>
export PATH="\$HOME/.local/bin:\$PATH"
alias $ALIAS='HTTPS_PROXY="http://127.0.0.1:$HTTP_PORT" HTTP_PROXY="http://127.0.0.1:$HTTP_PORT" NO_PROXY="$NO_PROXY_VAL" claude'
# <<< o3-claude alias <<<
EOF
ok "Алиас '$ALIAS' добавлен в ~/.zshrc"

# --- 8. финал: проверка + claude -------------------------------------------
sleep 4
echo; "$BIN/o3-claude-health" || true; echo

CLAUDE_INSTALL="ALL_PROXY=\"socks5h://127.0.0.1:$SOCKS_PORT\" curl -fsSL https://claude.ai/install.sh | bash"
if command -v claude >/dev/null 2>&1; then
  ok "Claude Code уже установлен"
elif curl -s --max-time 6 --socks5-hostname "127.0.0.1:$SOCKS_PORT" https://api.ipify.org >/dev/null 2>&1; then
  info "Ставлю Claude Code через туннель..."
  ALL_PROXY="socks5h://127.0.0.1:$SOCKS_PORT" curl -fsSL https://claude.ai/install.sh | bash \
    || warn "не удалось сейчас — поставь позже: $CLAUDE_INSTALL"
else
  warn "Туннель ещё не поднят — Claude Code поставишь позже: $CLAUDE_INSTALL"
fi

echo
ok "Готово."
echo "  Проверка цепочки:  o3-claude-health"
echo "  Запуск:            $ALIAS   (новый терминал или: source ~/.zshrc)"