diff --git a/AGENTS.md b/AGENTS.md index d23ed89..4885611 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,9 +93,10 @@ vps-tracker/ ## Статус блокировок (censorcheck) Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/cc | bash` (тот же контейнер, Traefik dual Host). +Раз в сутки: `curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily` (cron, свежий HMAC-токен на каждый запуск). - **Vendor:** `apps/api/scripts/censorcheck/censorcheck.sh` (pin SHA `12c5839`, MIT) -- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; прогресс-бар в stderr; `GET /cc/vendor` — скрипт (LF) +- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; прогресс-бар в stderr; `GET /cc/vendor` — скрипт (LF); `--daily` / `--remove-daily` - **Ingest:** `POST /api/integrations/censorcheck/runs` (без portal JWT) - **UI:** `/blocking` — текущие прогоны и история, группировка VPS / сервис - Env: `CENSORCHECK_INGEST_SECRET`, `CENSORCHECK_PUBLIC_URL`, `VPS_LAUNCHER_DOMAIN` @@ -103,9 +104,10 @@ vps-tracker/ ## GeoIP (ipregion) Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/ic | bash`. +Раз в сутки: `curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily`. - **Vendor:** `apps/api/scripts/ipregion/ipregion.sh` (pin SHA `7d1c25c`, MIT, [vernette/ipregion](https://github.com/vernette/ipregion)) -- **Launcher:** `GET /ic` минтит HMAC ingest-токен (тот же `CENSORCHECK_INGEST_SECRET`); `GET /ic/vendor` — pinned скрипт (LF) +- **Launcher:** `GET /ic` минтит HMAC ingest-токен (тот же `CENSORCHECK_INGEST_SECRET`); `GET /ic/vendor` — pinned скрипт (LF); `--daily` / `--remove-daily` - **Ingest:** `POST /api/integrations/ipregion/runs` (без portal JWT) - **UI:** `/geo` — матрица ISO-стран VPS × сервисы (primary / custom / cdn) diff --git a/apps/api/scripts/censorcheck/launcher.sh b/apps/api/scripts/censorcheck/launcher.sh index 93bb224..76f05ed 100644 --- a/apps/api/scripts/censorcheck/launcher.sh +++ b/apps/api/scripts/censorcheck/launcher.sh @@ -5,8 +5,10 @@ set -euo pipefail VT_API_URL="${VT_API_URL:-__VT_API_URL__}" VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}" -LAUNCHER_VERSION="5" +LAUNCHER_VERSION="6" VENDOR_SHA="12c5839" +DAILY_SLUG="vt-censorcheck" +DAILY_PATH="/cc" OS_ID="unknown" OS_LIKE="" @@ -247,6 +249,102 @@ detect_hoster() { return 1 } +show_launcher_help() { + cat </dev/null 2>&1; then + local current + current="$(crontab -l 2>/dev/null || true)" + if printf '%s\n' "$current" | grep -qF "vps-tracker:${DAILY_SLUG}"; then + printf '%s\n' "$current" | grep -vF "vps-tracker:${DAILY_SLUG}" | crontab - + removed=1 + fi + fi + if [ "$removed" -eq 1 ]; then + log "Ежедневная проверка снята (${DAILY_SLUG})" + else + log "Ежедневная проверка не была установлена" + fi +} + +install_daily() { + daily_slot + local run line prefix="" + local marker="# vps-tracker:${DAILY_SLUG}" + run="$(daily_run_cmd)" + + if [ -d /etc/cron.d ] && prefix="$(root_prefix)"; then + local tmp + tmp="$(mktemp)" + cat >"$tmp" <>/var/log/${DAILY_SLUG}.log 2>&1 +EOF + # shellcheck disable=SC2086 + $prefix cp "$tmp" "/etc/cron.d/${DAILY_SLUG}" + # shellcheck disable=SC2086 + $prefix chmod 644 "/etc/cron.d/${DAILY_SLUG}" + rm -f "$tmp" + log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (/etc/cron.d/${DAILY_SLUG})" + log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily" + return 0 + fi + + if ! command -v crontab >/dev/null 2>&1; then + die "Нет cron (ни /etc/cron.d, ни crontab). Установите пакет cron и повторите --daily" + fi + line="${DAILY_MINUTE} ${DAILY_HOUR} * * * ${run} >>/tmp/${DAILY_SLUG}.log 2>&1 ${marker}" + { + (crontab -l 2>/dev/null || true) | grep -vF "vps-tracker:${DAILY_SLUG}" || true + printf '%s\n' "$line" + } | crontab - + log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (crontab)" + log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily" +} + write_vendor() { local dest="$1" if [ -n "${CENSORCHECK_VENDOR_B64:-}" ]; then @@ -273,6 +371,22 @@ VT_API_URL="${VT_API_URL%/}" [ -n "$VT_API_URL" ] || die "VT_API_URL пуст" [ -n "$VT_INGEST_TOKEN" ] || die "VT_INGEST_TOKEN пуст" +DAILY_INSTALL=0 +DAILY_REMOVE=0 +for arg in "$@"; do + case "$arg" in + --daily|--install-daily) DAILY_INSTALL=1 ;; + --remove-daily|--uninstall-daily) DAILY_REMOVE=1 ;; + -h|--help) show_launcher_help; exit 0 ;; + *) die "Неизвестный аргумент: $arg. См. --help" ;; + esac +done + +if [ "$DAILY_REMOVE" -eq 1 ]; then + remove_daily + exit 0 +fi + TMPDIR="$(mktemp -d /tmp/vt-censorcheck.XXXXXX)" cleanup() { rm -rf "$TMPDIR"; } trap 'cleanup; exit 130' INT @@ -336,6 +450,9 @@ set -e if [ "$POST_EXIT" -ne 0 ]; then cp "$PAYLOAD" "$FALLBACK" log "API недоступен (curl exit ${POST_EXIT}). JSON сохранён: ${FALLBACK}" >&2 + if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily + fi exit 2 fi @@ -345,9 +462,15 @@ if [ -z "$CHECK_ID" ]; then cp "$PAYLOAD" "$FALLBACK" log "Некорректный ответ API. JSON сохранён: ${FALLBACK}" >&2 log "$RESP" >&2 + if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily + fi exit 2 fi log "Check ID: ${CHECK_ID}" log "VPS: ${MATCHED}" +if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily +fi exit 0 diff --git a/apps/api/scripts/ipregion/launcher.sh b/apps/api/scripts/ipregion/launcher.sh index 84ade31..c672647 100644 --- a/apps/api/scripts/ipregion/launcher.sh +++ b/apps/api/scripts/ipregion/launcher.sh @@ -5,8 +5,10 @@ set -euo pipefail VT_API_URL="${VT_API_URL:-__VT_API_URL__}" VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}" -LAUNCHER_VERSION="1" +LAUNCHER_VERSION="2" VENDOR_SHA="7d1c25c" +DAILY_SLUG="vt-ipregion" +DAILY_PATH="/ic" OS_ID="unknown" OS_LIKE="" @@ -247,6 +249,102 @@ detect_hoster() { return 1 } +show_launcher_help() { + cat </dev/null 2>&1; then + local current + current="$(crontab -l 2>/dev/null || true)" + if printf '%s\n' "$current" | grep -qF "vps-tracker:${DAILY_SLUG}"; then + printf '%s\n' "$current" | grep -vF "vps-tracker:${DAILY_SLUG}" | crontab - + removed=1 + fi + fi + if [ "$removed" -eq 1 ]; then + log "Ежедневная проверка снята (${DAILY_SLUG})" + else + log "Ежедневная проверка не была установлена" + fi +} + +install_daily() { + daily_slot + local run line prefix="" + local marker="# vps-tracker:${DAILY_SLUG}" + run="$(daily_run_cmd)" + + if [ -d /etc/cron.d ] && prefix="$(root_prefix)"; then + local tmp + tmp="$(mktemp)" + cat >"$tmp" <>/var/log/${DAILY_SLUG}.log 2>&1 +EOF + # shellcheck disable=SC2086 + $prefix cp "$tmp" "/etc/cron.d/${DAILY_SLUG}" + # shellcheck disable=SC2086 + $prefix chmod 644 "/etc/cron.d/${DAILY_SLUG}" + rm -f "$tmp" + log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (/etc/cron.d/${DAILY_SLUG})" + log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily" + return 0 + fi + + if ! command -v crontab >/dev/null 2>&1; then + die "Нет cron (ни /etc/cron.d, ни crontab). Установите пакет cron и повторите --daily" + fi + line="${DAILY_MINUTE} ${DAILY_HOUR} * * * ${run} >>/tmp/${DAILY_SLUG}.log 2>&1 ${marker}" + { + (crontab -l 2>/dev/null || true) | grep -vF "vps-tracker:${DAILY_SLUG}" || true + printf '%s\n' "$line" + } | crontab - + log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (crontab)" + log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily" +} + write_vendor() { local dest="$1" if [ -n "${IPREGION_VENDOR_B64:-}" ]; then @@ -273,6 +371,22 @@ VT_API_URL="${VT_API_URL%/}" [ -n "$VT_API_URL" ] || die "VT_API_URL пуст" [ -n "$VT_INGEST_TOKEN" ] || die "VT_INGEST_TOKEN пуст" +DAILY_INSTALL=0 +DAILY_REMOVE=0 +for arg in "$@"; do + case "$arg" in + --daily|--install-daily) DAILY_INSTALL=1 ;; + --remove-daily|--uninstall-daily) DAILY_REMOVE=1 ;; + -h|--help) show_launcher_help; exit 0 ;; + *) die "Неизвестный аргумент: $arg. См. --help" ;; + esac +done + +if [ "$DAILY_REMOVE" -eq 1 ]; then + remove_daily + exit 0 +fi + TMPDIR="$(mktemp -d /tmp/vt-ipregion.XXXXXX)" cleanup() { rm -rf "$TMPDIR"; } trap 'cleanup; exit 130' INT @@ -339,6 +453,9 @@ set -e if [ "$POST_EXIT" -ne 0 ]; then cp "$PAYLOAD" "$FALLBACK" log "API недоступен (curl exit ${POST_EXIT}). JSON сохранён: ${FALLBACK}" >&2 + if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily + fi exit 2 fi @@ -348,9 +465,15 @@ if [ -z "$CHECK_ID" ]; then cp "$PAYLOAD" "$FALLBACK" log "Некорректный ответ API. JSON сохранён: ${FALLBACK}" >&2 log "$RESP" >&2 + if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily + fi exit 2 fi log "Check ID: ${CHECK_ID}" log "VPS: ${MATCHED}" +if [ "$DAILY_INSTALL" -eq 1 ]; then + install_daily +fi exit 0 diff --git a/apps/api/src/routes/launcher.test.ts b/apps/api/src/routes/launcher.test.ts index 43c9148..78fc75e 100644 --- a/apps/api/src/routes/launcher.test.ts +++ b/apps/api/src/routes/launcher.test.ts @@ -33,6 +33,11 @@ describe('GET /cc launcher', () => { expect(res.body).toContain('/etc/os-release') expect(res.body).toContain('apt-get install -y -qq') expect(res.body).toContain('Проверяю сайты') + expect(res.body).toContain('--daily') + expect(res.body).toContain('--remove-daily') + expect(res.body).toContain('DAILY_SLUG="vt-censorcheck"') + expect(res.body).toContain('vps-tracker:${DAILY_SLUG}') + expect(res.body).toContain('/etc/cron.d') expect(res.body).not.toContain('2>/tmp/vt-censorcheck-err') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') @@ -80,6 +85,10 @@ describe('GET /ic launcher', () => { expect(res.body).toContain('/api/integrations/ipregion/runs') expect(res.body).toContain('/ic/vendor') expect(res.body).toContain('7d1c25c') + expect(res.body).toContain('--daily') + expect(res.body).toContain('--remove-daily') + expect(res.body).toContain('DAILY_SLUG="vt-ipregion"') + expect(res.body).toContain('vps-tracker:${DAILY_SLUG}') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__') diff --git a/apps/web/src/components/censorcheck/blocking-page.tsx b/apps/web/src/components/censorcheck/blocking-page.tsx index 0575d15..71269e1 100644 --- a/apps/web/src/components/censorcheck/blocking-page.tsx +++ b/apps/web/src/components/censorcheck/blocking-page.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { BanIcon, + CalendarClockIcon, CopyIcon, GlobeIcon, ServerIcon, @@ -38,6 +39,7 @@ import { filterCensorcheckRuns, serviceMatrixRows } from './blocking-filters' import { CENSORCHECK_STATUS_LABELS, LAUNCHER_CMD, + LAUNCHER_CMD_DAILY, formatCheckedAt, type CensorcheckRunDto, } from './types' @@ -143,14 +145,24 @@ export function BlockingPage() { const blocked = filtered.reduce((sum, row) => sum + row.summary.blocked, 0) const copyLauncher = ( - + <> + + + ) return ( diff --git a/apps/web/src/components/censorcheck/types.ts b/apps/web/src/components/censorcheck/types.ts index a64e6fb..e98c16b 100644 --- a/apps/web/src/components/censorcheck/types.ts +++ b/apps/web/src/components/censorcheck/types.ts @@ -57,6 +57,7 @@ export const CENSORCHECK_STATUS_LABELS: Record = { } export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/cc | bash' +export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily' export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string { return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB` diff --git a/apps/web/src/components/ipregion/geo-page.tsx b/apps/web/src/components/ipregion/geo-page.tsx index 951b70d..5348370 100644 --- a/apps/web/src/components/ipregion/geo-page.tsx +++ b/apps/web/src/components/ipregion/geo-page.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { + CalendarClockIcon, CopyIcon, GlobeIcon, MapPinIcon, @@ -43,6 +44,7 @@ import { import { IPREGION_STATUS_LABELS, LAUNCHER_CMD, + LAUNCHER_CMD_DAILY, formatCheckedAt, type IpregionRunDto, } from './types' @@ -148,14 +150,24 @@ export function GeoPage() { const mismatches = filtered.filter(isGeoMismatch).length const copyLauncher = ( - + <> + + + ) return ( diff --git a/apps/web/src/components/ipregion/types.ts b/apps/web/src/components/ipregion/types.ts index ebda039..de0db37 100644 --- a/apps/web/src/components/ipregion/types.ts +++ b/apps/web/src/components/ipregion/types.ts @@ -56,6 +56,7 @@ export const IPREGION_STATUS_LABELS: Record = { } export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/ic | bash' +export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily' export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string { return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB`