JM Tech Power

jm-tech-power-official/Flash Download

A desktop download manager that opens up to 32 connections against one file and can send each of them out a different proxy route.

Public Uncategorized
Clone jm-tech-power-official/flash-download.jmproj
src/main/engine/net.js
'use strict';

const http = require('http');
const https = require('https');
const { agentFor } = require('./agents');

const UA = 'FlashDownload/0.1';

class NetError extends Error {
  constructor(kind, message, status) {
    super(message);
    this.name = 'NetError';
    this.kind = kind;     // timeout | refused | reset | dns | proxy | http | redirect
    this.status = status; // HTTP status when kind === 'http'
  }
  // A route-level fault means "try this range on a different route",
  // as opposed to a fault with the file or the origin server.
  get routeFault() {
    if (this.kind === 'timeout' || this.kind === 'refused' ||
        this.kind === 'reset' || this.kind === 'proxy') return true;
    return this.kind === 'http' && (this.status === 403 || this.status === 407 || this.status === 429);
  }
}

function classify(err) {
  if (err instanceof NetError) return err;
  const code = err && err.code;
  if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT' || err.name === 'AbortError') {
    return new NetError('timeout', 'Connection timed out');
  }
  if (code === 'ECONNREFUSED') return new NetError('refused', 'Connection refused');
  if (code === 'ECONNRESET' || code === 'EPIPE') return new NetError('reset', 'Connection reset');
  if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return new NetError('dns', 'Host not found');
  if (/socks|proxy/i.test(err.message || '')) return new NetError('proxy', err.message);
  return new NetError('other', err.message || String(err));
}

/**
 * Issue a request, following redirects, through `route`.
 * Resolves with the live response stream — the caller consumes or destroys it.
 */
async function open(startUrl, { method = 'GET', headers = {}, route = null, signal, timeoutMs = 20000, maxRedirects = 5 } = {}) {
  let url = startUrl;

  for (let hop = 0; hop <= maxRedirects; hop++) {
    const target = new URL(url);
    const lib = target.protocol === 'https:' ? https : http;
    const agent = agentFor(route, target);

    const res = await new Promise((resolve, reject) => {
      let req;
      try {
        req = lib.request(target, {
          method,
          headers: { 'user-agent': UA, accept: '*/*', ...headers },
          agent,
          signal,
        }, resolve);
      } catch (err) { return reject(classify(err)); }

      req.setTimeout(timeoutMs, () => {
        req.destroy(new NetError('timeout', `No response in ${timeoutMs / 1000}s`));
      });
      req.on('error', (err) => reject(classify(err)));
      req.end();
    });

    const { statusCode, headers: h } = res;

    if (statusCode >= 300 && statusCode < 400 && h.location) {
      res.resume(); // discard the redirect body
      url = new URL(h.location, url).toString();
      continue;
    }

    if (statusCode >= 400) {
      res.resume();
      throw new NetError('http', `Server returned ${statusCode}`, statusCode);
    }

    return { res, finalUrl: url, status: statusCode, headers: h };
  }

  throw new NetError('redirect', `More than ${maxRedirects} redirects`);
}

/**
 * Ask the server what we are dealing with: size, range support, identity.
 * Uses a 1-byte ranged GET — more servers answer it honestly than answer HEAD.
 */
async function probe(url, { route = null, signal } = {}) {
  let info;
  try {
    info = await open(url, { headers: { range: 'bytes=0-0' }, route, signal });
  } catch (err) {
    if (err instanceof NetError && err.kind === 'http' && err.status === 416) {
      info = await open(url, { route, signal }); // zero-length file
    } else {
      throw err;
    }
  }

  const { res, finalUrl, status, headers } = info;
  res.destroy(); // we only wanted the headers

  let size = null;
  let acceptsRanges = false;

  const contentRange = headers['content-range'];
  if (status === 206 && contentRange) {
    const m = /bytes\s+\d+-\d+\/(\d+|\*)/i.exec(contentRange);
    if (m && m[1] !== '*') size = Number(m[1]);
    acceptsRanges = true;
  } else {
    if (headers['content-length'] != null) size = Number(headers['content-length']);
    acceptsRanges = /bytes/i.test(headers['accept-ranges'] || '');
  }

  // A server that ignores Range and sends the whole body cannot be split.
  if (status === 200 && headers['accept-ranges'] !== 'bytes') acceptsRanges = false;

  return {
    finalUrl,
    size: Number.isFinite(size) && size > 0 ? size : null,
    acceptsRanges,
    etag: headers.etag || null,
    lastModified: headers['last-modified'] || null,
    contentType: headers['content-type'] || null,
    filename: filenameFor(headers['content-disposition'], finalUrl),
  };
}

function filenameFor(disposition, url) {
  if (disposition) {
    const star = /filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i.exec(disposition);
    if (star) {
      try { return sanitize(decodeURIComponent(star[1].trim().replace(/^"|"$/g, ''))); } catch { /* fall through */ }
    }
    const plain = /filename\s*=\s*"?([^";]+)"?/i.exec(disposition);
    if (plain) return sanitize(plain[1].trim());
  }
  try {
    const name = decodeURIComponent(new URL(url).pathname.split('/').filter(Boolean).pop() || '');
    if (name) return sanitize(name);
  } catch { /* fall through */ }
  return 'download';
}

function sanitize(name) {
  return name.replace(/[/\\?%*:|"<>\x00-\x1f]/g, '_').slice(0, 180) || 'download';
}

module.exports = { open, probe, NetError, classify, sanitize };