bypass captcha keys

This commit is contained in:
localhost 2026-04-29 11:36:21 +02:00
parent 2a833c12b7
commit 2fd502039f
4 changed files with 40 additions and 27 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -11,6 +11,7 @@ services:
- /mnt/hdd/preservetube-videos:/usr/src/preservetube/backend/videos - /mnt/hdd/preservetube-videos:/usr/src/preservetube/backend/videos
- ./.env:/usr/src/preservetube/backend/.env - ./.env:/usr/src/preservetube/backend/.env
- ./s3.json:/usr/src/preservetube/backend/s3.json - ./s3.json:/usr/src/preservetube/backend/s3.json
- ./keys.yaml:/usr/src/preservetube/backend/keys.yaml
networks: networks:
public: public:

View File

@ -17,6 +17,7 @@
"@elysiajs/static": "^1.4.0", "@elysiajs/static": "^1.4.0",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/html-minifier-next": "^2.1.0", "@types/html-minifier-next": "^2.1.0",
"@types/js-yaml": "^4.0.9",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"age-encryption": "^0.2.4", "age-encryption": "^0.2.4",
@ -26,6 +27,7 @@
"html-minifier-next": "^2.1.4", "html-minifier-next": "^2.1.4",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"isomorphic-dompurify": "^2.18.0", "isomorphic-dompurify": "^2.18.0",
"js-yaml": "^4.1.1",
"kysely": "^0.27.4", "kysely": "^0.27.4",
"pg": "^8.13.1", "pg": "^8.13.1",
"readable-to-ms": "^1.0.3", "readable-to-ms": "^1.0.3",

View File

@ -1,5 +1,6 @@
import { Elysia, t } from 'elysia'; import { Elysia, t } from 'elysia';
import * as fs from 'node:fs' import * as fs from 'node:fs'
import yaml from 'js-yaml'
import { db } from '@/utils/database' import { db } from '@/utils/database'
import { validateVideo, validateChannel } from '@/utils/regex' import { validateVideo, validateChannel } from '@/utils/regex'
@ -20,6 +21,7 @@ const saveKey = (videoId: string) => `save:${videoId}`
const DEFAULT_STORAGE_LIMIT_MESSAGE = 'Daily storage limit reached. Is this an urgent archive? Please email me: admin@preservetube.com' const DEFAULT_STORAGE_LIMIT_MESSAGE = 'Daily storage limit reached. Is this an urgent archive? Please email me: admin@preservetube.com'
const NEW_VISITOR_STORAGE_LIMIT_MESSAGE = 'You are a new visitor, so your storage limit is lower for the first few hours. Please come back later. </br>Is this an urgent archive? Please email me: admin@preservetube.com' const NEW_VISITOR_STORAGE_LIMIT_MESSAGE = 'You are a new visitor, so your storage limit is lower for the first few hours. Please come back later. </br>Is this an urgent archive? Please email me: admin@preservetube.com'
const bKeys = yaml.load(await Bun.file('keys.yaml').text()) as { keys: string[] }
const checkMbLimit = async (subjects: string[], mb?: number): Promise<{ isLimited: boolean, isNewVisitorLimited: boolean }> => { const checkMbLimit = async (subjects: string[], mb?: number): Promise<{ isLimited: boolean, isNewVisitorLimited: boolean }> => {
const keys = subjects.map(subject => `save-mb:${Bun.hash(subject).toString()}`) const keys = subjects.map(subject => `save-mb:${Bun.hash(subject).toString()}`)
@ -114,7 +116,8 @@ const getRateLimitKey = (ip: string): string => {
app.ws('/save', { app.ws('/save', {
query: t.Object({ query: t.Object({
url: t.String(), url: t.String(),
rlid: t.Optional(t.String()) rlid: t.Optional(t.String()),
bKey: t.Optional(t.String())
}), }),
body: t.String(), body: t.String(),
open: async (ws) => { open: async (ws) => {
@ -140,16 +143,19 @@ app.ws('/save', {
ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`) ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`)
ws.close() ws.close()
} else { } else {
const subjects = getRateLimitSubjects( if (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) {
getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'), const subjects = getRateLimitSubjects(
ws.data.query.rlid getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'),
) ws.data.query.rlid
const limitStatus = await checkMbLimit(subjects) )
if (limitStatus.isLimited) { const limitStatus = await checkMbLimit(subjects)
return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE); if (limitStatus.isLimited) {
return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE);
}
console.log(`saving (${subjects.map(subject => Bun.hash(subject).toString()).join(',')}) - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
} }
console.log(`saving (${subjects.map(subject => Bun.hash(subject).toString()).join(',')}) - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
ws.send('DATA - This process is automatic. Your video will start archiving shortly.') ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
videoIds[ws.id] = videoId videoIds[ws.id] = videoId
@ -164,13 +170,15 @@ app.ws('/save', {
if (await redis.get(saveKey(videoId)) !== 'downloading') { if (await redis.get(saveKey(videoId)) !== 'downloading') {
await redis.set(saveKey(videoId), 'downloading', 'EX', 300) await redis.set(saveKey(videoId), 'downloading', 'EX', 300)
const captchaCheck = await checkCaptcha(message, ws.data.headers['cf-connecting-ip'] || '0.0.0.0') if (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) {
if (!captchaCheck.success) { const captchaCheck = await checkCaptcha(message, ws.data.headers['cf-connecting-ip'] || '0.0.0.0')
await cleanup(ws, videoId); if (!captchaCheck.success) {
console.log(`captcha failed for ${videoId} - ${JSON.stringify(captchaCheck)}`) await cleanup(ws, videoId);
return sendError(ws, 'Captcha validation failed.'); console.log(`captcha failed for ${videoId} - ${JSON.stringify(captchaCheck)}`)
} else { return sendError(ws, 'Captcha validation failed.');
ws.send('DATA - Captcha validated. Starting download...'); } else {
ws.send('DATA - Captcha validated. Starting download...');
}
} }
const data = await getVideo(videoId) const data = await getVideo(videoId)
@ -194,17 +202,19 @@ app.ws('/save', {
return sendError(ws, downloadResult.message); return sendError(ws, downloadResult.message);
} }
const mbsUsed = Math.ceil(downloadResult.size / (1024 * 1024)) if (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) {
const subjects = getRateLimitSubjects( const mbsUsed = Math.ceil(downloadResult.size / (1024 * 1024))
getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'), const subjects = getRateLimitSubjects(
ws.data.query.rlid getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'),
) ws.data.query.rlid
const limitStatus = await checkMbLimit(subjects, mbsUsed) )
if (limitStatus.isLimited) { const limitStatus = await checkMbLimit(subjects, mbsUsed)
const file = fs.readdirSync('./videos/').find(f => f.includes(`${videoId}.`)) if (limitStatus.isLimited) {
if (file) fs.unlinkSync('./videos/' + file) const file = fs.readdirSync('./videos/').find(f => f.includes(`${videoId}.`))
await cleanup(ws, videoId); if (file) fs.unlinkSync('./videos/' + file)
return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE); await cleanup(ws, videoId);
return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE);
}
} }
const uploadSuccess = await handleUpload(ws, videoId); const uploadSuccess = await handleUpload(ws, videoId);