Decode magnet URIs before torrent playback

This commit is contained in:
thePR0M3TH3AN committed 2025-09-23 14:21:07 -04:00
1 parent c88bc404b6
commit db43e3634b
5 files changed
+103 -15

No files matched your search

+16 -9
View File
@@ -5,7 +5,7 @@ import { nostrClient } from "./nostr.js";
import { torrentClient } from "./webtorrent.js";
import { isDevMode } from "./config.js";
import { isWhitelistEnabled } from "./config.js";
import { normalizeAndAugmentMagnet } from "./magnetUtils.js";
import { normalizeAndAugmentMagnet, safeDecodeMagnet } from "./magnetUtils.js";
import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
import {
initialWhitelist,
@@ -38,12 +38,15 @@ function isValidMagnetUri(magnet) {
return false;
}
if (/^[0-9a-f]{40}$/i.test(trimmed)) {
const decoded = safeDecodeMagnet(trimmed);
const candidate = decoded || trimmed;
if (/^[0-9a-f]{40}$/i.test(candidate)) {
return true;
}
try {
const parsed = new URL(trimmed);
const parsed = new URL(candidate);
if (parsed.protocol.toLowerCase() !== "magnet:") {
return false;
}
@@ -2227,8 +2230,10 @@ class bitvidApp {
const legacyInfoHash =
typeof video.infoHash === "string" ? video.infoHash.trim().toLowerCase() : "";
const magnetCandidate = rawMagnet || legacyInfoHash;
const magnetSupported = isValidMagnetUri(magnetCandidate);
const sanitizedMagnet = magnetSupported ? magnetCandidate : "";
const decodedMagnetCandidate = safeDecodeMagnet(magnetCandidate);
const usableMagnetCandidate = decodedMagnetCandidate || magnetCandidate;
const magnetSupported = isValidMagnetUri(usableMagnetCandidate);
const sanitizedMagnet = magnetSupported ? usableMagnetCandidate : "";
this.currentVideo = {
...video,
@@ -2297,7 +2302,7 @@ class bitvidApp {
await this.playVideoWithFallback({
url: trimmedUrl,
magnet: magnetCandidate,
magnet: usableMagnetCandidate,
infoHash: legacyInfoHash,
title: video.title,
description: video.description,
@@ -2312,8 +2317,10 @@ class bitvidApp {
} = {}) {
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
const magnetSupported = isValidMagnetUri(trimmedMagnet);
const sanitizedMagnet = magnetSupported ? trimmedMagnet : "";
const decodedMagnet = safeDecodeMagnet(trimmedMagnet);
const usableMagnet = decodedMagnet || trimmedMagnet;
const magnetSupported = isValidMagnetUri(usableMagnet);
const sanitizedMagnet = magnetSupported ? usableMagnet : "";
if (!sanitizedUrl && !sanitizedMagnet) {
const message = trimmedMagnet && !magnetSupported
@@ -2368,7 +2375,7 @@ class bitvidApp {
await this.playVideoWithFallback({
url: sanitizedUrl,
magnet: trimmedMagnet,
magnet: usableMagnet,
title,
description,
});
+32
View File
@@ -51,6 +51,38 @@ function sanitizeHttpUrl(candidate) {
return "";
}
export function safeDecodeMagnet(value) {
if (typeof value !== "string") {
return "";
}
let decoded = value.trim();
if (!decoded) {
return "";
}
for (let i = 0; i < 2; i += 1) {
if (!decoded.includes("%")) {
break;
}
try {
const candidate = decodeURIComponent(decoded);
if (!candidate) {
break;
}
if (candidate === decoded) {
break;
}
decoded = candidate.trim();
} catch (err) {
break;
}
}
return decoded;
}
export function normalizeAndAugmentMagnet(
rawValue,
{
+12 -6
View File
@@ -1,6 +1,9 @@
// js/playbackUtils.js
import { normalizeAndAugmentMagnet } from "./magnetUtils.js";
import {
normalizeAndAugmentMagnet,
safeDecodeMagnet,
} from "./magnetUtils.js";
const HEX_INFO_HASH = /^[0-9a-f]{40}$/i;
const MAGNET_URI = /^magnet:\?/i;
@@ -13,18 +16,21 @@ export function deriveTorrentPlaybackConfig({
appProtocol,
} = {}) {
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
const decodedMagnet = safeDecodeMagnet(trimmedMagnet);
const magnetCandidate = decodedMagnet || trimmedMagnet;
const trimmedInfoHash =
typeof infoHash === "string" ? infoHash.trim().toLowerCase() : "";
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
const magnetIsUri = MAGNET_URI.test(trimmedMagnet);
const magnetLooksLikeInfoHash = HEX_INFO_HASH.test(trimmedMagnet);
const magnetIsUri = MAGNET_URI.test(magnetCandidate);
const magnetLooksLikeInfoHash = HEX_INFO_HASH.test(magnetCandidate);
const resolvedInfoHash = trimmedInfoHash || (magnetLooksLikeInfoHash
? trimmedMagnet.toLowerCase()
? magnetCandidate.toLowerCase()
: "");
const normalizationInput = magnetIsUri ? trimmedMagnet : resolvedInfoHash;
const normalizationInput = magnetIsUri ? magnetCandidate : resolvedInfoHash;
const provided = Boolean(trimmedMagnet || trimmedInfoHash);
const decodeChanged = magnetCandidate !== trimmedMagnet;
if (!normalizationInput) {
return {
@@ -66,7 +72,7 @@ export function deriveTorrentPlaybackConfig({
provided,
usedInfoHash,
originalInput: normalizationInput,
didMutate: normalization.didChange || usedInfoHash,
didMutate: normalization.didChange || usedInfoHash || decodeChanged,
infoHash: resolvedInfoHash,
};
}
+21
View File
@@ -96,4 +96,25 @@ const LEGACY_INFO_HASH = "0123456789abcdef0123456789abcdef01234567";
assert.equal(result.provided, true);
})();
(function testPlaybackConfigDecodesEncodedMagnet() {
const rawMagnet =
`magnet:?xt=urn:btih:${LEGACY_INFO_HASH}&dn=Legacy+Example`;
const encodedMagnet = encodeURIComponent(rawMagnet);
const result = deriveTorrentPlaybackConfig({
magnet: encodedMagnet,
infoHash: "",
url: "",
});
assert.ok(result.magnet.startsWith("magnet:?"));
const parsed = new URL(result.magnet);
assert.equal(
parsed.searchParams.get("xt"),
`urn:btih:${LEGACY_INFO_HASH}`,
"Expected encoded magnet to be normalized"
);
assert.equal(result.provided, true);
assert.equal(result.usedInfoHash, false);
})();
console.log("legacy infohash tests passed");
+22
View File
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import {
DEFAULT_WSS_TRACKERS,
normalizeAndAugmentMagnet,
safeDecodeMagnet,
} from "../js/magnetUtils.js";
function getParamValues(magnet, key) {
@@ -92,4 +93,25 @@ function getParamValues(magnet, key) {
assert.deepEqual(seeds, ["http://cdn.example.com/video.mp4"]);
})();
(function testSafeDecodeMagnetHandlesEncodedValues() {
const rawMagnet =
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=Example";
const encodedMagnet = encodeURIComponent(rawMagnet);
assert.equal(
safeDecodeMagnet(encodedMagnet),
rawMagnet,
"Expected percent-encoded magnet to be decoded"
);
})();
(function testSafeDecodeMagnetLeavesPlainStringsUntouched() {
const rawMagnet =
"magnet:?xt=urn:btih:abcdefabcdefabcdefabcdefabcdefabcdefabcd";
assert.equal(
safeDecodeMagnet(rawMagnet),
rawMagnet,
"Plain magnet strings should pass through unchanged"
);
})();
console.log("magnet-utils tests passed");