UNPKG

massping

Version:

Mass send http requests for test web application

348 lines (303 loc) 10.8 kB
/** * @file app/core.js * @description massping core */ import { EventEmitter } from 'events' import fs from 'fs'; import net from 'net'; import dns from 'dns'; import got from 'got'; import FormData from 'form-data'; import { Mime } from 'mime/lite'; import standardTypes from 'mime/types/standard.js'; import otherTypes from 'mime/types/other.js'; import UserAgent from 'user-agents'; import { RotatingArray, LinkedList } from '../lib/collection.js' import SBL from '../lib/sbl.js' import { choose, rand, seq } from '../lib/generator.js'; import helper from '../lib/helper.js'; import TrafficStatsAgent from '../lib/traffic-stats-agent.js' import { loadFromString, toHeaderString } from '../lib/cookies.js'; const mime = new Mime(standardTypes, otherTypes); mime.define({ 'application/x-www-form-urlencoded': ['urlencoded'] }); mime.define({ 'multipart/form-data': ['form', 'form-data'] }); export default class Core { static defaultConfig = { concurrent: 16, delay: [1, 5], unit: [1, 1], header: [], cookies: '', body: '', // TODO bodyBinary: '', // TODO form: '', // TODO method: 'GET', referer: 'root', quality: [], // TODO proxy: '', silent: false, logLevel: 'notice', ip: [], http2: false, // TODO tag: '{...}', maxSize: 65536, shutdown: 0, // TODO } static defaultHeaders = { "upgrade-insecure-requests": "1", 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-dest': 'document', 'sec-fetch-user': '?1', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'user-agent': 'curl/7.8.0', } /** * * @param {typeof defaultConfig} config * @param {string[]} target */ constructor(config, target = 'http://httpbin.org/delay/10?id={1:}') { /** @type {typeof Core.defaultConfig} */ this.config = { ...Core.defaultConfig, ...config, } this.target = target this.eventEmitter = new EventEmitter() this.sbl = new SBL(this.config.tag.split('...')) this.alive = new LinkedList() this.running = false this.nextDelay = rand(this.config.delay[0] * 1000, this.config.delay[1] * 1000,) this.nextUnit = rand(...this.config.unit) this.nextID = seq(1) this.trafficStats = { tx: 0, rx: 0, req: 0, res: 0, } this.agent = TrafficStatsAgent({ keepAlive: true }, this.trafficStats, this.config.proxy) } async submit() { let { url, header, cookie, ip, body } = this.sbl.execute() let ua = new UserAgent().toString() let headers = { ...Core.defaultHeaders, // ...helper.buildClientHints(ua), 'user-agent': ua, ...helper.headersStringToObject(header), } switch (this.config.referer) { case 'root': headers['referer'] = new URL('/', url).toString() break; case 'same': headers['referer'] = url break; case 'none': break; default: headers['referer'] = this.config.referer } // randomized X-Forwarded-For and X-Real-IP address if (ip) { headers['X-Forwarded-For'] = ip headers['X-Real-IP'] = ip } if (cookie) { headers['cookie'] = toHeaderString(loadFromString(cookie)) } if (body) { headers['content-type'] = mime.getType(this.config.body) || 'text/plain' headers['content-length'] = body.length } let bodySummary = body.slice(0, 32) if (this.config.bodyBinary) { // body = fs.createReadStream(this.config.bodyBinary) body = fs.readFileSync(this.config.bodyBinary) headers['content-type'] = mime.getType(this.config.bodyBinary) || 'application/octet-stream' headers['content-length'] = body.length bodySummary = `[Binary:${this.config.bodyBinary}]` } // let sym = Symbol(url) let id = this.nextID().value let reqInfo = { id, url, headers, bodySummary, } const node = this.alive.append(id) this.emit('submit', reqInfo) const controller = new AbortController(); try { const req = got(url, { method: this.config.method, headers, body: body || undefined, isStream: true, responseType: 'buffer', throwHttpErrors: false, signal: controller.signal, // 绑定取消信号, agent: this.agent, dnsLookup:makeDNSLookuper(this.config.ip) }) this.trafficStats.req++; let maxSize = this.config.maxSize || 65536 let buff = Buffer.alloc(maxSize) let bi = 0 req.on('data', data => { const rem = maxSize - bi; // 计算剩余空间 if (data.length <= rem) { // 当前数据块可完全放入缓冲区 buff.set(data, bi); bi += data.length; } else { // 只取能填满缓冲区的部分 buff.set(data.subarray(0, rem), bi); bi = maxSize; // 标记缓冲区已满 } if (bi >= maxSize) { controller.abort(); req.destroy() req.emit('end') } }) req.on('end', () => { if (bi < maxSize) { buff = buff.subarray(0, bi); } this.alive.remove(node) // fix got lib `req.timings.phases.total` is undefined let phases = req.timings.phases.total || (Date.now() - req.timings.start) let response = req.response let result = { id, url: req.requestUrl, code: response.statusCode, headers: response.headers, phases, bodySummary: buff, } this.trafficStats.res++; // this.history.push(result) this.emit('result', result) }) req.on('error', error => { if (error.name !== 'AbortError') { this.alive.remove(node) this.emit('error', error, reqInfo) } }) } catch (error) { this.alive.remove(node) this.emit('error', error, reqInfo) } // this.emit('res', u) } async init() { try { this.sbl.load(this.target, 'url') this.sbl.load('{1-254}.{1-254}.{1-254}.{1-254}', 'ip') this.sbl.load(this.config.header.join('\n'), 'header') if (this.config.cookies) { // TODO: if extname == 'json' let data = fs.readFileSync(this.config.cookies, 'utf-8') this.sbl.load(data, 'cookie') } else { this.sbl.load('', 'cookie') } // this.sbl.load('', 'form') if (this.config.body) { let data = fs.readFileSync(this.config.body, 'utf-8') this.sbl.load(data, 'body') } else { this.sbl.load('', 'body') } this.sbl.ready() this.emit('ready') } catch (e) { console.log(e); console.log('massping: init fial'); process.exit(1) } } async start() { if (this.running) return; this.running = true this.emit('start') while (this.running) { await this.tick() } } async tick() { let currentUnit = this.nextUnit().value for (let i = 0; i < currentUnit && this.alive.length < this.config.concurrent; i++) { this.submit() } this.emit('tick') await helper.sleep(this.nextDelay().value) } on(event, listener) { this.eventEmitter.on(event, listener); return this; } once(event, listener) { this.eventEmitter.once(event, listener) return this; } off(event, listener) { this.eventEmitter.off(event, listener) return this; } emit(event, ...args) { this.eventEmitter.emit(event, ...args); return this; } } function makeDNSLookuper(ips = []) { if (ips.length) { let nextip = choose(ips, true) return function myDNSLookup(host, options, callback) { // 标准化参数处理 if (typeof options === 'function') { callback = options; options = {}; } else if (typeof options === 'number') { options = { family: options }; } // 设置默认值 options = { family: options.family || 0, hints: options.hints || 0, all: options.all || false, ...options }; const ip = nextip().value const family = net.isIP(ip); if (family === 0) { // 无效IP return callback(new Error(`Invalid IP address: ${ip}`)); } let address = ip; let resultFamily = family; // 处理V4MAPPED转换 if (options.family === 6 && family === 4 && (options.hints & dns.V4MAPPED)) { address = `::ffff:${ip}`; resultFamily = 6; } // 检查ADDRCONFIG配置 if ((options.hints & dns.ADDRCONFIG) && !hasAddressFamily(resultFamily)) { return handleNotFound(host, options, callback); } // 返回结果 if (options.all) { callback(null, [{ address, family: resultFamily }]); } else { callback(null, address, resultFamily); } } } else { return dns.lookup } }