feat(blocking): определять и показывать хостер в матрице блокировок
Docker / build (push) Failing after 26s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-25 13:19:02 +07:00
co-authored by Cursor
parent efebcaf16d
commit 60a22b9450
15 changed files with 273 additions and 13 deletions
@@ -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),
)!,
)
}
+3 -1
View File
@@ -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 = [
+1
View File
@@ -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),
+2 -1
View File
@@ -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)
}