fix(save): ban selected asns and expire synthient blocks
This commit is contained in:
parent
c1dc678629
commit
d11bf1f817
|
|
@ -13,6 +13,7 @@ import redis from '@/utils/redis';
|
||||||
import { parseSlop } from '@/utils/slop';
|
import { parseSlop } from '@/utils/slop';
|
||||||
import { checkIpRanges } from '@/utils/ranges';
|
import { checkIpRanges } from '@/utils/ranges';
|
||||||
import { getRateLimitState, getRateLimitSubjects } from '@/utils/rate-limit';
|
import { getRateLimitState, getRateLimitSubjects } from '@/utils/rate-limit';
|
||||||
|
import { isSynthientBlocked } from '@/utils/synthient'
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
const videoIds: Record<string, string> = {}
|
const videoIds: Record<string, string> = {}
|
||||||
|
|
@ -176,9 +177,12 @@ app.ws('/save', {
|
||||||
await cleanup(ws, videoId);
|
await cleanup(ws, videoId);
|
||||||
console.log(`captcha failed for ${videoId} - ${JSON.stringify(captchaCheck)}`)
|
console.log(`captcha failed for ${videoId} - ${JSON.stringify(captchaCheck)}`)
|
||||||
return sendError(ws, 'Captcha validation failed.');
|
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)
|
const data = await getVideo(videoId)
|
||||||
|
|
@ -272,9 +276,12 @@ app.ws('/savechannel', {
|
||||||
await cleanup(ws, channelId);
|
await cleanup(ws, channelId);
|
||||||
console.log(`captcha failed for ${channelId} - ${JSON.stringify(captchaCheck)}`)
|
console.log(`captcha failed for ${channelId} - ${JSON.stringify(captchaCheck)}`)
|
||||||
return sendError(ws, 'Captcha validation failed.');
|
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}`;
|
videoIds[ws.id] = `downloading-${channelId}`;
|
||||||
const [videos, channelData] = await Promise.all([getChannelVideos(channelId), getChannel(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)) {
|
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;
|
if (!video || (await redis.get(saveKey(video.videoId))) || (await redis.get(`blacklist:${video.videoId}`))) continue;
|
||||||
|
|
||||||
const already = await db.selectFrom('videos')
|
const already = await db.selectFrom('videos')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { readdir, stat } from 'node:fs/promises'
|
import { readdir, stat } from 'node:fs/promises'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import redis from '@/utils/redis'
|
||||||
|
|
||||||
export interface BlockedIpResult {
|
export interface BlockedIpResult {
|
||||||
blocked: boolean
|
blocked: boolean
|
||||||
|
|
@ -26,6 +27,10 @@ const asnBanList: number[] = [
|
||||||
|
|
||||||
14618, // aws
|
14618, // aws
|
||||||
|
|
||||||
|
62327,
|
||||||
|
8075, // microsoft
|
||||||
|
210644, // aeza group
|
||||||
|
|
||||||
// expressvpn
|
// expressvpn
|
||||||
206092,
|
206092,
|
||||||
137409,
|
137409,
|
||||||
|
|
@ -397,7 +402,14 @@ export async function checkIpRanges(ip: string): Promise<BlockedIpResult> {
|
||||||
return { blocked: false, list: null, range: null }
|
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)
|
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 entries = await readdir(BLOCKED_DIR, { withFileTypes: true })
|
||||||
|
|
||||||
const files = entries
|
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(() => {
|
const networkRefreshTimer = setInterval(() => {
|
||||||
void refreshNetworksCache(true)
|
void refreshNetworksCache(true)
|
||||||
}, NETWORKS_REFRESH_INTERVAL_MS)
|
}, NETWORKS_REFRESH_INTERVAL_MS)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue