diff --git a/bun.lockb b/bun.lockb index 7e8efb2..e2bb602 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml index a940e87..6fd75a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - /mnt/hdd/preservetube-videos:/usr/src/preservetube/backend/videos - ./.env:/usr/src/preservetube/backend/.env - ./s3.json:/usr/src/preservetube/backend/s3.json + - ./keys.yaml:/usr/src/preservetube/backend/keys.yaml networks: public: diff --git a/package.json b/package.json index 913c864..a32853a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@elysiajs/static": "^1.4.0", "@types/crypto-js": "^4.2.2", "@types/html-minifier-next": "^2.1.0", + "@types/js-yaml": "^4.0.9", "@types/pg": "^8.11.10", "@types/ws": "^8.18.1", "age-encryption": "^0.2.4", @@ -26,6 +27,7 @@ "html-minifier-next": "^2.1.4", "ioredis": "^5.4.1", "isomorphic-dompurify": "^2.18.0", + "js-yaml": "^4.1.1", "kysely": "^0.27.4", "pg": "^8.13.1", "readable-to-ms": "^1.0.3", diff --git a/src/router/websocket.ts b/src/router/websocket.ts index 6d60484..cc6c693 100644 --- a/src/router/websocket.ts +++ b/src/router/websocket.ts @@ -1,5 +1,6 @@ import { Elysia, t } from 'elysia'; import * as fs from 'node:fs' +import yaml from 'js-yaml' import { db } from '@/utils/database' 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 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.
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 keys = subjects.map(subject => `save-mb:${Bun.hash(subject).toString()}`) @@ -114,7 +116,8 @@ const getRateLimitKey = (ip: string): string => { app.ws('/save', { query: t.Object({ url: t.String(), - rlid: t.Optional(t.String()) + rlid: t.Optional(t.String()), + bKey: t.Optional(t.String()) }), body: t.String(), open: async (ws) => { @@ -140,16 +143,19 @@ app.ws('/save', { ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`) ws.close() } else { - const subjects = getRateLimitSubjects( - getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'), - ws.data.query.rlid - ) - const limitStatus = await checkMbLimit(subjects) - if (limitStatus.isLimited) { - return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE); + if (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) { + const subjects = getRateLimitSubjects( + getRateLimitKey(ws.data.headers['cf-connecting-ip'] || '0.0.0.0'), + ws.data.query.rlid + ) + const limitStatus = await checkMbLimit(subjects) + 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('CAPTCHA - Solving a cryptographic challenge before downloading.') videoIds[ws.id] = videoId @@ -164,13 +170,15 @@ app.ws('/save', { if (await redis.get(saveKey(videoId)) !== 'downloading') { await redis.set(saveKey(videoId), 'downloading', 'EX', 300) - const captchaCheck = await checkCaptcha(message, ws.data.headers['cf-connecting-ip'] || '0.0.0.0') - if (!captchaCheck.success) { - 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 (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) { + const captchaCheck = await checkCaptcha(message, ws.data.headers['cf-connecting-ip'] || '0.0.0.0') + if (!captchaCheck.success) { + 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...'); + } } const data = await getVideo(videoId) @@ -194,17 +202,19 @@ app.ws('/save', { return sendError(ws, downloadResult.message); } - const mbsUsed = Math.ceil(downloadResult.size / (1024 * 1024)) - const subjects = getRateLimitSubjects( - 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 file = fs.readdirSync('./videos/').find(f => f.includes(`${videoId}.`)) - if (file) fs.unlinkSync('./videos/' + file) - await cleanup(ws, videoId); - return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE); + if (!(ws.data.query.bKey && bKeys.keys.includes(ws.data.query.bKey))) { + const mbsUsed = Math.ceil(downloadResult.size / (1024 * 1024)) + const subjects = getRateLimitSubjects( + 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 file = fs.readdirSync('./videos/').find(f => f.includes(`${videoId}.`)) + if (file) fs.unlinkSync('./videos/' + file) + await cleanup(ws, videoId); + return sendError(ws, limitStatus.isNewVisitorLimited ? NEW_VISITOR_STORAGE_LIMIT_MESSAGE : DEFAULT_STORAGE_LIMIT_MESSAGE); + } } const uploadSuccess = await handleUpload(ws, videoId);