diff --git a/package.json b/package.json index 944dc8e..34c61d6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.4.6", + "version": "5.4.7", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/cobalt.js b/src/cobalt.js index c8169ce..118ddfb 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -5,7 +5,7 @@ import cors from "cors"; import rateLimit from "express-rate-limit"; import { randomBytes } from "crypto"; -process.env.streamSalt = randomBytes(64).toString('hex'); +const ipSalt = randomBytes(64).toString('hex'); import path from 'path'; import { fileURLToPath } from 'url'; @@ -24,7 +24,7 @@ import { changelogHistory } from "./modules/pageRender/onDemand.js"; import { sha256 } from "./modules/sub/crypto.js"; import findRendered from "./modules/pageRender/findRendered.js"; -if (process.env.selfURL && process.env.streamSalt && process.env.port) { +if (process.env.selfURL && process.env.port) { const commitHash = shortCommit(); const branch = getCurrentBranch(); const app = express(); @@ -38,7 +38,7 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) { max: 25, standardHeaders: false, legacyHeaders: false, - keyGenerator: (req, res) => sha256(getIP(req), process.env.streamSalt), + keyGenerator: (req, res) => sha256(getIP(req), ipSalt), handler: (req, res, next, opt) => { res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); return; @@ -49,7 +49,7 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) { max: 28, standardHeaders: false, legacyHeaders: false, - keyGenerator: (req, res) => sha256(getIP(req), process.env.streamSalt), + keyGenerator: (req, res) => sha256(getIP(req), ipSalt), handler: (req, res, next, opt) => { res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); return; @@ -96,7 +96,7 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) { app.post('/api/json', async (req, res) => { try { - let ip = sha256(getIP(req), process.env.streamSalt); + let ip = sha256(getIP(req), ipSalt); let lang = languageCode(req); let j = apiJSON(0, { t: "Bad request" }); try { @@ -122,7 +122,7 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) { app.get('/api/:type', (req, res) => { try { - let ip = sha256(getIP(req), process.env.streamSalt); + let ip = sha256(getIP(req), ipSalt); switch (req.params.type) { case 'stream': if (req.query.p) { diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index 71d5d94..4be8534 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -22,7 +22,7 @@ const representationMatch = { } export default async function(o) { - let html; + let html, url, filename = `vk_${o.userId}_${o.videoId}_`; html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, { headers: { "user-agent": genericUserAgent } }).then((r) => { return r.text() }).catch(() => { return false }); @@ -35,15 +35,25 @@ export default async function(o) { if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' }; if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })), + if (js.player.params[0]["manifest"]) { + let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })), repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"], bestQuality = repr[repr.length - 1], resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height'; - if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality]; - if (bestQuality) return { - urls: js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`], - filename: `vk_${o.userId}_${o.videoId}_${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4` + if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality]; + + url = js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`]; + filename = `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4` + + } else if (js.player.params[0]["url240"]) { // fallback for when video is too old + url = js.player.params[0]["url240"]; + filename += `320x240.mp4` + } + + if (url && filename) return { + urls: url, + filename: filename }; return { error: 'ErrorEmptyDownload' } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index 677bad2..bb64377 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -3,7 +3,7 @@ export const testers = { || (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13), "vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] - && patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9), + && patternMatch["userId"].length <= 10 && patternMatch["videoId"].length <= 10), "bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12), diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index ebc9b97..e01c2cb 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -1,10 +1,12 @@ import NodeCache from "node-cache"; +import { randomBytes } from "crypto"; import { nanoid } from 'nanoid'; import { sha256 } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true }); +const streamSalt = randomBytes(64).toString('hex'); streamCache.on("expired", (key) => { streamCache.del(key); @@ -13,7 +15,7 @@ streamCache.on("expired", (key) => { export function createStream(obj) { let streamID = nanoid(), exp = Math.floor(new Date().getTime()) + streamLifespan, - ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, process.env.streamSalt); + ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, streamSalt); if (!streamCache.has(streamID)) { streamCache.set(streamID, { @@ -46,7 +48,7 @@ export function verifyStream(ip, id, hmac, exp) { let streamInfo = streamCache.get(id); if (!streamInfo) return { error: 'this stream token does not exist', status: 400 }; - let ghmac = sha256(`${id},${ip},${streamInfo.service},${exp}`, process.env.streamSalt); + let ghmac = sha256(`${id},${ip},${streamInfo.service},${exp}`, streamSalt); if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac) && String(ip) === streamInfo.ip && Number(exp) > Math.floor(new Date().getTime())) { return streamInfo; diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index db27503..27a17b8 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -65,6 +65,8 @@ export function cleanURL(url, host) { let forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"] switch(host) { case "vk": + url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0]; + break; case "youtube": url = url.split('&')[0]; break; diff --git a/src/test/tests.json b/src/test/tests.json index 53301a5..5e94cb7 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -475,6 +475,14 @@ "code": 200, "status": "stream" } + }, { + "name": "ancient video (fallback to 240p)", + "url": "https://vk.com/video-1959_28496479", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } }, { "name": "inexistent video", "url": "https://vk.com/video-53333333_456233333",