Ctrl K
ComunidadGuías…

chrome-extension/provider-adapters.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* 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};
})();