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}`),
|
||||
]
|
||||
|
||||
@@ -54,6 +54,7 @@ export type CensorcheckRunDto = {
|
||||
createdAt: string
|
||||
completedAt: string
|
||||
observedSourceIp: string | null
|
||||
detectedHoster: string | null
|
||||
vps: CensorcheckVpsInfo | null
|
||||
results?: CensorcheckResultDto[]
|
||||
}
|
||||
@@ -80,6 +81,7 @@ export type CensorcheckInsertRun = {
|
||||
censorcheckVersion: string | null
|
||||
summary: CensorcheckSummary
|
||||
observedSourceIp: string | null
|
||||
detectedHoster: string | null
|
||||
results: CensorcheckInsertResult[]
|
||||
}
|
||||
|
||||
@@ -175,6 +177,7 @@ function toRunDto(row: RunRow, includeResults = false): CensorcheckRunDto {
|
||||
createdAt: row.createdAt,
|
||||
completedAt: row.completedAt,
|
||||
observedSourceIp: row.observedSourceIp ?? null,
|
||||
detectedHoster: row.detectedHoster ?? null,
|
||||
vps: hydrateVps(row.matchedVpsId ?? null),
|
||||
}
|
||||
if (includeResults) {
|
||||
@@ -248,6 +251,7 @@ export const censorcheckRepository = {
|
||||
createdAt: now,
|
||||
completedAt: now,
|
||||
observedSourceIp: input.observedSourceIp,
|
||||
detectedHoster: input.detectedHoster,
|
||||
})
|
||||
.run()
|
||||
for (const result of input.results) {
|
||||
@@ -304,6 +308,7 @@ export const censorcheckRepository = {
|
||||
like(schema.censorcheckRuns.probePublicIp, pattern),
|
||||
like(schema.censorcheckRuns.claimedPublicIp, pattern),
|
||||
like(schema.censorcheckRuns.runId, pattern),
|
||||
like(schema.censorcheckRuns.detectedHoster, pattern),
|
||||
)!,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -297,7 +297,8 @@ const CORE_TABLE_MIGRATIONS: string[] = [
|
||||
summaryJson TEXT NOT NULL DEFAULT '{}',
|
||||
createdAt TEXT NOT NULL,
|
||||
completedAt TEXT NOT NULL,
|
||||
observedSourceIp TEXT
|
||||
observedSourceIp TEXT,
|
||||
detectedHoster TEXT
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS censorcheck_results (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -366,6 +367,7 @@ const COLUMN_MIGRATIONS: string[] = [
|
||||
`ALTER TABLE sync_log ADD COLUMN summary TEXT`,
|
||||
`ALTER TABLE active_tariffs ADD COLUMN spaceId TEXT`,
|
||||
`ALTER TABLE tariff_sync_options ADD COLUMN spaceId TEXT`,
|
||||
`ALTER TABLE censorcheck_runs ADD COLUMN detectedHoster TEXT`,
|
||||
]
|
||||
|
||||
const SPACE_BACKFILL_TABLES = [
|
||||
|
||||
@@ -384,6 +384,7 @@ export const censorcheckRuns = sqliteTable(
|
||||
createdAt: text('createdAt').notNull(),
|
||||
completedAt: text('completedAt').notNull(),
|
||||
observedSourceIp: text('observedSourceIp'),
|
||||
detectedHoster: text('detectedHoster'),
|
||||
},
|
||||
(t) => ({
|
||||
runIdUniq: uniqueIndex('censorcheck_runs_runId').on(t.runId),
|
||||
|
||||
@@ -299,7 +299,8 @@ CREATE TABLE IF NOT EXISTS censorcheck_runs (
|
||||
summaryJson TEXT NOT NULL DEFAULT '{}',
|
||||
createdAt TEXT NOT NULL,
|
||||
completedAt TEXT NOT NULL,
|
||||
observedSourceIp TEXT
|
||||
observedSourceIp TEXT,
|
||||
detectedHoster TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS censorcheck_results (
|
||||
|
||||
@@ -77,6 +77,7 @@ export const censorcheckIngestBodySchema = z.object({
|
||||
runId: z.string().trim().min(8).max(80),
|
||||
probe: z.object({
|
||||
publicIp: z.string().trim().min(1).max(64),
|
||||
hoster: z.string().trim().max(160).optional(),
|
||||
}),
|
||||
censorcheck: z
|
||||
.object({
|
||||
@@ -112,3 +113,56 @@ export function emptyCensorcheckSummary(): CensorcheckSummary {
|
||||
error: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const HOSTER_ALIASES: ReadonlyArray<{ test: RegExp; name: string }> = [
|
||||
{ test: /digitalocean|digital ocean|\bas14061\b/i, name: 'DigitalOcean' },
|
||||
{ test: /hetzner|\bas24940\b/i, name: 'Hetzner' },
|
||||
{ test: /\bovh|\bas16276\b/i, name: 'OVH' },
|
||||
{ test: /linode|akamai|\bas63949\b/i, name: 'Linode' },
|
||||
{ test: /\bvultr|\bas20473\b/i, name: 'Vultr' },
|
||||
{ test: /amazon|aws|\bec2\b|\bas16509\b|\bas14618\b/i, name: 'AWS' },
|
||||
{ test: /google cloud|\bgcp\b|\bas15169\b/i, name: 'Google Cloud' },
|
||||
{ test: /microsoft|azure|\bas8075\b/i, name: 'Azure' },
|
||||
{ test: /oracle cloud|\bas31898\b/i, name: 'Oracle Cloud' },
|
||||
{ test: /selectel|\bas49505\b/i, name: 'Selectel' },
|
||||
{ test: /timeweb|\bas197695\b/i, name: 'Timeweb' },
|
||||
{ test: /vdsina/i, name: 'VDSina' },
|
||||
{ test: /4vps/i, name: '4VPS' },
|
||||
{ test: /macloud/i, name: 'Macloud' },
|
||||
{ test: /veesp/i, name: 'Veesp' },
|
||||
{ test: /ruvds|ru-vds|ru vds/i, name: 'RUVDS' },
|
||||
{ test: /\baeza\b/i, name: 'Aeza' },
|
||||
{ test: /firstvds|firstdedic/i, name: 'FirstVDS' },
|
||||
{ test: /yandex cloud|\bas200350\b/i, name: 'Yandex Cloud' },
|
||||
{ test: /cloud\.ru|sbercloud|\bas208677\b/i, name: 'Cloud.ru' },
|
||||
{ test: /\bbeget\b/i, name: 'Beget' },
|
||||
{ test: /fornex/i, name: 'Fornex' },
|
||||
{ test: /adminvps/i, name: 'AdminVPS' },
|
||||
{ test: /\bihor\b/i, name: 'Ihor' },
|
||||
{ test: /netcup/i, name: 'Netcup' },
|
||||
{ test: /contabo/i, name: 'Contabo' },
|
||||
{ test: /leaseweb/i, name: 'Leaseweb' },
|
||||
]
|
||||
|
||||
function tidyOrgName(value: string): string {
|
||||
const stripped = value
|
||||
.replace(/^as\d+\s+/i, '')
|
||||
.replace(/\s*[,;]?\s*\b(llc|ltd|inc|gmbh|corp|limited|jsc|ooo|ооо|ао)\b\.?$/i, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (!stripped) return value.slice(0, 80)
|
||||
const words = stripped.split(' ')
|
||||
if (stripped.length > 42 && words.length > 3) return words.slice(0, 3).join(' ')
|
||||
return stripped.slice(0, 80)
|
||||
}
|
||||
|
||||
/** Нормализует ASN/org/PTR/cloud-id в короткое имя хостера. */
|
||||
export function canonicalizeHoster(raw: string | null | undefined): string | null {
|
||||
const value = raw?.replace(/\s+/g, ' ').trim()
|
||||
if (!value) return null
|
||||
const sliced = value.slice(0, 160)
|
||||
for (const alias of HOSTER_ALIASES) {
|
||||
if (alias.test.test(sliced)) return alias.name
|
||||
}
|
||||
return tidyOrgName(sliced)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user