feat(blocking): определять и показывать хостер в матрице блокировок
Docker / build (push) Failing after 26s
Docker / build (push) Failing after 26s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
VT_API_URL="${VT_API_URL:-__VT_API_URL__}"
|
||||
VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}"
|
||||
LAUNCHER_VERSION="4"
|
||||
LAUNCHER_VERSION="5"
|
||||
VENDOR_SHA="12c5839"
|
||||
|
||||
OS_ID="unknown"
|
||||
@@ -165,6 +165,88 @@ detect_public_ip() {
|
||||
printf '%s' "$ip"
|
||||
}
|
||||
|
||||
# Короткий таймаут: на обычном VPS link-local просто не ответит.
|
||||
curl_meta() {
|
||||
curl -fsS --connect-timeout 1 --max-time 1 "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
detect_cloud_hoster() {
|
||||
local body=""
|
||||
body="$(curl_meta http://169.254.169.254/hetzner/v1/metadata)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'Hetzner'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta http://169.254.169.254/metadata/v1/id)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'DigitalOcean'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta http://169.254.169.254/v1/instanceid)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'Vultr'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta http://169.254.169.254/linode/v1/instance-id)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'Linode'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/id)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'Google Cloud'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta -H 'Metadata: true' 'http://169.254.169.254/metadata/instance?api-version=2021-02-01')"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'Azure'
|
||||
return 0
|
||||
fi
|
||||
body="$(curl_meta http://169.254.169.254/latest/meta-data/instance-id)"
|
||||
if [ -n "$body" ]; then
|
||||
printf 'AWS'
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_asn_org() {
|
||||
local ip="$1" json="" org=""
|
||||
json="$(curl -fsS --connect-timeout 4 --max-time 8 "https://ipwho.is/${ip}" 2>/dev/null || true)"
|
||||
if [ -n "$json" ]; then
|
||||
org="$(printf '%s' "$json" | jq -r '.connection.org // .org // empty' 2>/dev/null || true)"
|
||||
if [ -n "$org" ] && [ "$org" != "null" ]; then
|
||||
printf '%s' "$org"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
json="$(curl -fsS --connect-timeout 4 --max-time 8 "https://ipinfo.io/${ip}/json" 2>/dev/null || true)"
|
||||
if [ -n "$json" ]; then
|
||||
org="$(printf '%s' "$json" | jq -r '.org // empty' 2>/dev/null || true)"
|
||||
if [ -n "$org" ] && [ "$org" != "null" ]; then
|
||||
printf '%s' "$org"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_ptr_hint() {
|
||||
local ip="$1" ptr=""
|
||||
command -v dig >/dev/null 2>&1 || return 1
|
||||
ptr="$(dig +short -x "$ip" 2>/dev/null | awk 'NF{print; exit}' | tr -d '\r' | sed 's/\.$//')"
|
||||
[ -n "$ptr" ] || return 1
|
||||
printf '%s' "$ptr"
|
||||
}
|
||||
|
||||
detect_hoster() {
|
||||
local ip="$1" value=""
|
||||
value="$(detect_cloud_hoster)" && { printf '%s' "$value"; return 0; }
|
||||
value="$(detect_asn_org "$ip")" && { printf '%s' "$value"; return 0; }
|
||||
value="$(detect_ptr_hint "$ip")" && { printf '%s' "$value"; return 0; }
|
||||
return 1
|
||||
}
|
||||
|
||||
write_vendor() {
|
||||
local dest="$1"
|
||||
if [ -n "${CENSORCHECK_VENDOR_B64:-}" ]; then
|
||||
@@ -203,11 +285,14 @@ chmod +x "$VENDOR"
|
||||
PUBLIC_IP="$(detect_public_ip)"
|
||||
[ -n "$PUBLIC_IP" ] || die "Не удалось определить публичный IP"
|
||||
|
||||
HOSTER="$(detect_hoster "$PUBLIC_IP" || true)"
|
||||
|
||||
RUN_ID="$(uuid4)"
|
||||
[ -n "$RUN_ID" ] || die "Не удалось сгенерировать runId"
|
||||
|
||||
log "censorcheck launcher ${LAUNCHER_VERSION} (vendor ${VENDOR_SHA})"
|
||||
log "probe IP: ${PUBLIC_IP}"
|
||||
log "хостер: ${HOSTER:-не определён}"
|
||||
log "runId: ${RUN_ID}"
|
||||
log "Проверяю сайты (последовательно, несколько минут)..."
|
||||
|
||||
@@ -222,11 +307,11 @@ if [ "$CC_EXIT" -ne 0 ]; then
|
||||
fi
|
||||
|
||||
PAYLOAD="$TMPDIR/payload.json"
|
||||
printf '%s' "$RAW_JSON" | jq --arg runId "$RUN_ID" --arg ip "$PUBLIC_IP" --arg lv "$LAUNCHER_VERSION" '
|
||||
printf '%s' "$RAW_JSON" | jq --arg runId "$RUN_ID" --arg ip "$PUBLIC_IP" --arg lv "$LAUNCHER_VERSION" --arg hoster "$HOSTER" '
|
||||
{
|
||||
schemaVersion: 1,
|
||||
runId: $runId,
|
||||
probe: { publicIp: $ip },
|
||||
probe: ({ publicIp: $ip } + if ($hoster | length) > 0 then { hoster: $hoster } else {} end),
|
||||
launcherVersion: $lv,
|
||||
censorcheck: {
|
||||
version: ((.version | tostring) // "1"),
|
||||
|
||||
@@ -161,4 +161,44 @@ describe('censorcheck ingest + reads', () => {
|
||||
expect(detail.statusCode).toBe(200)
|
||||
expect(detail.json().results).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('сохраняет хостер из probe и канонизирует ASN', async () => {
|
||||
const res = await post(
|
||||
ingestPayload({
|
||||
runId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
|
||||
probe: { publicIp: '203.0.113.10', hoster: 'AS14061 DigitalOcean, LLC' },
|
||||
}),
|
||||
)
|
||||
expect(res.statusCode).toBe(200)
|
||||
const current = await app.inject({ method: 'GET', url: '/api/censorcheck/current' })
|
||||
expect(current.json().items[0].detectedHoster).toBe('DigitalOcean')
|
||||
expect(current.json().items[0].vps).toBeNull()
|
||||
})
|
||||
|
||||
it('для matched VPS отдаёт имя хостера из инвентаря', async () => {
|
||||
runWithSpace(MAIN_SPACE_ID, () =>
|
||||
vpsRepository.create({
|
||||
ip: '203.0.113.10',
|
||||
dns: 'edge.example.com',
|
||||
providerId: 'p1',
|
||||
providerAccountId: 'a1',
|
||||
status: 'active',
|
||||
tariffType: 'monthly',
|
||||
currency: 'RUB',
|
||||
vcpu: 2,
|
||||
ramGb: 4,
|
||||
diskGb: 40,
|
||||
}),
|
||||
)
|
||||
await post(
|
||||
ingestPayload({
|
||||
runId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
|
||||
probe: { publicIp: '203.0.113.10', hoster: 'Hetzner Online GmbH' },
|
||||
}),
|
||||
)
|
||||
const current = await app.inject({ method: 'GET', url: '/api/censorcheck/current' })
|
||||
const item = current.json().items[0]
|
||||
expect(item.vps.providerName).toBe('Test Host')
|
||||
expect(item.detectedHoster).toBe('Hetzner')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import { censorcheckIngestBodySchema } from '@cfdm/shared/contracts/censorcheck'
|
||||
import { canonicalizeHoster, censorcheckIngestBodySchema } from '@cfdm/shared/contracts/censorcheck'
|
||||
import { censorcheckRepository } from '@cfdm/db/repositories/censorcheck'
|
||||
import { actorFromRequest } from '../lib/audit-actor.js'
|
||||
import {
|
||||
@@ -82,6 +82,7 @@ export const censorcheckRoutes: FastifyPluginAsync = async (app) => {
|
||||
censorcheckVersion: parsed.data.censorcheck?.version ?? null,
|
||||
summary,
|
||||
observedSourceIp: observed ?? null,
|
||||
detectedHoster: canonicalizeHoster(parsed.data.probe.hoster),
|
||||
results,
|
||||
})
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ describe('GET /cc launcher', () => {
|
||||
expect(res.body).toContain('VT_INGEST_TOKEN')
|
||||
expect(res.body).toContain('ensure_cmds jq dig column')
|
||||
expect(res.body).toContain('detect_os')
|
||||
expect(res.body).toContain('detect_hoster')
|
||||
expect(res.body).toContain('ipwho.is')
|
||||
expect(res.body).toContain('/etc/os-release')
|
||||
expect(res.body).toContain('apt-get install -y -qq')
|
||||
expect(res.body).toContain('Проверяю сайты')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canonicalizeHoster } from '@cfdm/shared/contracts/censorcheck'
|
||||
|
||||
describe('canonicalizeHoster', () => {
|
||||
it('мапит ASN/org на короткое имя', () => {
|
||||
expect(canonicalizeHoster('AS14061 DigitalOcean, LLC')).toBe('DigitalOcean')
|
||||
expect(canonicalizeHoster('Hetzner Online GmbH')).toBe('Hetzner')
|
||||
expect(canonicalizeHoster('hosted-by.vdsina.ru')).toBe('VDSina')
|
||||
expect(canonicalizeHoster(' aeza.net. ')).toBe('Aeza')
|
||||
})
|
||||
|
||||
it('пустую строку отбрасывает', () => {
|
||||
expect(canonicalizeHoster('')).toBeNull()
|
||||
expect(canonicalizeHoster(' ')).toBeNull()
|
||||
expect(canonicalizeHoster(undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('неизвестную org чистит без alias', () => {
|
||||
expect(canonicalizeHoster('AS12345 Example Hosting LLC')).toBe('Example Hosting')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Filter } from '@/components/reui/filters'
|
||||
import { filterCensorcheckRuns, groupRunsByService, collectServiceColumns, collectProbeColumns, shortHostLabel } from './blocking-filters'
|
||||
import type { CensorcheckRunDto } from './types'
|
||||
import { runHosterLabel, type CensorcheckRunDto } from './types'
|
||||
|
||||
const run = (overrides: Partial<CensorcheckRunDto> = {}): CensorcheckRunDto => ({
|
||||
id: 'ccrun-1',
|
||||
@@ -88,6 +88,37 @@ describe('filterCensorcheckRuns', () => {
|
||||
]),
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('фильтрует по хостеру из прогона, если инвентарь пуст', () => {
|
||||
const unmatched = run({
|
||||
matchedVpsId: null,
|
||||
vps: null,
|
||||
detectedHoster: 'DigitalOcean',
|
||||
})
|
||||
expect(
|
||||
filterCensorcheckRuns([unmatched], [
|
||||
{ id: '1', field: 'hoster', operator: 'contains', values: ['digital'] },
|
||||
]),
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
filterCensorcheckRuns([unmatched], [
|
||||
{ id: '1', field: 'hoster', operator: 'contains', values: ['hetzner'] },
|
||||
]),
|
||||
).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('runHosterLabel', () => {
|
||||
it('предпочитает имя из инвентаря', () => {
|
||||
expect(runHosterLabel(run())).toBe('Hoster')
|
||||
expect(runHosterLabel(run({ detectedHoster: 'Hetzner' }))).toBe('Hoster')
|
||||
})
|
||||
|
||||
it('берёт detectedHoster если VPS не сматчен', () => {
|
||||
expect(runHosterLabel(run({ matchedVpsId: null, vps: null, detectedHoster: 'Hetzner' }))).toBe(
|
||||
'Hetzner',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('groupRunsByService', () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ export function filterCensorcheckRuns(
|
||||
continue
|
||||
}
|
||||
if (filter.field === 'hoster') {
|
||||
const name = (run.vps?.providerName ?? '').toLowerCase()
|
||||
const name = `${run.vps?.providerName ?? ''} ${run.detectedHoster ?? ''}`.toLowerCase()
|
||||
const hit = values.some((value) => name.includes(value.toLowerCase()) || name === value.toLowerCase())
|
||||
if (!hit) return false
|
||||
continue
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from './blocking-filters'
|
||||
import { resolveServiceIcon, ServiceGlyph } from './service-icons'
|
||||
import { StatusMatrixCell } from './status-matrix-cell'
|
||||
import type { CensorcheckRunDto } from './types'
|
||||
import { runHosterLabel, type CensorcheckRunDto } from './types'
|
||||
|
||||
/** DNA data-grid-base-4: auto width + H-scroll + pin start. Preview: https://reui.io/preview/base/data-grid-base-4 */
|
||||
export const BLOCKING_MATRIX_GRID = {
|
||||
@@ -31,12 +31,14 @@ function vpsIdentityColumn(): DataGridColumn<CensorcheckRunDto> {
|
||||
icon: ServerIcon,
|
||||
enableHiding: false,
|
||||
enablePinning: true,
|
||||
size: 220,
|
||||
minSize: 180,
|
||||
size: 240,
|
||||
minSize: 200,
|
||||
sortValue: (row) => row.vps?.dns || row.probePublicIp,
|
||||
cell: (row) => {
|
||||
const title = row.vps?.dns || row.probePublicIp
|
||||
const ip = row.probePublicIp
|
||||
const hoster = runHosterLabel(row)
|
||||
const secondary = hoster ? `${ip} · ${hoster}` : ip
|
||||
const link = row.matchedVpsId ? (
|
||||
<Link
|
||||
to="/vps/$vpsId"
|
||||
@@ -49,7 +51,7 @@ function vpsIdentityColumn(): DataGridColumn<CensorcheckRunDto> {
|
||||
) : (
|
||||
<span className="font-medium">Unknown VPS</span>
|
||||
)
|
||||
return dataGridCellStack(link, ip)
|
||||
return dataGridCellStack(link, secondary)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { GlobeIcon, MapPinIcon, ServerIcon, ShieldAlertIcon } from 'lucide-react'
|
||||
import { Building2Icon, GlobeIcon, MapPinIcon, ServerIcon, ShieldAlertIcon } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CENSORCHECK_STATUS_LABELS,
|
||||
formatCheckedAt,
|
||||
formatVpsResources,
|
||||
runHosterLabel,
|
||||
type CensorcheckRunDto,
|
||||
} from './types'
|
||||
|
||||
@@ -67,6 +68,12 @@ export function CheckRunSheet({ run, open, onOpenChange }: CheckRunSheetProps) {
|
||||
</Link>
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
id: 'hoster',
|
||||
icon: <Building2Icon />,
|
||||
label: 'Хостер',
|
||||
description: runHosterLabel(detail) || '—',
|
||||
},
|
||||
{
|
||||
id: 'geo',
|
||||
icon: <MapPinIcon />,
|
||||
|
||||
@@ -40,6 +40,7 @@ export type CensorcheckRunDto = {
|
||||
createdAt: string
|
||||
completedAt: string
|
||||
observedSourceIp: string | null
|
||||
detectedHoster?: string | null
|
||||
vps: CensorcheckVpsInfo | null
|
||||
results?: CensorcheckResultDto[]
|
||||
}
|
||||
@@ -67,12 +68,19 @@ export function formatCheckedAt(iso: string): string {
|
||||
return date.toLocaleString('ru-RU')
|
||||
}
|
||||
|
||||
export function runHosterLabel(run: CensorcheckRunDto): string {
|
||||
const inventory = run.vps?.providerName?.trim() ?? ''
|
||||
if (inventory) return inventory
|
||||
return run.detectedHoster?.trim() ?? ''
|
||||
}
|
||||
|
||||
export function runSearchText(run: CensorcheckRunDto): string {
|
||||
const parts = [
|
||||
run.probePublicIp,
|
||||
run.claimedPublicIp ?? '',
|
||||
run.vps?.dns ?? '',
|
||||
run.vps?.providerName ?? '',
|
||||
run.detectedHoster ?? '',
|
||||
run.vps?.country ?? '',
|
||||
...(run.results ?? []).map((row) => `${row.serviceKey} ${row.serviceLabel}`),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user