mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +00:00
Refactor magnet helpers into shared module
This commit is contained in:
1 parent
632ee88b90
commit
7f586a38c4
4 files changed
+604
-590
No files matched your search
+31
-205
@@ -1,224 +1,50 @@
|
||||
import { WSS_TRACKERS } from "./constants.js";
|
||||
import {
|
||||
buildMagnetUri,
|
||||
ensureTrackers,
|
||||
ensureTorrentHint,
|
||||
ensureWebSeeds,
|
||||
formatAbsoluteUrl,
|
||||
normalizeMagnetInput,
|
||||
resolveAppProtocol,
|
||||
extractMagnetHints as sharedExtractMagnetHints,
|
||||
} from "./magnetShared.js";
|
||||
|
||||
const HEX_INFO_HASH = /^[0-9a-f]{40}$/i;
|
||||
const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi;
|
||||
export const extractMagnetHints = sharedExtractMagnetHints;
|
||||
|
||||
function getOriginProtocol() {
|
||||
if (typeof window !== "undefined" && window.location?.protocol) {
|
||||
return window.location.protocol.toLowerCase();
|
||||
}
|
||||
return "https:";
|
||||
}
|
||||
|
||||
function decodeLoose(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(trimmed);
|
||||
} catch (err) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeForComparison(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim().replace(/\/+$/, "").toLowerCase();
|
||||
}
|
||||
|
||||
function tryFormatAbsoluteUrl(candidate) {
|
||||
if (typeof candidate !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return parsed.toString();
|
||||
} catch (err) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize and enrich a magnet string entered in the upload form so that it
|
||||
* always produces a WebTorrent-friendly URI. The logic mirrors the behaviour of
|
||||
* the legacy magnetUtils helper but returns a plain string for ease of use.
|
||||
*/
|
||||
export function normalizeAndAugmentMagnet(rawValue, { ws = "", xs = "" } = {}) {
|
||||
const initial = typeof rawValue === "string" ? rawValue.trim() : "";
|
||||
const {
|
||||
initial,
|
||||
canonicalValue,
|
||||
isMagnet,
|
||||
normalizedScheme,
|
||||
fragment,
|
||||
params,
|
||||
} = normalizeMagnetInput(rawValue);
|
||||
|
||||
if (!initial) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let working = initial;
|
||||
|
||||
if (HEX_INFO_HASH.test(working)) {
|
||||
working = `magnet:?xt=urn:btih:${working.toLowerCase()}`;
|
||||
if (!isMagnet) {
|
||||
return canonicalValue;
|
||||
}
|
||||
|
||||
working = working.replace(ENCODED_BTih_PATTERN, (_, hash) => `xt=urn:btih:${hash}`);
|
||||
|
||||
if (!/^magnet:/i.test(working)) {
|
||||
return working;
|
||||
}
|
||||
|
||||
let fragment = "";
|
||||
const hashIndex = working.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
fragment = working.slice(hashIndex);
|
||||
working = working.slice(0, hashIndex);
|
||||
}
|
||||
|
||||
const [, queryPart = ""] = working.split("?", 2);
|
||||
const normalizedScheme = "magnet:";
|
||||
const rawParams = queryPart
|
||||
.split("&")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const params = [];
|
||||
|
||||
for (const rawParam of rawParams) {
|
||||
const [rawKey, rawVal = ""] = rawParam.split("=", 2);
|
||||
const key = rawKey.trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
let value = rawVal.trim();
|
||||
if (key.toLowerCase() === "xt" && value) {
|
||||
const decoded = decodeLoose(value);
|
||||
if (decoded) {
|
||||
value = decoded;
|
||||
}
|
||||
}
|
||||
params.push({
|
||||
key,
|
||||
value,
|
||||
lowerKey: key.toLowerCase(),
|
||||
compareValue: normalizeForComparison(value),
|
||||
});
|
||||
}
|
||||
|
||||
const appendUniqueParam = (key, value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue) {
|
||||
return;
|
||||
}
|
||||
const lowerKey = key.toLowerCase();
|
||||
const compareValue = normalizeForComparison(trimmedValue);
|
||||
if (compareValue) {
|
||||
const exists = params.some(
|
||||
(param) => param.lowerKey === lowerKey && param.compareValue === compareValue
|
||||
);
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
params.push({
|
||||
key,
|
||||
value: trimmedValue,
|
||||
lowerKey,
|
||||
compareValue,
|
||||
});
|
||||
};
|
||||
|
||||
for (const tracker of WSS_TRACKERS) {
|
||||
if (typeof tracker !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmedTracker = tracker.trim();
|
||||
if (!/^wss:\/\//i.test(trimmedTracker)) {
|
||||
continue;
|
||||
}
|
||||
appendUniqueParam("tr", trimmedTracker);
|
||||
}
|
||||
ensureTrackers(params);
|
||||
|
||||
const normalizedXs = typeof xs === "string" ? xs.trim() : "";
|
||||
if (normalizedXs) {
|
||||
appendUniqueParam("xs", normalizedXs);
|
||||
ensureTorrentHint(params, normalizedXs, { requireHttp: false });
|
||||
}
|
||||
|
||||
const originProtocol = getOriginProtocol();
|
||||
const rawWs = typeof ws === "string" ? ws.trim() : "";
|
||||
if (rawWs) {
|
||||
const formattedWs = tryFormatAbsoluteUrl(rawWs);
|
||||
const lowerWs = formattedWs.toLowerCase();
|
||||
const isHttpSeed = lowerWs.startsWith("http://");
|
||||
const allowHttpSeed = originProtocol === "http:";
|
||||
if (!isHttpSeed || allowHttpSeed) {
|
||||
appendUniqueParam("ws", formattedWs);
|
||||
}
|
||||
const formattedWs = formatAbsoluteUrl(rawWs);
|
||||
const allowHttpSeed = resolveAppProtocol() === "http:";
|
||||
ensureWebSeeds(params, formattedWs, {
|
||||
allowHttp: allowHttpSeed,
|
||||
allowUnparsed: true,
|
||||
});
|
||||
}
|
||||
|
||||
const queryString = params
|
||||
.map(({ key, value }) => (value ? `${key}=${value}` : key))
|
||||
.join("&");
|
||||
|
||||
return `${normalizedScheme}${queryString ? `?${queryString}` : ""}${fragment}`;
|
||||
}
|
||||
|
||||
export function extractMagnetHints(rawValue) {
|
||||
const hints = { ws: "", xs: "" };
|
||||
if (typeof rawValue !== "string") {
|
||||
return hints;
|
||||
}
|
||||
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed || !trimmed.toLowerCase().startsWith("magnet:")) {
|
||||
return hints;
|
||||
}
|
||||
|
||||
const queryIndex = trimmed.indexOf("?");
|
||||
if (queryIndex === -1 || queryIndex === trimmed.length - 1) {
|
||||
return hints;
|
||||
}
|
||||
|
||||
const query = trimmed.slice(queryIndex + 1);
|
||||
if (!query) {
|
||||
return hints;
|
||||
}
|
||||
|
||||
const parts = query.split("&");
|
||||
for (const part of parts) {
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
const [rawKey, rawValue = ""] = part.split("=", 2);
|
||||
if (!rawKey) {
|
||||
continue;
|
||||
}
|
||||
const lowerKey = rawKey.trim().toLowerCase();
|
||||
if (lowerKey !== "ws" && lowerKey !== "xs") {
|
||||
continue;
|
||||
}
|
||||
if (hints[lowerKey]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let decoded = rawValue;
|
||||
try {
|
||||
decoded = decodeURIComponent(rawValue);
|
||||
} catch (err) {
|
||||
// Ignore decode errors and use the raw value.
|
||||
}
|
||||
const clean = typeof decoded === "string" ? decoded.trim() : "";
|
||||
if (!clean) {
|
||||
continue;
|
||||
}
|
||||
hints[lowerKey] = clean;
|
||||
}
|
||||
|
||||
return hints;
|
||||
return buildMagnetUri(normalizedScheme, params, fragment);
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import { WSS_TRACKERS } from "./constants.js";
|
||||
|
||||
const HEX_INFO_HASH = /^[0-9a-f]{40}$/i;
|
||||
const BTIH_PREFIX = "urn:btih:";
|
||||
const MAGNET_SCHEME = "magnet:";
|
||||
const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi;
|
||||
|
||||
function decodeLoose(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(trimmed);
|
||||
} catch (err) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
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 || candidate === decoded) {
|
||||
break;
|
||||
}
|
||||
decoded = candidate.trim();
|
||||
} catch (err) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function normalizeForComparison(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const pathname = parsed.pathname.replace(/\/?$/, "");
|
||||
const normalizedPath = pathname ? pathname : "";
|
||||
return (
|
||||
`${parsed.protocol}//${parsed.host}${normalizedPath}${parsed.search}${parsed.hash}`
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
} catch (err) {
|
||||
return trimmed.replace(/\/?$/, "").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function createParam(key, value) {
|
||||
const trimmedValue = typeof value === "string" ? value.trim() : "";
|
||||
const decoded = decodeLoose(trimmedValue);
|
||||
const comparisonBasis = decoded || trimmedValue;
|
||||
return {
|
||||
key,
|
||||
value: trimmedValue,
|
||||
lowerKey: key.toLowerCase(),
|
||||
decoded,
|
||||
comparison: normalizeForComparison(comparisonBasis),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeMagnetInput(rawValue) {
|
||||
const initial = typeof rawValue === "string" ? rawValue.trim() : "";
|
||||
if (!initial) {
|
||||
return {
|
||||
initial,
|
||||
canonicalValue: "",
|
||||
didMutate: false,
|
||||
isMagnet: false,
|
||||
normalizedScheme: "",
|
||||
fragment: "",
|
||||
params: [],
|
||||
};
|
||||
}
|
||||
|
||||
let working = initial;
|
||||
let didMutate = false;
|
||||
|
||||
if (HEX_INFO_HASH.test(working)) {
|
||||
working = `${MAGNET_SCHEME}?xt=${BTIH_PREFIX}${working.toLowerCase()}`;
|
||||
didMutate = true;
|
||||
}
|
||||
|
||||
ENCODED_BTih_PATTERN.lastIndex = 0;
|
||||
const decodedXt = working.replace(ENCODED_BTih_PATTERN, (_, hash) => {
|
||||
didMutate = true;
|
||||
return `xt=${BTIH_PREFIX}${hash}`;
|
||||
});
|
||||
working = decodedXt;
|
||||
|
||||
let fragment = "";
|
||||
const hashIndex = working.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
fragment = working.slice(hashIndex);
|
||||
working = working.slice(0, hashIndex);
|
||||
}
|
||||
|
||||
const canonicalValue = working;
|
||||
if (!/^magnet:/i.test(working)) {
|
||||
return {
|
||||
initial,
|
||||
canonicalValue,
|
||||
didMutate: didMutate || canonicalValue !== initial,
|
||||
isMagnet: false,
|
||||
normalizedScheme: "",
|
||||
fragment: "",
|
||||
params: [],
|
||||
};
|
||||
}
|
||||
|
||||
const [schemePart, queryPart = ""] = working.split("?", 2);
|
||||
const normalizedScheme = MAGNET_SCHEME;
|
||||
if (schemePart !== normalizedScheme) {
|
||||
didMutate = true;
|
||||
}
|
||||
|
||||
const rawParams = queryPart
|
||||
.split("&")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const params = [];
|
||||
for (const rawParam of rawParams) {
|
||||
const [rawKey, rawVal = ""] = rawParam.split("=", 2);
|
||||
const key = rawKey.trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const lowerKey = key.toLowerCase();
|
||||
let value = rawVal.trim();
|
||||
if (lowerKey === "xt" && value) {
|
||||
const decoded = decodeLoose(value);
|
||||
if (decoded && decoded !== value) {
|
||||
value = decoded;
|
||||
didMutate = true;
|
||||
}
|
||||
}
|
||||
params.push(createParam(key, value));
|
||||
}
|
||||
|
||||
return {
|
||||
initial,
|
||||
canonicalValue,
|
||||
didMutate,
|
||||
isMagnet: true,
|
||||
normalizedScheme,
|
||||
fragment,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMagnetUri(normalizedScheme, params, fragment) {
|
||||
const queryString = params
|
||||
.map(({ key, value }) => (value ? `${key}=${value}` : key))
|
||||
.join("&");
|
||||
return `${normalizedScheme}${queryString ? `?${queryString}` : ""}${fragment || ""}`;
|
||||
}
|
||||
|
||||
export function appendUniqueParam(params, key, value) {
|
||||
if (!Array.isArray(params)) {
|
||||
return false;
|
||||
}
|
||||
const candidate = createParam(key, value);
|
||||
if (!candidate.value && candidate.value !== "") {
|
||||
return false;
|
||||
}
|
||||
if (candidate.comparison) {
|
||||
const exists = params.some(
|
||||
(param) => param.lowerKey === candidate.lowerKey && param.comparison === candidate.comparison
|
||||
);
|
||||
if (exists) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const exists = params.some(
|
||||
(param) => param.lowerKey === candidate.lowerKey && param.value === candidate.value
|
||||
);
|
||||
if (exists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
params.push(candidate);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ensureTrackers(params, trackers = WSS_TRACKERS) {
|
||||
let didMutate = false;
|
||||
for (const tracker of trackers) {
|
||||
if (typeof tracker !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmedTracker = tracker.trim();
|
||||
if (!trimmedTracker) {
|
||||
continue;
|
||||
}
|
||||
if (!/^wss:\/\//i.test(trimmedTracker)) {
|
||||
continue;
|
||||
}
|
||||
if (appendUniqueParam(params, "tr", trimmedTracker)) {
|
||||
didMutate = true;
|
||||
}
|
||||
}
|
||||
return didMutate;
|
||||
}
|
||||
|
||||
export function sanitizeHttpUrl(candidate) {
|
||||
if (typeof candidate !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
||||
return parsed.toString();
|
||||
}
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function ensureTorrentHint(params, candidate, { requireHttp = true } = {}) {
|
||||
if (requireHttp) {
|
||||
const sanitized = sanitizeHttpUrl(candidate);
|
||||
if (!sanitized) {
|
||||
return false;
|
||||
}
|
||||
return appendUniqueParam(params, "xs", sanitized);
|
||||
}
|
||||
|
||||
if (typeof candidate !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return appendUniqueParam(params, "xs", trimmed);
|
||||
}
|
||||
|
||||
export function resolveAppProtocol(explicitProtocol) {
|
||||
if (typeof explicitProtocol === "string" && explicitProtocol.trim()) {
|
||||
return explicitProtocol.trim().toLowerCase();
|
||||
}
|
||||
if (typeof window !== "undefined" && window.location?.protocol) {
|
||||
return window.location.protocol.toLowerCase();
|
||||
}
|
||||
return "https:";
|
||||
}
|
||||
|
||||
export function ensureWebSeeds(
|
||||
params,
|
||||
seeds,
|
||||
{ allowHttp = false, allowUnparsed = false, logger } = {}
|
||||
) {
|
||||
const list = Array.isArray(seeds)
|
||||
? seeds
|
||||
: typeof seeds === "string"
|
||||
? [seeds]
|
||||
: [];
|
||||
let didMutate = false;
|
||||
for (const seed of list) {
|
||||
if (typeof seed !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmedSeed = seed.trim();
|
||||
if (!trimmedSeed) {
|
||||
continue;
|
||||
}
|
||||
let finalValue = trimmedSeed;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmedSeed);
|
||||
const protocol = parsed.protocol;
|
||||
if (protocol === "https:") {
|
||||
finalValue = parsed.toString();
|
||||
} else if (protocol === "http:") {
|
||||
if (!allowHttp) {
|
||||
if (typeof logger === "function") {
|
||||
logger(`[normalizeAndAugmentMagnet] Skipping insecure web seed: ${trimmedSeed}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
finalValue = parsed.toString();
|
||||
} else if (!allowUnparsed) {
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!allowUnparsed) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (appendUniqueParam(params, "ws", finalValue)) {
|
||||
didMutate = true;
|
||||
}
|
||||
}
|
||||
return didMutate;
|
||||
}
|
||||
|
||||
export function formatAbsoluteUrl(candidate) {
|
||||
if (typeof candidate !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return parsed.toString();
|
||||
} catch (err) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMagnetHints(rawValue) {
|
||||
const { params } = normalizeMagnetInput(rawValue);
|
||||
const hints = { ws: "", xs: "" };
|
||||
for (const param of params) {
|
||||
if (param.lowerKey === "ws" && !hints.ws) {
|
||||
hints.ws = param.decoded || param.value;
|
||||
} else if (param.lowerKey === "xs" && !hints.xs) {
|
||||
hints.xs = param.decoded || param.value;
|
||||
}
|
||||
if (hints.ws && hints.xs) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
+44
-261
@@ -1,101 +1,19 @@
|
||||
// js/magnetUtils.js
|
||||
|
||||
import { WSS_TRACKERS } from "./constants.js";
|
||||
import {
|
||||
buildMagnetUri,
|
||||
ensureTrackers,
|
||||
ensureTorrentHint,
|
||||
ensureWebSeeds,
|
||||
normalizeMagnetInput,
|
||||
resolveAppProtocol,
|
||||
safeDecodeMagnet as sharedSafeDecodeMagnet,
|
||||
} from "./magnetShared.js";
|
||||
|
||||
export { WSS_TRACKERS };
|
||||
export const safeDecodeMagnet = sharedSafeDecodeMagnet;
|
||||
|
||||
const HEX_INFO_HASH = /^[0-9a-f]{40}$/i;
|
||||
const BTIH_PREFIX = "urn:btih:";
|
||||
const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi;
|
||||
|
||||
function normalizeForComparison(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const pathname = parsed.pathname.replace(/\/?$/, "");
|
||||
const normalizedPath = pathname ? pathname : "";
|
||||
return (
|
||||
`${parsed.protocol}//${parsed.host}${normalizedPath}${parsed.search}${parsed.hash}`
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
} catch (err) {
|
||||
return trimmed.replace(/\/?$/, "").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeHttpUrl(candidate) {
|
||||
if (typeof candidate !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
||||
return parsed.toString();
|
||||
}
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a magnet URI while preserving legacy payload quirks and augmenting it
|
||||
* for browser playback.
|
||||
*
|
||||
* Key behaviors:
|
||||
* - Accepts bare info-hash strings and promotes them to full `magnet:?xt=urn:btih:` links.
|
||||
* - Leaves the existing `xt` payload untouched—no percent re-encoding or normalization
|
||||
* beyond decoding legacy `%3A` segments.
|
||||
* - Only appends browser-safe WSS trackers in addition to whatever the caller provides.
|
||||
* - Skips insecure `http:` web seeds unless the application itself is running over HTTP.
|
||||
*
|
||||
* Note: `didChange` may be reported even if the resulting magnet string is textually
|
||||
* identical to the input. Callers should not rely on `didChange` to detect a modified
|
||||
* URI.
|
||||
*/
|
||||
export function normalizeAndAugmentMagnet(
|
||||
rawValue,
|
||||
{
|
||||
@@ -107,83 +25,32 @@ export function normalizeAndAugmentMagnet(
|
||||
appProtocol,
|
||||
} = {}
|
||||
) {
|
||||
const log = typeof logger === "function" ? logger : () => {};
|
||||
const initial = typeof rawValue === "string" ? rawValue.trim() : "";
|
||||
const {
|
||||
initial,
|
||||
canonicalValue,
|
||||
didMutate,
|
||||
isMagnet,
|
||||
normalizedScheme,
|
||||
fragment,
|
||||
params,
|
||||
} = normalizeMagnetInput(rawValue);
|
||||
|
||||
if (!initial) {
|
||||
return { magnet: "", didChange: false };
|
||||
}
|
||||
|
||||
let working = initial;
|
||||
let didMutate = false;
|
||||
const bareHashMatch = HEX_INFO_HASH.test(working);
|
||||
if (bareHashMatch) {
|
||||
working = `magnet:?xt=${BTIH_PREFIX}${working.toLowerCase()}`;
|
||||
didMutate = true;
|
||||
}
|
||||
|
||||
const hashIndex = working.indexOf("#");
|
||||
let fragment = "";
|
||||
if (hashIndex !== -1) {
|
||||
fragment = working.slice(hashIndex);
|
||||
working = working.slice(0, hashIndex);
|
||||
}
|
||||
|
||||
const decodedXt = working.replace(ENCODED_BTih_PATTERN, (_, hash) => {
|
||||
didMutate = true;
|
||||
return `xt=${BTIH_PREFIX}${hash}`;
|
||||
});
|
||||
working = decodedXt;
|
||||
|
||||
if (!/^magnet:/i.test(working)) {
|
||||
if (!isMagnet) {
|
||||
const magnet = canonicalValue;
|
||||
return {
|
||||
magnet: working,
|
||||
didChange: didMutate || working !== initial,
|
||||
magnet,
|
||||
didChange: didMutate || magnet !== initial,
|
||||
};
|
||||
}
|
||||
|
||||
const [schemePart, queryPart = ""] = working.split("?", 2);
|
||||
const normalizedScheme = "magnet:";
|
||||
if (schemePart !== normalizedScheme) {
|
||||
didMutate = true;
|
||||
}
|
||||
let didChange = didMutate;
|
||||
|
||||
const rawParams = queryPart
|
||||
.split("&")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const params = [];
|
||||
|
||||
const decodeLoose = (value) => {
|
||||
if (typeof value !== "string" || !value) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (err) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
for (const rawParam of rawParams) {
|
||||
const [rawKey, rawValue = ""] = rawParam.split("=", 2);
|
||||
const key = rawKey.trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
let value = rawValue.trim();
|
||||
if (key.toLowerCase() === "xt" && value) {
|
||||
const decoded = decodeLoose(value);
|
||||
if (decoded !== value) {
|
||||
value = decoded;
|
||||
didMutate = true;
|
||||
}
|
||||
}
|
||||
params.push({
|
||||
key,
|
||||
value,
|
||||
decoded: decodeLoose(value),
|
||||
});
|
||||
if (ensureTrackers(params, [...WSS_TRACKERS, ...extraTrackers])) {
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
const torrentHint = typeof torrentUrl === "string" && torrentUrl.trim()
|
||||
@@ -192,118 +59,34 @@ export function normalizeAndAugmentMagnet(
|
||||
? xs.trim()
|
||||
: "";
|
||||
|
||||
const safeTorrentHint = sanitizeHttpUrl(torrentHint);
|
||||
if (torrentHint) {
|
||||
if (ensureTorrentHint(params, torrentHint, { requireHttp: true })) {
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
const seedInputs = Array.isArray(webSeed)
|
||||
? webSeed
|
||||
: typeof webSeed === "string"
|
||||
? [webSeed]
|
||||
: [];
|
||||
|
||||
const resolvedProtocol = typeof appProtocol === "string" && appProtocol
|
||||
? appProtocol.toLowerCase()
|
||||
: typeof window !== "undefined" && window.location && window.location.protocol
|
||||
? window.location.protocol.toLowerCase()
|
||||
: "https:";
|
||||
const resolvedProtocol = resolveAppProtocol(appProtocol);
|
||||
|
||||
const existingTrackers = new Set(
|
||||
params
|
||||
.filter((param) => param.key.toLowerCase() === "tr")
|
||||
.map((param) => normalizeForComparison(param.decoded))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const trackerCandidates = [
|
||||
...WSS_TRACKERS,
|
||||
...extraTrackers,
|
||||
];
|
||||
|
||||
for (const tracker of trackerCandidates) {
|
||||
if (typeof tracker !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmedTracker = tracker.trim();
|
||||
if (!trimmedTracker) {
|
||||
continue;
|
||||
}
|
||||
if (!/^wss:\/\//i.test(trimmedTracker)) {
|
||||
continue;
|
||||
}
|
||||
const normalizedTracker = normalizeForComparison(trimmedTracker);
|
||||
if (!normalizedTracker || existingTrackers.has(normalizedTracker)) {
|
||||
continue;
|
||||
}
|
||||
params.push({ key: "tr", value: trimmedTracker, decoded: trimmedTracker });
|
||||
existingTrackers.add(normalizedTracker);
|
||||
didMutate = true;
|
||||
if (
|
||||
ensureWebSeeds(params, seedInputs, {
|
||||
allowHttp: resolvedProtocol === "http:",
|
||||
allowUnparsed: false,
|
||||
logger,
|
||||
})
|
||||
) {
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (safeTorrentHint) {
|
||||
const existingXs = new Set(
|
||||
params
|
||||
.filter((param) => param.key.toLowerCase() === "xs")
|
||||
.map((param) => normalizeForComparison(param.decoded))
|
||||
.filter(Boolean)
|
||||
);
|
||||
const normalizedXs = normalizeForComparison(safeTorrentHint);
|
||||
if (normalizedXs && !existingXs.has(normalizedXs)) {
|
||||
params.push({
|
||||
key: "xs",
|
||||
value: safeTorrentHint,
|
||||
decoded: safeTorrentHint,
|
||||
});
|
||||
didMutate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (seedInputs.length) {
|
||||
const existingWs = new Set(
|
||||
params
|
||||
.filter((param) => param.key.toLowerCase() === "ws")
|
||||
.map((param) => normalizeForComparison(param.decoded))
|
||||
.filter(Boolean)
|
||||
);
|
||||
for (const seedInput of seedInputs) {
|
||||
if (typeof seedInput !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmedSeed = seedInput.trim();
|
||||
if (!trimmedSeed) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsedSeed = new URL(trimmedSeed);
|
||||
const seedProtocol = parsedSeed.protocol;
|
||||
const allowHttpSeed = resolvedProtocol === "http:";
|
||||
if (
|
||||
seedProtocol === "https:" ||
|
||||
(seedProtocol === "http:" && allowHttpSeed)
|
||||
) {
|
||||
const seedValue = parsedSeed.toString();
|
||||
const normalizedSeed = normalizeForComparison(seedValue);
|
||||
if (normalizedSeed && !existingWs.has(normalizedSeed)) {
|
||||
params.push({ key: "ws", value: seedValue, decoded: seedValue });
|
||||
existingWs.add(normalizedSeed);
|
||||
didMutate = true;
|
||||
}
|
||||
} else if (seedProtocol === "http:") {
|
||||
log(
|
||||
`[normalizeAndAugmentMagnet] Skipping insecure web seed: ${trimmedSeed}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore invalid web seed values silently.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params
|
||||
.map(({ key, value }) => (value ? `${key}=${value}` : key))
|
||||
.join("&");
|
||||
|
||||
const finalMagnet = `${normalizedScheme}${queryString ? `?${queryString}` : ""}${fragment}`;
|
||||
const finalMagnet = buildMagnetUri(normalizedScheme, params, fragment);
|
||||
|
||||
return {
|
||||
magnet: finalMagnet,
|
||||
didChange: didMutate || finalMagnet !== initial,
|
||||
didChange: didChange || finalMagnet !== initial,
|
||||
};
|
||||
}
|
||||
+169
-124
@@ -1,160 +1,205 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { WSS_TRACKERS } from "../js/constants.js";
|
||||
import { normalizeAndAugmentMagnet, safeDecodeMagnet } from "../js/magnetUtils.js";
|
||||
import { normalizeAndAugmentMagnet as formNormalize } from "../js/magnet.js";
|
||||
import {
|
||||
normalizeAndAugmentMagnet as normalizeMagnetObject,
|
||||
safeDecodeMagnet,
|
||||
} from "../js/magnetUtils.js";
|
||||
import {
|
||||
extractMagnetHints,
|
||||
normalizeAndAugmentMagnet as normalizeMagnetString,
|
||||
} from "../js/magnet.js";
|
||||
|
||||
function getParamValues(magnet, key) {
|
||||
const parsed = new URL(magnet);
|
||||
return parsed.searchParams.getAll(key);
|
||||
}
|
||||
|
||||
(function testBareInfoHashNormalization() {
|
||||
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
||||
const result = normalizeAndAugmentMagnet(infoHash);
|
||||
assert.ok(result.didChange, "Bare info hash should mark didChange true");
|
||||
const xtValues = getParamValues(result.magnet, "xt");
|
||||
assert.deepEqual(xtValues, [`urn:btih:${infoHash}`]);
|
||||
const trackerValues = getParamValues(result.magnet, "tr");
|
||||
function assertDefaultTrackersPresent(magnet) {
|
||||
const trackers = getParamValues(magnet, "tr");
|
||||
for (const tracker of WSS_TRACKERS) {
|
||||
assert.ok(
|
||||
trackerValues.includes(tracker),
|
||||
`Expected tracker ${tracker} to be appended`
|
||||
trackers.includes(tracker),
|
||||
`Expected default tracker ${tracker} to be present`
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
(function testEncodedXtDecoding() {
|
||||
const infoHash = "abcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
const encodedMagnet = `magnet:?xt=urn%3Abtih%3A${infoHash}&dn=Example`;
|
||||
const result = normalizeAndAugmentMagnet(encodedMagnet);
|
||||
const xtValues = getParamValues(result.magnet, "xt");
|
||||
assert.deepEqual(xtValues, [`urn:btih:${infoHash}`]);
|
||||
})();
|
||||
|
||||
(function testDuplicateTrackerFiltering() {
|
||||
const infoHash = "fedcba9876543210fedcba9876543210fedcba98";
|
||||
const baseMagnet =
|
||||
`magnet:?xt=urn:btih:${infoHash}` +
|
||||
"&tr=wss://tracker.openwebtorrent.com" +
|
||||
"&tr=WsS://tracker.openwebtorrent.com/" +
|
||||
"&tr=https://not-allowed.example";
|
||||
const result = normalizeAndAugmentMagnet(baseMagnet, {
|
||||
extraTrackers: [
|
||||
"wss://tracker.fastcast.nz",
|
||||
"http://tracker.invalid",
|
||||
"wss://tracker.openwebtorrent.com",
|
||||
],
|
||||
});
|
||||
const trackers = getParamValues(result.magnet, "tr");
|
||||
const openwebCount = trackers.filter(
|
||||
(value) => value === "wss://tracker.openwebtorrent.com"
|
||||
).length;
|
||||
assert.equal(openwebCount, 1, "Duplicate WSS tracker should be collapsed");
|
||||
assert.ok(
|
||||
trackers.includes("wss://tracker.fastcast.nz"),
|
||||
"Expected extra WSS tracker to be preserved"
|
||||
);
|
||||
assert.ok(
|
||||
!trackers.includes("http://tracker.invalid"),
|
||||
"Non-WSS tracker candidates should be ignored"
|
||||
);
|
||||
})();
|
||||
|
||||
(function testWebSeedHandlingWithLogging() {
|
||||
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
||||
const messages = [];
|
||||
const result = normalizeAndAugmentMagnet(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
webSeed: [
|
||||
"https://cdn.example.com/video.mp4",
|
||||
"http://cdn.example.com/legacy.mp4",
|
||||
"https://cdn.example.com/video.mp4",
|
||||
],
|
||||
logger: (msg) => messages.push(msg),
|
||||
appProtocol: "https:",
|
||||
});
|
||||
const seeds = getParamValues(result.magnet, "ws");
|
||||
assert.deepEqual(seeds, ["https://cdn.example.com/video.mp4"]);
|
||||
assert.equal(messages.length, 1, "HTTP seed should have been skipped with a log");
|
||||
assert.ok(
|
||||
messages[0].includes("Skipping insecure web seed"),
|
||||
"Expected skip message for HTTP seed"
|
||||
);
|
||||
})();
|
||||
|
||||
(function testHttpWebSeedAllowedOnHttpOrigin() {
|
||||
const infoHash = "abcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
const result = normalizeAndAugmentMagnet(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
webSeed: "http://cdn.example.com/video.mp4",
|
||||
appProtocol: "http:",
|
||||
});
|
||||
const seeds = getParamValues(result.magnet, "ws");
|
||||
assert.deepEqual(seeds, ["http://cdn.example.com/video.mp4"]);
|
||||
})();
|
||||
|
||||
(function testSafeDecodeMagnetHandlesEncodedValues() {
|
||||
test("safeDecodeMagnet handles encoded values", () => {
|
||||
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"
|
||||
"Percent-encoded magnets should be decoded"
|
||||
);
|
||||
})();
|
||||
});
|
||||
|
||||
(function testSafeDecodeMagnetLeavesPlainStringsUntouched() {
|
||||
test("safeDecodeMagnet leaves plain strings untouched", () => {
|
||||
const rawMagnet =
|
||||
"magnet:?xt=urn:btih:abcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
assert.equal(
|
||||
safeDecodeMagnet(rawMagnet),
|
||||
rawMagnet,
|
||||
"Plain magnet strings should pass through unchanged"
|
||||
"Plain magnet strings should pass through"
|
||||
);
|
||||
})();
|
||||
});
|
||||
|
||||
(function testFormHelperAugmentsWsXsAndDecodesXt() {
|
||||
const infoHash = "abcdef0123456789abcdef0123456789abcdef01";
|
||||
const encodedMagnet = `magnet:?xt=urn%3Abtih%3A${infoHash}`;
|
||||
const normalized = formNormalize(encodedMagnet, {
|
||||
ws: "https://cdn.example.com/video-base/",
|
||||
test("bare hashes normalize consistently across helpers", () => {
|
||||
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
||||
const objectResult = normalizeMagnetObject(infoHash);
|
||||
const stringResult = normalizeMagnetString(infoHash);
|
||||
|
||||
assert.ok(objectResult.didChange, "Bare hashes should mark didChange true");
|
||||
assert.equal(
|
||||
stringResult,
|
||||
objectResult.magnet,
|
||||
"String helper should match object helper output"
|
||||
);
|
||||
|
||||
const xtValues = getParamValues(stringResult, "xt");
|
||||
assert.deepEqual(xtValues, [`urn:btih:${infoHash}`]);
|
||||
assertDefaultTrackersPresent(stringResult);
|
||||
});
|
||||
|
||||
test("legacy %3A payloads decode across helpers", () => {
|
||||
const infoHash = "abcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
const encoded = `magnet:?xt=urn%3Abtih%3A${infoHash}&dn=Example`;
|
||||
const objectResult = normalizeMagnetObject(encoded);
|
||||
const stringResult = normalizeMagnetString(encoded);
|
||||
|
||||
assert.equal(
|
||||
stringResult,
|
||||
objectResult.magnet,
|
||||
"Helpers should agree on encoded xt payload normalization"
|
||||
);
|
||||
|
||||
const xtValues = getParamValues(stringResult, "xt");
|
||||
assert.deepEqual(xtValues, [`urn:btih:${infoHash}`]);
|
||||
});
|
||||
|
||||
test("duplicate trackers and hints stay in sync", () => {
|
||||
const infoHash = "fedcba9876543210fedcba9876543210fedcba98";
|
||||
const baseMagnet =
|
||||
`magnet:?xt=urn:btih:${infoHash}` +
|
||||
"&tr=wss://tracker.openwebtorrent.com" +
|
||||
"&tr=WsS://tracker.openwebtorrent.com/";
|
||||
|
||||
const objectResult = normalizeMagnetObject(baseMagnet, {
|
||||
extraTrackers: [
|
||||
"wss://tracker.fastcast.nz",
|
||||
"http://tracker.invalid",
|
||||
"wss://tracker.openwebtorrent.com",
|
||||
],
|
||||
xs: "https://cdn.example.com/video.torrent",
|
||||
webSeed: "https://cdn.example.com/files/",
|
||||
});
|
||||
|
||||
const stringResult = normalizeMagnetString(baseMagnet, {
|
||||
ws: "https://cdn.example.com/files/", // ensure string helper adds hint
|
||||
xs: "https://cdn.example.com/video.torrent",
|
||||
});
|
||||
const parsed = new URL(normalized);
|
||||
assert.equal(
|
||||
parsed.searchParams.get("xt"),
|
||||
`urn:btih:${infoHash}`,
|
||||
"Form helper should decode encoded xt payloads"
|
||||
);
|
||||
assert.deepEqual(
|
||||
parsed.searchParams.getAll("ws"),
|
||||
["https://cdn.example.com/video-base/"],
|
||||
"Expected ws hint to be appended"
|
||||
);
|
||||
assert.deepEqual(
|
||||
parsed.searchParams.getAll("xs"),
|
||||
["https://cdn.example.com/video.torrent"],
|
||||
"Expected xs hint to be appended"
|
||||
);
|
||||
const trackers = parsed.searchParams.getAll("tr");
|
||||
for (const tracker of WSS_TRACKERS) {
|
||||
assert.ok(
|
||||
trackers.includes(tracker),
|
||||
`Expected helper to append tracker ${tracker}`
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
(function testFormHelperSkipsInsecureWsOnHttpsOrigin() {
|
||||
assert.equal(
|
||||
stringResult,
|
||||
objectResult.magnet,
|
||||
"Helpers should emit identical magnets after augmentation"
|
||||
);
|
||||
|
||||
const trackers = getParamValues(stringResult, "tr");
|
||||
const openwebCount = trackers.filter(
|
||||
(value) => value === "wss://tracker.openwebtorrent.com"
|
||||
).length;
|
||||
assert.equal(openwebCount, 1, "Duplicate trackers should be deduped when appended");
|
||||
assert.ok(
|
||||
trackers.includes("wss://tracker.fastcast.nz"),
|
||||
"Extra WSS tracker should be retained"
|
||||
);
|
||||
assert.ok(
|
||||
!trackers.includes("http://tracker.invalid"),
|
||||
"HTTP tracker candidates should be ignored"
|
||||
);
|
||||
|
||||
const webSeeds = getParamValues(stringResult, "ws");
|
||||
assert.deepEqual(webSeeds, ["https://cdn.example.com/files/"]);
|
||||
|
||||
const xsValues = getParamValues(stringResult, "xs");
|
||||
assert.deepEqual(xsValues, ["https://cdn.example.com/video.torrent"]);
|
||||
});
|
||||
|
||||
test("object helper enforces HTTPS web seeds when on HTTPS", () => {
|
||||
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
||||
const normalized = formNormalize(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
const messages = [];
|
||||
const result = normalizeMagnetObject(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
webSeed: [
|
||||
"https://cdn.example.com/video.mp4",
|
||||
"http://cdn.example.com/legacy.mp4",
|
||||
],
|
||||
logger: (msg) => messages.push(msg),
|
||||
appProtocol: "https:",
|
||||
});
|
||||
|
||||
const seeds = getParamValues(result.magnet, "ws");
|
||||
assert.deepEqual(seeds, ["https://cdn.example.com/video.mp4"]);
|
||||
assert.equal(messages.length, 1, "HTTP web seeds should trigger a log message");
|
||||
assert.ok(
|
||||
messages[0].includes("Skipping insecure web seed"),
|
||||
"Expected skip log for HTTP seed"
|
||||
);
|
||||
});
|
||||
|
||||
test("object helper allows HTTP seeds on HTTP origins", () => {
|
||||
const infoHash = "abcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
const result = normalizeMagnetObject(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
webSeed: "http://cdn.example.com/video.mp4",
|
||||
appProtocol: "http:",
|
||||
});
|
||||
|
||||
const seeds = getParamValues(result.magnet, "ws");
|
||||
assert.deepEqual(seeds, ["http://cdn.example.com/video.mp4"]);
|
||||
});
|
||||
|
||||
test("extractMagnetHints returns first ws/xs pair", () => {
|
||||
const magnet =
|
||||
"magnet:?xt=urn:btih:abcdef0123456789abcdef0123456789abcdef01" +
|
||||
"&ws=https://cdn.example.com/base/" +
|
||||
"&ws=https://cdn.example.com/duplicate/" +
|
||||
"&xs=https://cdn.example.com/video.torrent";
|
||||
|
||||
const hints = extractMagnetHints(magnet);
|
||||
assert.deepEqual(hints, {
|
||||
ws: "https://cdn.example.com/base/",
|
||||
xs: "https://cdn.example.com/video.torrent",
|
||||
});
|
||||
});
|
||||
|
||||
test("string helper skips insecure ws hints on HTTPS origins", () => {
|
||||
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
||||
const normalized = normalizeMagnetString(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
ws: "http://cdn.example.com/video-base/",
|
||||
});
|
||||
const parsed = new URL(normalized);
|
||||
assert.equal(
|
||||
parsed.searchParams.getAll("ws").length,
|
||||
0,
|
||||
"HTTP ws value should be skipped on HTTPS origins"
|
||||
);
|
||||
})();
|
||||
assert.equal(parsed.searchParams.getAll("ws").length, 0);
|
||||
});
|
||||
|
||||
console.log("magnet-utils tests passed");
|
||||
test("object helper reports didChange when output differs", () => {
|
||||
const infoHash = "abcdef0123456789abcdef0123456789abcdef01";
|
||||
const result = normalizeMagnetObject(`magnet:?xt=urn:btih:${infoHash}`, {
|
||||
webSeed: "https://cdn.example.com/video.mp4",
|
||||
});
|
||||
assert.ok(result.didChange, "Augmenting with web seeds should mark didChange");
|
||||
});
|
||||
|
||||
test("helpers trim fragments from non-magnet values", () => {
|
||||
const url = "https://example.com/video.mp4#fragment";
|
||||
const objectResult = normalizeMagnetObject(url);
|
||||
const stringResult = normalizeMagnetString(url);
|
||||
assert.equal(objectResult.magnet, "https://example.com/video.mp4");
|
||||
assert.equal(stringResult, "https://example.com/video.mp4");
|
||||
assert.ok(
|
||||
objectResult.didChange,
|
||||
"Dropping the fragment should be reflected in didChange"
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user