chrome-extension/provider-adapters.js
Rama main · réplica de lectura del repositorio original.
/* Shared by the isolated content script and service worker. No page code is evaluated. */
globalThis.NASDropProviders = (() => {
const id = '[A-Za-z0-9_-]+';
const xShareId = '[A-Za-z0-9_-]{6,64}';
function parse(value, base) {
try {
if (typeof value !== 'string' || /[\x00-\x20\x7f\\]/.test(value) || value.length > 8192) return null;
const url = new URL(value, base);
return url.protocol === 'https:' && !url.username && !url.password && !url.port ? url : null;
} catch { return null; }
}
function provider(value) {
const u = parse(value);
if (!u) return '';
if (/^[a-z0-9-]+\.gigafile\.nu$/.test(u.hostname)) return 'gigafile';
if (['gofile.io', 'www.gofile.io'].includes(u.hostname)) return 'gofile';
if (['pixeldrain.com', 'www.pixeldrain.com', 'pixeldrain.net', 'pixeldra.in'].includes(u.hostname)) return 'pixeldrain';
if (['buzzheavier.com', 'www.buzzheavier.com'].includes(u.hostname)) return 'buzzheavier';
if (['akirabox.to', 'akirabox.com'].includes(u.hostname)) return 'akirabox';
if (['vik1ngfile.site', 'vikingfile.com'].includes(u.hostname)) return 'vikingfile';
if (['send.now', 'www.send.now'].includes(u.hostname)) return 'sendnow';
if (u.hostname === 'x-share.net') return 'xshare';
return '';
}
function share(value) {
const u = parse(value);
const type = provider(value);
if (!u || !type) return '';
if (type === 'akirabox') return new RegExp(`^/${id}/file/?$`).test(u.pathname) ? u.origin + u.pathname.replace(/\/$/, '') : '';
if (type === 'vikingfile') return new RegExp(`^/f/${id}/?$`).test(u.pathname) ? u.origin + u.pathname.replace(/\/$/, '') : '';
if (type === 'sendnow') return new RegExp(`^/(?:d/)?${id}/?$`).test(u.pathname) && !u.search && !u.hash ? u.origin + u.pathname.replace(/\/$/, '') : '';
if (type === 'xshare') return new RegExp(`^/s/${xShareId}/?$`).test(u.pathname) && !u.search && !u.hash ? u.origin + u.pathname.replace(/\/$/, '') : '';
const prefix = type === 'gofile' ? '/d/' : type === 'pixeldrain' ? '/u/' : '/';
return new RegExp(`^${prefix}${id}/?$`).test(u.pathname) ? u.origin + u.pathname.replace(/\/$/, '') : '';
}
function signedBuzz(value, page) {
const u = parse(value), p = parse(page);
return Boolean(u && p && /^[a-z0-9-]+\.buzzheavier\.com$/.test(u.hostname)
&& u.pathname === `/d${p.pathname.replace(/\/$/, '')}`
&& [...u.searchParams.keys()].length === 1 && u.searchParams.has('v')
&& /^[A-Za-z0-9._~+-]+$/.test(u.searchParams.get('v')));
}
function buzzEndpoint(raw, page) {
const u = parse(raw, page), p = parse(page);
if (!u || !p || u.origin !== p.origin || u.pathname !== `${p.pathname.replace(/\/$/, '')}/download`) return '';
return u.href;
}
function resolve(element, page, sourcePage = '') {
const source = share(page) || share(sourcePage);
const type = provider(source || page);
if (!type || !source || !element) return null;
const a = name => element.getAttribute(name) || '';
const href = parse(a('href'), page);
if (type === 'sendnow') {
const current = parse(page);
if (element.matches('button#downloadbtn') && current?.origin === new URL(source).origin
&& current.pathname === '/' && !current.search && !current.hash) {
return {handoff:'sendnow', captureDownload:true, source};
}
}
if (type === 'xshare' && element.matches('button#dl-btn.btn.btn-primary')) {
return {handoff:'xshare', captureXShare:true, source};
}
if (type === 'akirabox' || type === 'vikingfile' || type === 'sendnow') {
const result = classifyHandoff(element, page, source);
if (result.kind === 'file') return {url:result.url, handoff:result.provider};
if (result.kind === 'expired') return {error:'expiredLink'};
return null;
}
if (type === 'buzzheavier') {
if (a('href') && signedBuzz(href?.href, page)) return {url: href.href};
const endpoint = buzzEndpoint(a('hx-get') || a('data-hx-get'), page);
if (element.matches('a.download-btn') && endpoint) return {endpoint};
// Read the literal argument only; never execute the inline handler.
const copy = a('onclick').match(/^\s*copyDownloadLink\(\s*(['"])(.*?)\1\s*\)\s*;?\s*$/);
if (element.matches('a.copy') && copy) {
const copiedEndpoint = buzzEndpoint(copy[2].replace(/\\\//g, '/'), page);
if (copiedEndpoint) return {endpoint: copiedEndpoint};
}
return null;
}
if (type === 'gigafile') {
// GigaFile's official controls use download(file, keyRequired, ...).
// Parse literal arguments only; never execute page JavaScript.
const call = (a('onclick') || a('href')).match(/^(?:javascript:)?\s*(?:return\s+)?download\(\s*(['"])([A-Za-z0-9_-]+)\1\s*,\s*(true|false)\s*,\s*(true|false)\s*\)\s*;?\s*$/);
const official = element.matches('button.download_panel_btn_dl.gfbtn, span#dl.download_file_caption');
if (call && official) return {url: `${new URL(page).origin}/${call[2]}`, gigafileKeyRequired:call[3] === 'true'};
if (a('href') && href?.origin === new URL(page).origin && ['/download.php', '/bypass_dl.php'].includes(href.pathname)) {
const file = href.searchParams.get('file');
if (file && new RegExp(`^${id}$`).test(file)) return {url: `${href.origin}/${file}`};
}
}
if (type === 'gofile' && a('data-action') === 'download') {
const row = element.closest('.fm-row[data-id]');
const file = row?.getAttribute('data-id');
if (file && new RegExp(`^${id}$`).test(file)) return {url: `https://gofile.io/d/${file}`};
return {url: source};
}
if (type === 'pixeldrain') {
if (a('href') && href && provider(href.href) === type && new RegExp(`^/api/file/${id}$`).test(href.pathname) && href.searchParams.has('download')) {
return {url: `${href.origin}/u/${href.pathname.split('/').pop()}`};
}
// Only the first-party file viewer's explicit download control; never text-only matches.
if (element.matches('button#download, button#download_button, button.download_button, a#download')) return {url: source};
// Current Svelte viewer: hashed classes vary between builds. Match the stable
// viewer region, button class and direct icon, including clicks on nested spans.
const viewerButton = (element.matches('button.toolbar_button') && element.closest('.toolbar'))
|| (element.matches('button') && element.closest('.description'));
if (viewerButton && element.querySelector(':scope > i.icon')?.textContent.trim() === 'download') return {url: source};
}
return null;
}
function allowedSubmission(page, value) {
const type = provider(page);
if (!share(page)) return false;
if (type === 'akirabox' || type === 'vikingfile' || type === 'sendnow' || type === 'xshare') return validHandoffURL(value, type, page);
if (type === 'buzzheavier') return signedBuzz(value, page);
return Boolean(share(value) && provider(value) === type && (type !== 'gigafile' || new URL(value).origin === new URL(page).origin));
}
function handoffURL(value, type, page = '') {
const u = parse(value);
if (!u || u.hash) return null;
if (type === 'sendnow') {
const source = share(page);
if (!source || u.href === source || u.href === `${source}/`) return null;
// Send.now may rotate its first-party/CDN download hosts, so do not pin a
// single observed hostname. Keep the browser-side check structural and
// leave DNS/redirect/content validation to the NAS server.
if (!u.hostname || u.hostname === 'localhost' || u.hostname.endsWith('.localhost')
|| u.hostname.endsWith('.local') || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(u.hostname)
|| u.hostname.includes(':')) return null;
if (['send.now','www.send.now'].includes(u.hostname) && !u.search && new RegExp(`^/(?:d/)?${id}/?$`).test(u.pathname)) return null;
return u;
}
if (type === 'xshare') {
const source = parse(share(page));
if (!source || u.origin !== source.origin || u.hash) return null;
const shareId = source.pathname.split('/').filter(Boolean)[1] || '';
if (!shareId || u.pathname !== `/api/download/${shareId}`) return null;
const keys = [...u.searchParams.keys()];
const key = u.searchParams.get('key') || '';
if (keys.length !== 1 || keys[0] !== 'key' || u.searchParams.getAll('key').length !== 1
|| key.length < 1 || key.length > 4096 || !/^[\x21-\x7e]+$/.test(key)) return null;
return u;
}
const parts = u.pathname.split('/');
if (parts.length !== 4 || !parts[2] || !parts[3]) return null;
let filename;
try { filename = decodeURIComponent(parts[3]); } catch { return null; }
if (!filename || filename === '.' || filename === '..' || /[\/\\\x00-\x1f\x7f]/.test(filename)) return null;
if (type === 'akirabox') {
if (u.hostname !== 'akirabox.com' || parts[1] !== 'download' || !/^[A-Za-z0-9_+=-]+$/.test(parts[2])) return null;
const keys = [...u.searchParams.keys()];
const legacy = keys.length === 2
&& u.searchParams.getAll('expiration').length === 1
&& u.searchParams.getAll('signature').length === 1;
const currentFields = ['expiration', 't', 's', 'b'];
const current = keys.length === currentFields.length
&& currentFields.every(key => u.searchParams.getAll(key).length === 1);
if (!legacy && !current) return null;
if (legacy) {
if (!/^\d{10,11}$/.test(u.searchParams.get('expiration'))
|| !/^[a-f0-9]{64}$/i.test(u.searchParams.get('signature'))) return null;
} else {
const expiration = u.searchParams.get('expiration');
const issued = u.searchParams.get('t');
if (!/^\d{10}$/.test(expiration) || !/^\d{10}$/.test(issued)
|| !/^[a-f0-9]{64}$/i.test(u.searchParams.get('s'))
|| !/^[a-f0-9]{64}$/i.test(u.searchParams.get('b'))) return null;
const expirationTime = Number(expiration);
const issuedTime = Number(issued);
if (issuedTime > Date.now()/1000 + 300 || issuedTime > expirationTime
|| expirationTime - issuedTime > 3600) return null;
}
} else if (type === 'vikingfile') {
if (u.hostname !== 'vikingfile.com' || parts[1] !== 'd' || !/^[A-Za-z0-9_-]+$/.test(parts[2]) || u.search) return null;
} else return null;
return u;
}
function validHandoffURL(value, type, page = '') {
const u = handoffURL(value,type,page);
return Boolean(u && (type !== 'akirabox' || Number(u.searchParams.get('expiration')) > Date.now()/1000));
}
// Structural classification only. The server must validate redirects and the
// real response: an opaque token alone cannot prove file identity or access.
function classifyHandoff(element, page, sourcePage = '') {
const source = share(page) || share(sourcePage);
const type = provider(source || page);
if (!source || !['akirabox','vikingfile','sendnow'].includes(type)) return {kind:'unrelated'};
const official = type === 'akirabox' ? element.matches('a#download.download-button')
: type === 'vikingfile' ? element.matches('a#download-link.button')
: element.matches('#direct_link a[href]') || element.matches('a#downloadbtn[href]');
if (!official) return {kind:'unrelated'};
if (type === 'akirabox' && element.getAttribute('aria-disabled') !== 'false') return {kind:'preparing'};
if (element.getAttribute('aria-disabled') === 'true' || element.hasAttribute?.('disabled')) return {kind:'preparing'};
const raw = element.getAttribute('href') || '';
if (!raw || raw === '#') return {kind:'preparing'};
const u = handoffURL(raw,type,source);
if (!u) return {kind:'unrecognized'};
if (!validHandoffURL(raw,type,source)) return {kind:'expired'};
return {kind:'file', provider:type, url:u.href};
}
async function resolveBuzzLink(endpoint, page, request = fetch) {
if (!buzzEndpoint(endpoint, page)) throw new Error('Invalid download request.');
const response = await request(endpoint, {
headers: {'HX-Request': 'true'}, credentials: 'same-origin',
redirect: 'error', cache: 'no-store', signal: AbortSignal.timeout(20000),
});
const link = response.headers.get('HX-Redirect');
if (!response.ok || !signedBuzz(link, page)) throw new Error('Buzzheavier did not provide a download link. Refresh the page and try again.');
// Never follow this URL here: only NASDrop may transfer the file body.
return link;
}
return {provider, share, signedBuzz, resolve, allowedSubmission, resolveBuzzLink, classifyHandoff, validHandoffURL};
})();