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 };