fix(save): ban selected asns and expire synthient blocks

This commit is contained in:
localhost 2026-10-03 17:49:45 +02:00
parent c1dc678629
commit d11bf1f817
3 changed files with 99 additions and 4 deletions

View File

@ -13,6 +13,7 @@ import redis from '@/utils/redis';
import { parseSlop } from '@/utils/slop';
import { checkIpRanges } from '@/utils/ranges';
import { getRateLimitState, getRateLimitSubjects } from '@/utils/rate-limit';
import { isSynthientBlocked } from '@/utils/synthient'
const app = new Elysia()
const videoIds: Record<string, string> = {}
@ -176,9 +177,12 @@ app.ws('/save', {
await cleanup(ws, videoId);
console.log(`captcha failed for ${videoId} - ${JSON.stringify(captchaCheck)}`)
return sendError(ws, 'Captcha validation failed.');
} else {
ws.send('DATA - Captcha validated. Starting download...');
}
if (await isSynthientBlocked(ws.data.headers['cf-connecting-ip'] || '')) {
await cleanup(ws, videoId)
return sendError(ws, 'Your network is flagged as malicious.')
}
ws.send('DATA - Captcha validated. Starting download...');
}
const data = await getVideo(videoId)
@ -272,9 +276,12 @@ app.ws('/savechannel', {
await cleanup(ws, channelId);
console.log(`captcha failed for ${channelId} - ${JSON.stringify(captchaCheck)}`)
return sendError(ws, 'Captcha validation failed.');
} else {
ws.send('DATA - Captcha validated. Starting download...');
}
if (await isSynthientBlocked(ws.data.headers['cf-connecting-ip'] || '')) {
await cleanup(ws, channelId)
return sendError(ws, 'Your network is flagged as malicious.')
}
ws.send('DATA - Captcha validated. Starting download...');
videoIds[ws.id] = `downloading-${channelId}`;
const [videos, channelData] = await Promise.all([getChannelVideos(channelId), getChannel(channelId)]);
@ -284,6 +291,10 @@ app.ws('/savechannel', {
}
for (const video of videos.slice(0, 5)) {
if ((await checkIpRanges(ws.data.headers['cf-connecting-ip'] || '')).blocked) {
await cleanup(ws, channelId)
return sendError(ws, 'Your network is flagged as malicious.')
}
if (!video || (await redis.get(saveKey(video.videoId))) || (await redis.get(`blacklist:${video.videoId}`))) continue;
const already = await db.selectFrom('videos')

View File

@ -1,5 +1,6 @@
import { readdir, stat } from 'node:fs/promises'
import * as path from 'node:path'
import redis from '@/utils/redis'
export interface BlockedIpResult {
blocked: boolean
@ -26,6 +27,10 @@ const asnBanList: number[] = [
14618, // aws
62327,
8075, // microsoft
210644, // aeza group
// expressvpn
206092,
137409,
@ -397,7 +402,14 @@ export async function checkIpRanges(ip: string): Promise<BlockedIpResult> {
return { blocked: false, list: null, range: null }
}
if (await redis.exists(`synthient:blocked-ip:${ip}`)) {
return { blocked: true, list: 'synthient', range: null }
}
const asnMatch = await resolveIpAsn(parsedIp)
if (asnMatch.range && await redis.exists(`synthient:blocked-range:${asnMatch.range}`)) {
return { blocked: true, list: 'synthient', range: asnMatch.range }
}
const entries = await readdir(BLOCKED_DIR, { withFileTypes: true })
const files = entries
@ -444,6 +456,11 @@ export async function checkIpRanges(ip: string): Promise<BlockedIpResult> {
}
}
export async function getIpNetworkRange(ip: string): Promise<string | null> {
const parsedIp = parseIp(ip)
return parsedIp ? (await resolveIpAsn(parsedIp)).range : null
}
const networkRefreshTimer = setInterval(() => {
void refreshNetworksCache(true)
}, NETWORKS_REFRESH_INTERVAL_MS)

67
src/utils/synthient.ts Normal file
View File

@ -0,0 +1,67 @@
import { isIP } from 'node:net'
import redis from '@/utils/redis'
import { checkIpRanges, getIpNetworkRange } from '@/utils/ranges'
const responseKey = (ip: string) => `synthient:response:${ip}`
const banTtlSeconds = 30 * 24 * 60 * 60
function getBlockDecision(body: string) {
const result = JSON.parse(body) as {
intelligence?: {
categories?: string[]
providers?: { provider?: string, type?: string }[]
risk_score?: number
}
}
if (!result.intelligence || !Array.isArray(result.intelligence.categories) || !Array.isArray(result.intelligence.providers)) {
throw new Error('Invalid IP lookup response')
}
if (result.intelligence.categories.includes('RESIDENTIAL_PROXY')) {
return {
blocked: typeof result.intelligence.risk_score === 'number' && result.intelligence.risk_score > 90,
blockRange: false
}
}
return {
blocked: result.intelligence.categories.includes('FREE_VPN') || result.intelligence.providers.some(provider =>
provider.type === 'FREE_VPN' ||
provider.provider === 'NORDVPN' ||
provider.provider === 'PROTONVPN'),
blockRange: true
}
}
export async function isSynthientBlocked(ip: string): Promise<boolean> {
if (!isIP(ip)) return false
if ((await checkIpRanges(ip)).blocked) return true
try {
const cached = await redis.get(responseKey(ip))
if (cached) return getBlockDecision(cached).blocked
if (!process.env.SYNTHIENT_API_KEY) return false
const response = await fetch(`https://api.synthient.com/api/v4/lookup/ip/${encodeURIComponent(ip)}`, {
headers: { 'x-api-key': process.env.SYNTHIENT_API_KEY },
signal: AbortSignal.timeout(5000)
})
const body = await response.text()
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const decision = getBlockDecision(body)
if (!decision.blocked) {
await redis.set(responseKey(ip), body, 'EX', 24 * 60 * 60)
return false
}
const range = decision.blockRange ? await getIpNetworkRange(ip) : null
const transaction = redis.multi()
transaction.set(responseKey(ip), body, 'EX', banTtlSeconds)
transaction.set(`synthient:blocked-ip:${ip}`, '1', 'EX', banTtlSeconds)
if (range) transaction.set(`synthient:blocked-range:${range}`, '1', 'EX', banTtlSeconds)
await transaction.exec()
return true
} catch {
return false
}
}