From 4b7c2e091c8dc4317842542cba86bead8da6098a Mon Sep 17 00:00:00 2001 From: Micooz Date: Thu, 15 Feb 2018 11:01:24 +0800 Subject: [PATCH] src,test: isolate config among presets --- src/core/__tests__/middleware.test.js | 8 +- src/core/config.js | 63 ++- src/core/hub.js | 11 +- src/core/middleware.js | 56 +-- src/core/pipe.js | 16 +- src/core/relay.js | 9 +- src/presets/access-control.js | 458 ------------------ src/presets/aead-random-cipher.js | 44 +- src/presets/auto-conf.js | 16 +- src/presets/base-auth.js | 35 +- src/presets/defs.js | 89 ++-- src/presets/index.js | 28 -- src/presets/mux.js | 3 +- src/presets/obfs-http.js | 16 +- src/presets/obfs-random-padding.js | 3 +- src/presets/obfs-tls1.2-ticket.js | 11 +- src/presets/ss-aead-cipher.js | 64 ++- src/presets/ss-base.js | 2 +- src/presets/ss-stream-cipher.js | 23 +- src/presets/ssr-auth-aes128-md5.js | 4 +- src/presets/ssr-auth-aes128-sha1.js | 4 +- src/presets/ssr-auth-aes128.js | 22 +- src/presets/ssr-auth-chain-a.js | 4 +- src/presets/ssr-auth-chain-b.js | 4 +- src/presets/ssr-auth-chain.js | 43 +- src/presets/tracker.js | 2 +- src/presets/v2ray-vmess.js | 62 +-- src/transports/defs.js | 5 +- test/common/preset-runner.js | 12 +- .../__snapshots__/access-control.test.js.snap | 41 -- test/presets/access-control.test.js | 40 -- test/presets/defs.test.js | 7 +- test/presets/index.test.js | 2 +- test/presets/v2ray-vmess.test.js | 51 -- 34 files changed, 290 insertions(+), 968 deletions(-) delete mode 100644 src/presets/access-control.js delete mode 100644 test/presets/__snapshots__/access-control.test.js.snap delete mode 100644 test/presets/access-control.test.js delete mode 100644 test/presets/v2ray-vmess.test.js diff --git a/src/core/__tests__/middleware.test.js b/src/core/__tests__/middleware.test.js index 2cb303f..e8cda82 100644 --- a/src/core/__tests__/middleware.test.js +++ b/src/core/__tests__/middleware.test.js @@ -1,16 +1,16 @@ import {Middleware} from '../middleware'; test('Middleware#constructor', () => { - expect(() => new Middleware({'name': 'unknown-preset'})).toThrow(); + expect(() => new Middleware({preset: {'name': 'unknown-preset'}})).toThrow(); }); test('Middleware#hasListener', () => { - const middleware = new Middleware({'name': 'ss-base'}); + const middleware = new Middleware({preset: {'name': 'ss-base'}}); expect(middleware.hasListener('event')).toBe(false); }); test('Middleware#onPresetNext', () => { - const middleware = new Middleware({'name': 'ss-base'}); + const middleware = new Middleware({preset: {'name': 'ss-base'}}); middleware.on('next_1', (arg) => { expect(arg).toBe(null); }); @@ -18,6 +18,6 @@ test('Middleware#onPresetNext', () => { }); test('Middleware#getImplement', () => { - const middleware = new Middleware({'name': 'ss-base'}); + const middleware = new Middleware({preset: {'name': 'ss-base'}}); expect(middleware.getImplement()).toBeDefined(); }); diff --git a/src/core/config.js b/src/core/config.js index f4ff770..c754998 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -7,25 +7,26 @@ import url from 'url'; import qs from 'qs'; import winston from 'winston'; import isPlainObject from 'lodash.isplainobject'; -import { getPresetClassByName, IPresetAddressing } from '../presets'; -import { DNSCache, isValidHostname, isValidPort, logger, DNS_DEFAULT_EXPIRE } from '../utils'; +import {getPresetClassByName, IPresetAddressing} from '../presets'; +import {DNSCache, isValidHostname, isValidPort, logger, DNS_DEFAULT_EXPIRE} from '../utils'; function loadFileSync(file) { return fs.readFileSync(path.resolve(process.cwd(), file)); } export class Config { + local_protocol = null; local_host = null; local_port = null; + forward_host = null; + forward_port = null; + servers = null; is_client = null; is_server = null; - forward_host = null; - forward_port = null; - timeout = null; redirect = null; workers = null; @@ -50,8 +51,12 @@ export class Config { log_level = null; log_max_days = null; + // an isolate space where presets can store something in. + // store[i] is for presets[i] + stores = []; + constructor(json) { - const { protocol, hostname, port, query } = url.parse(json.service); + const {protocol, hostname, port, query} = url.parse(json.service); this.local_protocol = protocol.slice(0, -1); this.local_host = hostname; this.local_port = +port; @@ -72,8 +77,8 @@ export class Config { } if (this.is_client && this.local_protocol === 'tcp') { - const { forward } = qs.parse(query); - const { hostname, port } = url.parse('tcp://' + forward); + const {forward} = qs.parse(query); + const {hostname, port} = url.parse('tcp://' + forward); this.forward_host = hostname; this.forward_port = +port; } @@ -95,7 +100,7 @@ export class Config { initServer(server) { // service - const { protocol, hostname, port } = url.parse(server.service); + const {protocol, hostname, port} = url.parse(server.service); this.transport = protocol.slice(0, -1); this.server_host = hostname; this.server_port = +port; @@ -123,17 +128,21 @@ export class Config { // remove unnecessary presets if (this.mux) { this.presets = this.presets.filter( - ({ name }) => !IPresetAddressing.isPrototypeOf(getPresetClassByName(name)) + ({name}) => !IPresetAddressing.isPrototypeOf(getPresetClassByName(name)) ); } - // pre-init presets - for (const { name, params = {} } of server.presets) { + // pre-cache presets + this.stores = (new Array(this.presets.length)).fill({}); + for (let i = 0; i < server.presets.length; i++) { + const {name, params = {}} = server.presets[i]; const clazz = getPresetClassByName(name); - clazz.checked = false; - clazz.checkParams(params); - clazz.initialized = false; - clazz.onInit(params); + const data = clazz.onCache(params); + if (data instanceof Promise) { + data.then((d) => this.stores[i] = d); + } else { + this.stores[i] = clazz.onCache(params); + } } } @@ -188,7 +197,7 @@ export class Config { throw Error('"service" must be provided as "://:[?params]"'); } - const { protocol: _protocol, hostname, port, query } = url.parse(json.service); + const {protocol: _protocol, hostname, port, query} = url.parse(json.service); // service.protocol if (typeof _protocol !== 'string') { @@ -216,14 +225,14 @@ export class Config { // service.query if (protocol === 'tcp') { - const { forward } = qs.parse(query); + const {forward} = qs.parse(query); // ?forward if (!forward) { throw Error('require "?forward=:" parameter in service when using "tcp" on client side'); } - const { hostname, port } = url.parse('tcp://' + forward); + const {hostname, port} = url.parse('tcp://' + forward); if (!isValidHostname(hostname)) { throw Error('service.?forward.host is invalid'); } @@ -278,7 +287,7 @@ export class Config { throw Error('"service" must be provided as "://:[?params]"'); } - const { protocol: _protocol, hostname, port } = url.parse(server.service); + const {protocol: _protocol, hostname, port} = url.parse(server.service); // service.protocol if (typeof _protocol !== 'string') { @@ -343,7 +352,7 @@ export class Config { // presets[].parameters for (const preset of server.presets) { - const { name, params } = preset; + const {name, params} = preset; if (typeof name !== 'string') { throw Error('"server.presets[].name" must be a string'); } @@ -354,6 +363,8 @@ export class Config { if (!isPlainObject(params)) { throw Error('"server.presets[].params" must be an plain object'); } + const clazz = getPresetClassByName(name); + clazz.onCheckParams(params); } } } @@ -361,7 +372,7 @@ export class Config { static _testCommon(common) { // timeout if (common.timeout !== undefined) { - const { timeout } = common; + const {timeout} = common; if (typeof timeout !== 'number') { throw Error('"timeout" must be a number'); } @@ -390,7 +401,7 @@ export class Config { // log_max_days if (common.log_max_days !== undefined) { - const { log_max_days } = common; + const {log_max_days} = common; if (typeof log_max_days !== 'number') { throw Error('"log_max_days" must a number'); } @@ -401,7 +412,7 @@ export class Config { // workers if (common.workers !== undefined) { - const { workers } = common; + const {workers} = common; if (typeof workers !== 'number') { throw Error('"workers" must be a number'); } @@ -415,7 +426,7 @@ export class Config { // dns if (common.dns !== undefined) { - const { dns } = common; + const {dns} = common; if (!Array.isArray(dns)) { throw Error('"dns" must be an array'); } @@ -428,7 +439,7 @@ export class Config { // dns_expire if (common.dns_expire !== undefined) { - const { dns_expire } = common; + const {dns_expire} = common; if (typeof dns_expire !== 'number') { throw Error('"dns_expire" must be a number'); } diff --git a/src/core/hub.js b/src/core/hub.js index 0888e7b..9047bf2 100644 --- a/src/core/hub.js +++ b/src/core/hub.js @@ -315,26 +315,27 @@ export class Hub { _createRelay(context, isMux = false) { const props = { + config: this._config, context: context, transport: this._config.transport, presets: this._config.presets }; if (isMux) { - return new MuxRelay(props, this._config); + return new MuxRelay(props); } if (this._config.mux) { if (this._config.is_client) { - return new Relay({...props, transport: 'mux', presets: []}, this._config); + return new Relay({...props, transport: 'mux', presets: []}); } else { - return new MuxRelay(props, this._config); + return new MuxRelay(props); } } else { - return new Relay(props, this._config); + return new Relay(props); } } _createUdpRelay(context) { - return new Relay({transport: 'udp', context, presets: this._config.udp_presets}, this._config); + return new Relay({config: this._config, transport: 'udp', context, presets: this._config.udp_presets}); } _selectMuxRelay() { diff --git a/src/core/middleware.js b/src/core/middleware.js index 30dd431..b38eee3 100644 --- a/src/core/middleware.js +++ b/src/core/middleware.js @@ -1,54 +1,37 @@ import EventEmitter from 'events'; -import {getPresetClassByName, IPresetStatic} from '../presets'; +import {getPresetClassByName} from '../presets'; import {PIPE_ENCODE} from '../constants'; import {kebabCase} from '../utils'; -const staticPresetCache = new Map(/* 'ClassName': */); - -function createPreset(name, params = {}, config) { +function createPreset({config, preset}) { + const name = preset.name; + const params = preset.params || {}; const ImplClass = getPresetClassByName(name); - const createOne = () => { - ImplClass.config = config; - ImplClass.checkParams(params); - ImplClass.onInit(params); - return new ImplClass(params); - }; - let preset = null; - if (IPresetStatic.isPrototypeOf(ImplClass)) { - // only create one instance for IPresetStatic - preset = staticPresetCache.get(ImplClass.name); - if (preset === undefined) { - preset = createOne(); - staticPresetCache.set(ImplClass.name, preset); - } - } else { - preset = createOne(); - } - return preset; + const instance = new ImplClass({config, params}); + instance.onInit(params); + return instance; } -/** - * abstraction of middleware - */ export class Middleware extends EventEmitter { - _impl = null; _config = null; - constructor(preset, config) { + _impl = null; + + constructor({config, preset}) { super(); - this._config = config; this.onPresetNext = this.onPresetNext.bind(this); this.onPresetBroadcast = this.onPresetBroadcast.bind(this); this.onPresetFail = this.onPresetFail.bind(this); - this._impl = createPreset(preset.name, preset.params || {}, this._config); + this._config = config; + this._impl = createPreset({config, preset}); this._impl.next = this.onPresetNext; this._impl.broadcast = this.onPresetBroadcast; this._impl.fail = this.onPresetFail; } get name() { - return this._impl.getName() || kebabCase(this._impl.constructor.name).replace(/(.*)-preset/i, '$1'); + return kebabCase(this._impl.constructor.name).replace(/(.*)-preset/i, '$1'); } getImplement() { @@ -76,21 +59,10 @@ export class Middleware extends EventEmitter { } onDestroy() { - // prevent destroy on static preset - if (!(this._impl instanceof IPresetStatic)) { - this._impl.onDestroy(); - } + this._impl.onDestroy(); this.removeAllListeners(); } - /** - * call hook functions of implement in order - * @param direction - * @param buffer - * @param direct - * @param isUdp - * @param extraArgs - */ write({direction, buffer, direct, isUdp}, extraArgs) { const type = (direction === PIPE_ENCODE ? 'Out' : 'In') + (isUdp ? 'Udp' : ''); diff --git a/src/core/pipe.js b/src/core/pipe.js index 3da19ec..aeb7c9f 100644 --- a/src/core/pipe.js +++ b/src/core/pipe.js @@ -20,7 +20,7 @@ export class Pipe extends EventEmitter { _destroyed = false; _presets = null; - + _config = null; get destroyed() { @@ -67,7 +67,7 @@ export class Pipe extends EventEmitter { } createMiddlewares(presets) { - const middlewares = presets.map((preset) => this._createMiddleware(preset)); + const middlewares = presets.map((preset, i) => this._createMiddleware(preset, i)); this._upstream_middlewares = middlewares; this._downstream_middlewares = [].concat(middlewares).reverse(); this._presets = presets; @@ -89,7 +89,8 @@ export class Pipe extends EventEmitter { } // create non-exist middleware and reuse exist one const middlewares = []; - for (const preset of presets) { + for (let i = 0; i < presets.length; i++) { + const preset = presets[i]; let md = mdIndex[preset.name]; if (md) { // remove all listeners for later re-chain later in _feed() @@ -98,7 +99,7 @@ export class Pipe extends EventEmitter { this._attachEvents(md); delete mdIndex[preset.name]; } else { - md = this._createMiddleware(preset); + md = this._createMiddleware(preset, i); } middlewares.push(md); } @@ -142,12 +143,13 @@ export class Pipe extends EventEmitter { this.removeAllListeners(); } - _createMiddleware(preset) { - const middleware = new Middleware(preset, this._config); + _createMiddleware(preset, index) { + const middleware = new Middleware({config: this._config, preset}); this._attachEvents(middleware); - // set readProperty() + // set readProperty() and getStore() const impl = middleware.getImplement(); impl.readProperty = (...args) => this.onReadProperty(middleware.name, ...args); + impl.getStore = () => this._config.stores[index]; return middleware; } diff --git a/src/core/relay.js b/src/core/relay.js index 87a4022..12617a7 100644 --- a/src/core/relay.js +++ b/src/core/relay.js @@ -65,14 +65,14 @@ export class Relay extends EventEmitter { this._ctx.cid = id; } - constructor({transport, context, presets = []}, config) { + constructor({config, transport, context, presets = []}) { super(); - this._config = config; this.updatePresets = this.updatePresets.bind(this); this.onBroadcast = this.onBroadcast.bind(this); this.onEncoded = this.onEncoded.bind(this); this.onDecoded = this.onDecoded.bind(this); this._id = Relay.idcounter++; + this._config = config; this._transport = transport; this._remoteInfo = context.remoteInfo; // pipe @@ -85,8 +85,9 @@ export class Relay extends EventEmitter { }; // bounds const {Inbound, Outbound} = this.getBounds(transport); - const inbound = new Inbound({context: this._ctx, globalContext: this._config}); - const outbound = new Outbound({context: this._ctx, globalContext: this._config}); + const props = {config, context: this._ctx}; + const inbound = new Inbound(props); + const outbound = new Outbound(props); this._inbound = inbound; this._outbound = outbound; // outbound diff --git a/src/presets/access-control.js b/src/presets/access-control.js deleted file mode 100644 index 39f377f..0000000 --- a/src/presets/access-control.js +++ /dev/null @@ -1,458 +0,0 @@ -import fs from 'fs'; -import os from 'os'; -import net from 'net'; -import path from 'path'; -import readline from 'readline'; -import ip from 'ip'; -import { - IPreset, - CONNECTION_CREATED, - CONNECT_TO_REMOTE, - PRESET_FAILED, - PRESET_CLOSE_CONNECTION, - PRESET_PAUSE_RECV, - PRESET_PAUSE_SEND, - PRESET_RESUME_RECV, - PRESET_RESUME_SEND -} from './defs'; -import {logger, isValidHostname, isValidPort} from '../utils'; - -let rules = []; - -let cachedRules = { - // : -}; - -// rule's methods - -function ruleIsMatch(host, port) { - const {host: rHost, port: rPort} = this; - const slashIndex = rHost.indexOf('/'); - - let isHostMatch = false; - if (slashIndex !== -1) { - isHostMatch = ip.cidrSubnet(rHost).contains(host); - } else { - isHostMatch = (rHost === host); - } - - if (rHost === '*' || isHostMatch) { - if (rPort === '*' || port === rPort) { - return true; - } - } - return false; -} - -function ruleToString() { - return `${this.host}:${this.port} ${this.isBan ? 1 : 0} ${this.upLimit} ${this.dlLimit}`; -} - -// rule parsing - -function parseHost(host) { - const slashIndex = host.indexOf('/'); - if (slashIndex < 0) { - if (host !== '*' && !net.isIP(host) && !isValidHostname(host)) { - return null; - } - return host; - } - if (slashIndex < 7) { - return null; - } - const parts = host.split('/'); - const ip = parts[0]; - const mask = parts[parts.length - 1]; - if (!net.isIP(ip)) { - return null; - } - if (mask === '' || !Number.isInteger(+mask) || +mask < 0 || +mask > 32) { - return null; - } - return host; -} - -function parseSpeed(speed) { - const regex = /^(\d+)(b|k|kb|m|mb|g|gb)$/g; - const results = regex.exec(speed.toLowerCase()); - if (results !== null) { - const [, num, unit] = results; - return +num * { - 'b': 1, - 'k': 1024, - 'kb': 1024, - 'm': 1048576, - 'mb': 1048576, - 'g': 1073741824, - 'gb': 1073741824 - }[unit]; - } - return null; -} - -function parseLine(line) { - if (line.length > 300) { - return null; - } - line = line.trim(); - if (line.length < 1) { - return null; - } - if (line[0] === '#') { - return null; - } - const [addr, ban, up, dl] = line.split(' ').filter(p => p.length > 0); - - let _host = null; - let _port = null; - let _isBan = false; - let _upLimit = '-'; - let _dlLimit = '-'; - - // [addr[/mask][:port]] - if (addr.indexOf(':') > 0) { - const parts = addr.split(':'); - const host = parts[0]; - const port = parts[parts.length - 1]; - _host = parseHost(host); - if (port !== '*') { - if (!isValidPort(+port)) { - return null; - } - _port = +port; - } else { - _port = port; - } - } else { - _host = parseHost(addr); - _port = '*'; - } - - if (_host === null) { - return null; - } - - // [ban] - if (ban !== undefined) { - if (ban !== '0' && ban !== '1') { - return null; - } - _isBan = ban !== '0'; - } - - // [max_upload_speed(/s)] - if (up !== undefined && up !== '-') { - _upLimit = parseSpeed(up); - if (!_upLimit) { - return null; - } - } - - // [max_download_speed(/s)] - if (dl !== undefined && dl !== '-') { - _dlLimit = parseSpeed(dl); - if (!_dlLimit) { - return null; - } - } - - return { - host: _host, - port: _port, - isBan: _isBan, - upLimit: _upLimit, - dlLimit: _dlLimit, - isMatch: ruleIsMatch, - toString: ruleToString - }; -} - -// helpers - -function reloadRules(aclPath) { - logger.verbose('[acl] (re)loading access list'); - const rs = fs.createReadStream(aclPath, {encoding: 'utf-8'}); - rs.on('error', (err) => { - logger.warn(`[acl] fail to reload acl: ${err.message}, keep using previous rules`); - }); - const rl = readline.createInterface({input: rs}); - const _rules = []; - rl.on('line', (line) => { - const rule = parseLine(line); - if (rule !== null) { - _rules.push(rule); - } - }); - rl.on('close', () => { - rules = _rules.reverse(); - cachedRules = {}; - logger.info(`[acl] ${rules.length} rules loaded`); - }); -} - -function findRule(host, port) { - const cacheKey = `${host}:${port}`; - const cacheRule = cachedRules[cacheKey]; - if (cacheRule !== undefined) { - return cacheRule; - } else { - for (const rule of rules) { - if (rule.isMatch(host, port)) { - return cachedRules[cacheKey] = rule; - } - } - // rule not found - return cachedRules[cacheKey] = null; - } -} - -const DEFAULT_MAX_TRIES = 60; -const tries = { - // : -}; - -/** - * @description - * Apply access control to each connection. - * - * @notice - * This preset can ONLY be used on server side. - * - * @params - * acl: A path to a text file which contains a list of rules in order. - * max_tries(optional): The maximum tries from client, default is 60. - * - * @examples - * { - * "name": "access-control", - * "params": { - * "acl": "acl.txt", - * "max_tries": 60 - * } - * } - * - * // acl.txt - * # [addr[/mask][:port]] [ban] [max_upload_speed(/s)] [max_download_speed(/s)] - * - * example.com 1 # prevent access to example.com - * example.com:* 1 # prevent access to example.com:*, equal to above - * example.com:443 1 # prevent access to example.com:443 only - * *:25 1 # prevent access to SMTP servers - * *:* 1 # prevent all access from/to all endpoints - * 127.0.0.1 1 # ban localhost - * 192.168.0.0/16 1 # ban hosts in 192.168.*.* - * 172.27.1.100 0 120K # limit upload speed to 120KB/s - * 172.27.1.100 0 - 120K # limit download speed to 120KB/s - * 172.27.1.100 0 120K 120K # limit upload and download speed to 120KB/s - */ -export default class AccessControlPreset extends IPreset { - - // params(readonly) - - _aclPath = ''; - - _maxTries = 0; - - // members - - _hrTimeBegin = process.hrtime(); - - _remoteHost = null; - - _remotePort = null; - - _dstHost = null; - - _dstPort = null; - - _totalOut = 0; - - _totalIn = 0; - - // flags - - _isBlocking = false; - - _isDlPaused = false; - - _isUpPaused = false; - - static checkParams({acl, max_tries = DEFAULT_MAX_TRIES}) { - if (typeof acl !== 'string' || acl === '') { - throw Error('\'acl\' must be a non-empty string'); - } - const aclPath = path.resolve(process.cwd(), acl); - if (!fs.existsSync(aclPath)) { - throw Error(`"${aclPath}" not found`); - } - if (max_tries !== undefined) { - if (typeof max_tries !== 'number' || !Number.isInteger(max_tries)) { - throw Error('\'max_tries\' must be an integer'); - } - if (max_tries < 1) { - throw Error('\'max_tries\' must be greater than 0'); - } - } - } - - static onInit({acl}) { - const aclPath = path.resolve(process.cwd(), acl); - // note: should load rules once server up - reloadRules(aclPath); - if (process.env.NODE_ENV !== 'test') { - fs.watchFile(aclPath, (curr, prev) => { - if (curr.mtime > prev.mtime) { - reloadRules(aclPath); - } - }); - } - } - - constructor({acl, max_tries = DEFAULT_MAX_TRIES}) { - super(); - this._aclPath = path.resolve(process.cwd(), acl); - this._maxTries = max_tries; - } - - applyRule(rule) { - const {host, port, isBan, upLimit, dlLimit} = rule; - logger.debug(`[acl] [${this._remoteHost}:${this._remotePort}] apply rule: "${rule}"`); - - // ban - if (isBan) { - logger.info(`[acl] [${host}:${port}] baned by rule: "${rule}"`); - this.broadcast({type: PRESET_CLOSE_CONNECTION}); - this._isBlocking = true; - } - - // max_upload_speed - if (upLimit !== '-') { - // calculate average download speed - const [sec, nano] = process.hrtime(this._hrTimeBegin); - const speed = Math.ceil(this._totalIn / (sec + nano / 1e9)); // b/s - - logger.debug(`[acl] upload speed: ${speed}b/s`); - - if (speed > upLimit && !this._isUpPaused) { - this._isUpPaused = true; - this.broadcast({type: PRESET_PAUSE_RECV}); - - // determine timeout to resume - const timeout = speed / upLimit * 1.1; // more 10% cost - const direction = `[${this._remoteHost}:${this._remotePort}] -> [${this._dstHost}:${this._dstPort}]`; - logger.info(`[acl] ${direction} upload speed exceed: ${speed}b/s > ${upLimit}b/s, pause for ${timeout}s...`); - - setTimeout(() => { - this.broadcast({type: PRESET_RESUME_RECV}); - this._isUpPaused = false; - }, timeout * 1e3); - } - } - - // max_download_speed - if (dlLimit !== '-') { - // calculate average download speed - const [sec, nano] = process.hrtime(this._hrTimeBegin); - const speed = Math.ceil(this._totalOut / (sec + nano / 1e9)); // b/s - - logger.debug(`[acl] download speed: ${speed}b/s`); - - if (speed > dlLimit && !this._isDlPaused) { - this._isDlPaused = true; - this.broadcast({type: PRESET_PAUSE_SEND}); - - // determine timeout to resume - const timeout = speed / dlLimit * 1.1; // more 10% cost - const direction = `[${this._remoteHost}:${this._remotePort}] <- [${this._dstHost}:${this._dstPort}]`; - logger.info(`[acl] ${direction} download speed exceed: ${speed}b/s > ${dlLimit}b/s, pause for ${timeout}s...`); - - setTimeout(() => { - this.broadcast({type: PRESET_RESUME_SEND}); - this._isDlPaused = false; - }, timeout * 1e3); - } - } - } - - checkRule(host, port) { - const rule = findRule(host, port); - if (rule !== null) { - this.applyRule(rule); - } - } - - appendToAcl(line) { - logger.info(`[acl] append rule: "${line}" to acl`); - fs.appendFile(this._aclPath, `${os.EOL}${line}`, (err) => { - if (err) { - logger.warn(`[acl] fail to update acl: ${err.message}`); - } - rules.push(parseLine(line)); - }); - } - - onNotified({type, payload}) { - switch (type) { - case CONNECTION_CREATED: { - const {host, port} = payload; - this._remoteHost = host; - this._remotePort = port; - this.checkRule(host, port); - break; - } - case CONNECT_TO_REMOTE: { - const {host, port} = payload; - this._dstHost = host; - this._dstPort = port; - this.checkRule(host, port); - break; - } - case PRESET_FAILED: { - const host = this._remoteHost; - const maxTries = this._maxTries; - if (tries[host] === undefined) { - tries[host] = 0; - } - if (++tries[host] >= maxTries) { - logger.warn(`[acl] ${host} max tries(${maxTries}) exceed`); - this.broadcast({type: PRESET_CLOSE_CONNECTION}); - this._isBlocking = true; - if (findRule(host, '*') === null) { - this.appendToAcl(`${host}:* 1`); - } - } - return; - } - } - } - - beforeOut({buffer}) { - this._totalOut += buffer.length; - if (this._isBlocking) { - return; // drop - } - this.checkRule(this._remoteHost, this._remotePort); - this.checkRule(this._dstHost, this._dstPort); - return buffer; - } - - beforeIn({buffer}) { - this._totalIn += buffer.length; - if (this._isBlocking) { - return; // drop - } - this.checkRule(this._remoteHost, this._remotePort); - this.checkRule(this._dstHost, this._dstPort); - return buffer; - } - - beforeOutUdp(...args) { - return this.beforeOut(...args); - } - - beforeInUdp(...args) { - return this.beforeIn(...args); - } - -} diff --git a/src/presets/aead-random-cipher.js b/src/presets/aead-random-cipher.js index db90169..b8e989b 100644 --- a/src/presets/aead-random-cipher.js +++ b/src/presets/aead-random-cipher.js @@ -74,15 +74,15 @@ const HKDF_HASH_ALGORITHM = 'sha1'; */ export default class AeadRandomCipherPreset extends IPreset { - static cipherName = ''; + _cipherName = ''; - static info = null; + _info = null; - static factor = DEFAULT_FACTOR; + _factor = DEFAULT_FACTOR; - static rawKey = null; + _rawKey = null; - static keySaltSize = 0; // key and salt size + _keySaltSize = 0; // key and salt size _cipherKey = null; @@ -99,7 +99,7 @@ export default class AeadRandomCipherPreset extends IPreset { _adBuf = null; - static checkParams({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { + static onCheckParams({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { if (method === undefined || method === '') { throw Error('\'method\' must be set'); } @@ -118,16 +118,12 @@ export default class AeadRandomCipherPreset extends IPreset { } } - static onInit({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { - AeadRandomCipherPreset.cipherName = method; - AeadRandomCipherPreset.info = Buffer.from(info); - AeadRandomCipherPreset.factor = factor; - AeadRandomCipherPreset.rawKey = Buffer.from(AeadRandomCipherPreset.config.key); - AeadRandomCipherPreset.keySaltSize = ciphers[method]; - } - - constructor() { - super(); + onInit({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { + this._cipherName = method; + this._info = Buffer.from(info); + this._factor = factor; + this._rawKey = Buffer.from(this._config.key); + this._keySaltSize = ciphers[method]; this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } @@ -145,9 +141,9 @@ export default class AeadRandomCipherPreset extends IPreset { beforeOut({buffer}) { let salt = null; if (this._cipherKey === null) { - const size = AeadRandomCipherPreset.keySaltSize; + const size = this._keySaltSize; salt = crypto.randomBytes(size); - this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, AeadRandomCipherPreset.rawKey, AeadRandomCipherPreset.info, size); + this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size); } const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => { // random padding @@ -174,12 +170,12 @@ export default class AeadRandomCipherPreset extends IPreset { onReceiving(buffer, {fail}) { // 1. init this._decipherKey if (this._decipherKey === null) { - const size = AeadRandomCipherPreset.keySaltSize; + const size = this._keySaltSize; if (buffer.length < size) { return; // too short to get salt } const salt = buffer.slice(0, size); - this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, AeadRandomCipherPreset.rawKey, AeadRandomCipherPreset.info, size); + this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size); return buffer.slice(size); // drop salt } @@ -225,15 +221,15 @@ export default class AeadRandomCipherPreset extends IPreset { getPaddingLength(key, nonce) { const nonceBuffer = numberToBuffer(nonce, NONCE_LEN, BYTE_ORDER_LE); - const cipher = crypto.createCipheriv(AeadRandomCipherPreset.cipherName, key, nonceBuffer); + const cipher = crypto.createCipheriv(this._cipherName, key, nonceBuffer); cipher.update(nonceBuffer); cipher.final(); - return cipher.getAuthTag()[0] * AeadRandomCipherPreset.factor; + return cipher.getAuthTag()[0] * this._factor; } encrypt(message) { const cipher = crypto.createCipheriv( - AeadRandomCipherPreset.cipherName, + this._cipherName, this._cipherKey, numberToBuffer(this._cipherNonce, NONCE_LEN, BYTE_ORDER_LE) ); @@ -245,7 +241,7 @@ export default class AeadRandomCipherPreset extends IPreset { decrypt(ciphertext, tag) { const decipher = crypto.createDecipheriv( - AeadRandomCipherPreset.cipherName, + this._cipherName, this._decipherKey, numberToBuffer(this._decipherNonce, NONCE_LEN, BYTE_ORDER_LE) ); diff --git a/src/presets/auto-conf.js b/src/presets/auto-conf.js index 2d55485..cb4bc5e 100644 --- a/src/presets/auto-conf.js +++ b/src/presets/auto-conf.js @@ -76,15 +76,13 @@ export default class AutoConfPreset extends IPreset { _header = null; - static suites = []; - - static checkParams({suites}) { + static onCheckParams({suites}) { if (typeof suites !== 'string' || suites.length < 1) { throw Error('\'suites\' is invalid'); } } - static async onInit({suites: uri}) { + static async onCache({suites: uri}) { logger.info(`[auto-conf] loading suites from: ${uri}`); let suites = []; if (uri.startsWith('http')) { @@ -101,7 +99,7 @@ export default class AutoConfPreset extends IPreset { throw Error(`you must provide at least one suite in ${uri}`); } logger.info(`[auto-conf] ${suites.length} suites loaded`); - AutoConfPreset.suites = suites; + return {suites}; } onDestroy() { @@ -111,7 +109,7 @@ export default class AutoConfPreset extends IPreset { createRequestHeader(suites) { const sid = crypto.randomBytes(2); const utc = ntb(getCurrentTimestampInt(), 4, BYTE_ORDER_LE); - const key = EVP_BytesToKey(Buffer.from(AutoConfPreset.config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); + const key = EVP_BytesToKey(Buffer.from(this._config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); const cipher = crypto.createCipheriv('rc4', key, NOOP); const enc_utc = cipher.update(utc); const request_hmac = hmac('md5', key, Buffer.concat([sid, enc_utc])); @@ -122,7 +120,7 @@ export default class AutoConfPreset extends IPreset { } encodeChangeSuite({buffer, broadcast, fail}) { - const {suites} = AutoConfPreset; + const {suites} = this.getStore(); if (suites.length < 1) { return fail('suites are not initialized properly'); } @@ -140,7 +138,7 @@ export default class AutoConfPreset extends IPreset { } decodeChangeSuite({buffer, broadcast, fail}) { - const {suites} = AutoConfPreset; + const {suites} = this.getStore(); if (suites.length < 1) { return fail('suites are not initialized properly'); } @@ -149,7 +147,7 @@ export default class AutoConfPreset extends IPreset { } const sid = buffer.slice(0, 2); const request_hmac = buffer.slice(6, 22); - const key = EVP_BytesToKey(Buffer.from(AutoConfPreset.config.KEY).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); + const key = EVP_BytesToKey(Buffer.from(this._config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); const hmac_calc = hmac('md5', key, buffer.slice(0, 6)); if (!hmac_calc.equals(request_hmac)) { return fail(`unexpected hmac of client request, dump=${dumpHex(buffer)}`); diff --git a/src/presets/base-auth.js b/src/presets/base-auth.js index 0499785..63ef951 100644 --- a/src/presets/base-auth.js +++ b/src/presets/base-auth.js @@ -56,11 +56,11 @@ const DEFAULT_HASH_METHOD = 'sha1'; */ export default class BaseAuthPreset extends IPresetAddressing { - static hmacMethod = DEFAULT_HASH_METHOD; + _hmacMethod = DEFAULT_HASH_METHOD; - static hmacLen = null; + _hmacLen = null; - static hmacKey = null; + _hmacKey = null; _cipher = null; @@ -78,24 +78,20 @@ export default class BaseAuthPreset extends IPresetAddressing { _port = null; // buffer - static checkParams({method = DEFAULT_HASH_METHOD}) { + static onCheckParams({method = DEFAULT_HASH_METHOD}) { const methods = Object.keys(HMAC_METHODS); if (!methods.includes(method)) { throw Error(`base-auth 'method' must be one of [${methods}]`); } } - static onInit({method = DEFAULT_HASH_METHOD}) { - BaseAuthPreset.hmacMethod = method; - BaseAuthPreset.hmacLen = HMAC_METHODS[method]; - BaseAuthPreset.hmacKey = EVP_BytesToKey(BaseAuthPreset.config.key, 16, 16); - } - - constructor() { - super(); - const {hmacKey: key} = BaseAuthPreset; - const iv = hash('md5', Buffer.from(BaseAuthPreset.config.key + 'base-auth')); - if (BaseAuthPreset.config.is_client) { + onInit({method = DEFAULT_HASH_METHOD}) { + const key = EVP_BytesToKey(this._config.key, 16, 16); + const iv = hash('md5', Buffer.from(this._config.key + 'base-auth')); + this._hmacMethod = method; + this._hmacLen = HMAC_METHODS[method]; + this._hmacKey = key; + if (this._config.is_client) { this._cipher = crypto.createCipheriv('aes-128-cfb', key, iv); } else { this._decipher = crypto.createDecipheriv('aes-128-cfb', key, iv); @@ -111,7 +107,7 @@ export default class BaseAuthPreset extends IPresetAddressing { } onNotified(action) { - if (BaseAuthPreset.config.is_client && action.type === CONNECT_TO_REMOTE) { + if (this._config.is_client && action.type === CONNECT_TO_REMOTE) { const {host, port} = action.payload; this._host = Buffer.from(host); this._port = numberToBuffer(port); @@ -119,15 +115,14 @@ export default class BaseAuthPreset extends IPresetAddressing { } encodeHeader() { - const {hmacMethod, hmacKey} = BaseAuthPreset; const header = Buffer.concat([numberToBuffer(this._host.length, 1), this._host, this._port]); const encHeader = this._cipher.update(header); - const mac = hmac(hmacMethod, hmacKey, encHeader); + const mac = hmac(this._hmacMethod, this._hmacKey, encHeader); return Buffer.concat([encHeader, mac]); } decodeHeader({buffer, fail}) { - const {hmacMethod, hmacLen, hmacKey} = BaseAuthPreset; + const hmacLen = this._hmacLen; // minimal length required if (buffer.length < 31) { @@ -142,7 +137,7 @@ export default class BaseAuthPreset extends IPresetAddressing { // check hmac const givenHmac = buffer.slice(1 + alen + 2, 1 + alen + 2 + hmacLen); - const expHmac = hmac(hmacMethod, hmacKey, buffer.slice(0, 1 + alen + 2)); + const expHmac = hmac(this._hmacMethod, this._hmacKey, buffer.slice(0, 1 + alen + 2)); if (!givenHmac.equals(expHmac)) { return fail(`unexpected HMAC=${givenHmac.toString('hex')} want=${expHmac.toString('hex')} dump=${buffer.slice(0, 60).toString('hex')}`); } diff --git a/src/presets/defs.js b/src/presets/defs.js index 468476d..2eaba5d 100644 --- a/src/presets/defs.js +++ b/src/presets/defs.js @@ -97,55 +97,60 @@ export const MUX_DATA_FRAME = '@action:mux_data_frame'; export const MUX_CLOSE_CONN = '@action:mux_close_conn'; /** - * * @lifecycle - * static checkParams() -> static onInit() -> constructor() -> ... -> onDestroy() - * Only called once + * static onCheckParams() + * static onCache() + * constructor() + * onInit() + * ... + * onDestroy() + * + * @note + * static onCheckParams() and static onCache() are called only once since new Hub(). */ export class IPreset { /** - * will become true after checkParams() - * @type {boolean} - */ - static checked = false; - - - /** - * server config + * config * @type {Config} */ - static config = null; - - /** - * will become true after onInit() - * @type {boolean} - */ - static initialized = false; + _config = null; /** * check params passed to the preset, if any errors, should throw directly * @param params */ - static checkParams(params) { + static onCheckParams(params) { } /** - * you can make some cache in this function + * you can make some cache in store or just return something + * you want to put in store, then access store later in other + * hook functions via this.getStore() + * @param params + * @param store + */ + static onCache(params, store) { + // or return something + } + + /** + * constructor + * @param config * @param params */ - static onInit(params) { - + constructor({config, params} = {}) { + if (config) { + this._config = config; + } } - // properties - /** - * return the preset name - * @returns {string} + * constructor alternative to do initialization + * @param params */ - getName() { + onInit(params) { } @@ -218,7 +223,7 @@ export class IPreset { return buffer; } - // auto-generated methods for convenience, DO NOT implement them! + // auto-generated methods, DO NOT implement them! next(direction, buffer) { @@ -241,6 +246,13 @@ export class IPreset { } + /** + * return store passed to onCache() + */ + getStore() { + + } + } /** @@ -250,23 +262,6 @@ export class IPresetAddressing extends IPreset { } -/** - * a class which only have one instance - */ -export class IPresetStatic extends IPreset { - - static isInstantiated = false; - - constructor() { - super(); - if (IPresetStatic.isInstantiated) { - throw Error(`${this.constructor.name} is singleton and can only be instantiated once`); - } - IPresetStatic.isInstantiated = true; - } - -} - /** * check if a class is a valid preset class * @param clazz @@ -278,14 +273,14 @@ export function checkPresetClass(clazz) { } // check require hooks const requiredMethods = [ - 'getName', 'onNotified', 'onDestroy', + 'onNotified', 'onDestroy', 'onInit', 'beforeOut', 'beforeIn', 'clientOut', 'serverIn', 'serverOut', 'clientIn', 'beforeOutUdp', 'beforeInUdp', 'clientOutUdp', 'serverInUdp', 'serverOutUdp', 'clientInUdp' ]; if (requiredMethods.some((method) => typeof clazz.prototype[method] !== 'function')) { return false; } - const requiredStaticMethods = ['checkParams', 'onInit']; + const requiredStaticMethods = ['onCheckParams', 'onCache']; if (requiredStaticMethods.some((method) => typeof clazz[method] !== 'function')) { return false; } diff --git a/src/presets/index.js b/src/presets/index.js index 5334e62..00a6281 100644 --- a/src/presets/index.js +++ b/src/presets/index.js @@ -1,9 +1,7 @@ import {checkPresetClass} from './defs'; // functional -import StatsPreset from './stats'; import TrackerPreset from './tracker'; -import AccessControlPreset from './access-control'; import AutoConfPreset from './auto-conf'; import MuxPreset from './mux'; @@ -32,33 +30,9 @@ import ObfsTls12TicketPreset from './obfs-tls1.2-ticket'; // others import AeadRandomCipherPreset from './aead-random-cipher'; -function monkeyPatch(clazz) { - // patch onInit() - clazz.onInit = (function (onInit) { - return function _onInit(...args) { - if (!clazz.initialized) { - onInit(...args); - clazz.initialized = true; - } - }; - })(clazz.onInit); - - // patch checkParams() - clazz.checkParams = (function (checkParams) { - return function _checkParams(...args) { - if (!clazz.checked) { - checkParams(...args); - clazz.checked = true; - } - }; - })(clazz.checkParams); -} - const mapping = { // functional - 'stats': StatsPreset, 'tracker': TrackerPreset, - 'access-control': AccessControlPreset, 'auto-conf': AutoConfPreset, 'mux': MuxPreset, @@ -90,8 +64,6 @@ const mapping = { const presetClasses = {...mapping}; -Object.keys(presetClasses).forEach((clazzName) => monkeyPatch(presetClasses[clazzName])); - export function getPresetClassByName(name) { let clazz = presetClasses[name]; if (clazz === undefined) { diff --git a/src/presets/mux.js b/src/presets/mux.js index 83b4702..9aac701 100644 --- a/src/presets/mux.js +++ b/src/presets/mux.js @@ -53,8 +53,7 @@ export default class MuxPreset extends IPresetAddressing { _adBuf = null; - constructor() { - super(); + onInit() { this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } diff --git a/src/presets/obfs-http.js b/src/presets/obfs-http.js index cbc213c..d2a4003 100644 --- a/src/presets/obfs-http.js +++ b/src/presets/obfs-http.js @@ -63,22 +63,22 @@ function parseFile(file) { */ export default class ObfsHttpPreset extends IPreset { - static pairs = null; - _isHeaderSent = false; _isHeaderRecv = false; _response = null; - static checkParams({file}) { + static onCheckParams({file}) { if (typeof file !== 'string' || file === '') { throw Error('\'file\' must be a non-empty string'); } } - static onInit({file}) { - ObfsHttpPreset.pairs = parseFile(file); + static onCache({file}) { + return { + pairs: parseFile(file), + }; } onDestroy() { @@ -87,7 +87,7 @@ export default class ObfsHttpPreset extends IPreset { clientOut({buffer}) { if (!this._isHeaderSent) { - const {pairs} = ObfsHttpPreset; + const {pairs} = this.getStore(); this._isHeaderSent = true; const index = crypto.randomBytes(1)[0] % pairs.length; const {request} = pairs[index]; @@ -99,7 +99,7 @@ export default class ObfsHttpPreset extends IPreset { serverIn({buffer, fail}) { if (!this._isHeaderRecv) { - const found = ObfsHttpPreset.pairs.find(({request}) => buffer.indexOf(request) === 0); + const found = this.getStore().pairs.find(({request}) => buffer.indexOf(request) === 0); if (found !== undefined) { this._isHeaderRecv = true; this._response = found.response; @@ -123,7 +123,7 @@ export default class ObfsHttpPreset extends IPreset { clientIn({buffer, fail}) { if (!this._isHeaderRecv) { - const found = ObfsHttpPreset.pairs.find(({response}) => buffer.indexOf(response) === 0); + const found = this._config.store.pairs.find(({response}) => buffer.indexOf(response) === 0); if (found !== undefined) { this._isHeaderRecv = true; return buffer.slice(found.response.length); diff --git a/src/presets/obfs-random-padding.js b/src/presets/obfs-random-padding.js index ef26588..6680db3 100644 --- a/src/presets/obfs-random-padding.js +++ b/src/presets/obfs-random-padding.js @@ -43,8 +43,7 @@ export default class ObfsRandomPaddingPreset extends IPreset { _adBuf = null; - constructor() { - super(); + onInit() { this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } diff --git a/src/presets/obfs-tls1.2-ticket.js b/src/presets/obfs-tls1.2-ticket.js index 1d20841..38ad6d4 100644 --- a/src/presets/obfs-tls1.2-ticket.js +++ b/src/presets/obfs-tls1.2-ticket.js @@ -78,7 +78,7 @@ export default class ObfsTls12TicketPreset extends IPreset { _adBuf = null; - static checkParams({sni}) { + static onCheckParams({sni}) { if (typeof sni === 'undefined') { throw Error('\'sni\' must be set'); } @@ -90,13 +90,10 @@ export default class ObfsTls12TicketPreset extends IPreset { } } - constructor({sni}) { - super(); - this.onReceiving = this.onReceiving.bind(this); - this.onChunkReceived = this.onChunkReceived.bind(this); + onInit({sni}) { this._sni = Array.isArray(sni) ? sni : [sni]; - this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving}); - this._adBuf.on('data', this.onChunkReceived); + this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); + this._adBuf.on('data', this.onChunkReceived.bind(this)); } onDestroy() { diff --git a/src/presets/ss-aead-cipher.js b/src/presets/ss-aead-cipher.js index b236da5..03f2aee 100644 --- a/src/presets/ss-aead-cipher.js +++ b/src/presets/ss-aead-cipher.js @@ -95,19 +95,19 @@ const HKDF_INFO = 'ss-subkey'; */ export default class SsAeadCipherPreset extends IPreset { - static cipherName = ''; + _cipherName = ''; - static info = Buffer.from(HKDF_INFO); + _info = Buffer.from(HKDF_INFO); - static keySize = 0; + _keySize = 0; - static saltSize = 0; + _saltSize = 0; - static nonceSize = 0; + _nonceSize = 0; - static evpKey = null; + _evpKey = null; - static isUseLibSodium = false; + _isUseLibSodium = false; _cipherKey = null; @@ -119,25 +119,21 @@ export default class SsAeadCipherPreset extends IPreset { _adBuf = null; - static checkParams({method}) { + static onCheckParams({method}) { const cipherNames = Object.keys(ciphers); if (!cipherNames.includes(method)) { throw Error(`'method' must be one of [${cipherNames}]`); } } - static onInit({method}) { + onInit({method}) { const [keySize, saltSize, nonceSize] = ciphers[method]; - SsAeadCipherPreset.cipherName = method; - SsAeadCipherPreset.keySize = keySize; - SsAeadCipherPreset.saltSize = saltSize; - SsAeadCipherPreset.nonceSize = nonceSize; - SsAeadCipherPreset.evpKey = EVP_BytesToKey(SsAeadCipherPreset.config.key, keySize, 16); - SsAeadCipherPreset.isUseLibSodium = Object.keys(libsodium_functions).includes(method); - } - - constructor() { - super(); + this._cipherName = method; + this._keySize = keySize; + this._saltSize = saltSize; + this._nonceSize = nonceSize; + this._evpKey = EVP_BytesToKey(this._config.key, keySize, 16); + this._isUseLibSodium = Object.keys(libsodium_functions).includes(method); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } @@ -156,9 +152,8 @@ export default class SsAeadCipherPreset extends IPreset { beforeOut({buffer}) { let salt = null; if (this._cipherKey === null) { - const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; - salt = crypto.randomBytes(saltSize); - this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); + salt = crypto.randomBytes(this._saltSize); + this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); } const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => { const dataLen = numberToBuffer(chunk.length); @@ -179,12 +174,12 @@ export default class SsAeadCipherPreset extends IPreset { onReceiving(buffer, {fail}) { if (this._decipherKey === null) { - const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; + const saltSize = this._saltSize; if (buffer.length < saltSize) { return; // too short to get salt } const salt = buffer.slice(0, saltSize); - this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); + this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); return buffer.slice(saltSize); // drop salt } @@ -218,12 +213,12 @@ export default class SsAeadCipherPreset extends IPreset { } encrypt(message) { - const {isUseLibSodium, cipherName, nonceSize} = SsAeadCipherPreset; + const cipherName = this._cipherName; const cipherKey = this._cipherKey; - const nonce = numberToBuffer(this._cipherNonce, nonceSize, BYTE_ORDER_LE); + const nonce = numberToBuffer(this._cipherNonce, this._nonceSize, BYTE_ORDER_LE); let ciphertext = null; let tag = null; - if (isUseLibSodium) { + if (this._isUseLibSodium) { const noop = Buffer.alloc(0); const result = libsodium[libsodium_functions[cipherName][0]]( message, noop, noop, nonce, cipherKey @@ -240,10 +235,10 @@ export default class SsAeadCipherPreset extends IPreset { } decrypt(ciphertext, tag) { - const {isUseLibSodium, cipherName, nonceSize} = SsAeadCipherPreset; + const cipherName = this._cipherName; const decipherKey = this._decipherKey; - const nonce = numberToBuffer(this._decipherNonce, nonceSize, BYTE_ORDER_LE); - if (isUseLibSodium) { + const nonce = numberToBuffer(this._decipherNonce, this._nonceSize, BYTE_ORDER_LE); + if (this._isUseLibSodium) { const noop = Buffer.alloc(0); try { const plaintext = libsodium[libsodium_functions[cipherName][1]]( @@ -270,21 +265,20 @@ export default class SsAeadCipherPreset extends IPreset { // udp beforeOutUdp({buffer}) { - const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; - const salt = crypto.randomBytes(saltSize); - this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); + const salt = crypto.randomBytes(this._saltSize); + this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); this._cipherNonce = 0; const [ciphertext, tag] = this.encrypt(buffer); return Buffer.concat([salt, ciphertext, tag]); } beforeInUdp({buffer, fail}) { - const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; + const saltSize = this._saltSize; if (buffer.length < saltSize) { return fail(`too short to get salt, len=${buffer.length} dump=${buffer.toString('hex')}`); } const salt = buffer.slice(0, saltSize); - this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); + this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); this._decipherNonce = 0; if (buffer.length < saltSize + TAG_SIZE + 1) { return fail(`too short to verify Data, len=${buffer.length} dump=${buffer.toString('hex')}`); diff --git a/src/presets/ss-base.js b/src/presets/ss-base.js index 223b7fb..0dbaa8b 100644 --- a/src/presets/ss-base.js +++ b/src/presets/ss-base.js @@ -84,7 +84,7 @@ export default class SsBasePreset extends IPresetAddressing { } onNotified(action) { - if (SsBasePreset.config.is_client && action.type === CONNECT_TO_REMOTE) { + if (this._config.is_client && action.type === CONNECT_TO_REMOTE) { const {host, port} = action.payload; const type = getHostType(host); this._atyp = type; diff --git a/src/presets/ss-stream-cipher.js b/src/presets/ss-stream-cipher.js index 6efa5ab..3e9814a 100644 --- a/src/presets/ss-stream-cipher.js +++ b/src/presets/ss-stream-cipher.js @@ -79,7 +79,15 @@ export default class SsStreamCipherPreset extends IPreset { _cipher = null; _decipher = null; - static checkParams({method}) { + get key() { + return this._key; + } + + get iv() { + return this._iv; + } + + static onCheckParams({method}) { if (typeof method !== 'string' || method === '') { throw Error('\'method\' must be set'); } @@ -89,22 +97,13 @@ export default class SsStreamCipherPreset extends IPreset { } } - get key() { - return this._key; - } - - get iv() { - return this._iv; - } - - constructor({method}) { - super(); + onInit({method}) { const [keySize, ivSize] = ciphers[method]; const iv = crypto.randomBytes(ivSize); this._algorithm = ['rc4-md5', 'rc4-md5-6'].includes(method) ? 'rc4' : method; this._keySize = keySize; this._ivSize = ivSize; - this._key = EVP_BytesToKey(SsStreamCipherPreset.config.key, keySize, ivSize); + this._key = EVP_BytesToKey(this._config.key, keySize, ivSize); this._iv = method === 'rc4-md5-6' ? iv.slice(0, 6) : iv; } diff --git a/src/presets/ssr-auth-aes128-md5.js b/src/presets/ssr-auth-aes128-md5.js index 8069228..fc49605 100644 --- a/src/presets/ssr-auth-aes128-md5.js +++ b/src/presets/ssr-auth-aes128-md5.js @@ -16,8 +16,8 @@ import SsrAuthAes128Preset from './ssr-auth-aes128'; */ export default class SsrAuthAes128Md5Preset extends SsrAuthAes128Preset { - constructor(params) { - super(params); + constructor(props) { + super(props); this._hashFunc = 'md5'; this._salt = 'auth_aes128_md5'; } diff --git a/src/presets/ssr-auth-aes128-sha1.js b/src/presets/ssr-auth-aes128-sha1.js index bff6dd1..fe10b20 100644 --- a/src/presets/ssr-auth-aes128-sha1.js +++ b/src/presets/ssr-auth-aes128-sha1.js @@ -16,8 +16,8 @@ import SsrAuthAes128Preset from './ssr-auth-aes128'; */ export default class SsrAuthAes128Sha1Preset extends SsrAuthAes128Preset { - constructor(params) { - super(params); + constructor(props) { + super(props); this._hashFunc = 'sha1'; this._salt = 'auth_aes128_sha1'; } diff --git a/src/presets/ssr-auth-aes128.js b/src/presets/ssr-auth-aes128.js index c7af7bb..1b91340 100644 --- a/src/presets/ssr-auth-aes128.js +++ b/src/presets/ssr-auth-aes128.js @@ -82,9 +82,9 @@ const MAX_TIME_DIFF = 30; // seconds */ export default class SsrAuthAes128Preset extends IPreset { - static clientId = null; + _clientId = null; - static connectionId = null; + _connectionId = null; _userKey = null; @@ -102,14 +102,9 @@ export default class SsrAuthAes128Preset extends IPreset { _adBuf = null; - static onInit() { - SsrAuthAes128Preset.userKey = EVP_BytesToKey(SsrAuthAes128Preset.config.key, 16, 16); - SsrAuthAes128Preset.clientId = crypto.randomBytes(4); - SsrAuthAes128Preset.connectionId = getRandomInt(0, 0x00ffffff); - } - - constructor() { - super(); + onInit() { + this._clientId = crypto.randomBytes(4); + this._connectionId = getRandomInt(0, 0x00ffffff); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } @@ -125,7 +120,8 @@ export default class SsrAuthAes128Preset extends IPreset { } createRequest(buffer) { - const {clientId, connectionId} = SsrAuthAes128Preset; + const clientId = this._clientId; + const connectionId = this._connectionId; const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key'); const iv = this.readProperty('ss-stream-cipher', 'iv'); @@ -148,9 +144,9 @@ export default class SsrAuthAes128Preset extends IPreset { if (connectionId > 0xff000000) { connection_id = getRandomInt(0, 0x00ffffff); client_id = crypto.randomBytes(4); - SsrAuthAes128Preset.connectionId = connection_id; + this._connectionId = connection_id; } else { - connection_id = ++SsrAuthAes128Preset.connectionId; + connection_id = ++this._connectionId; } const random_bytes_len = getRandomInt(0, buffer.length > 400 ? 512 : 1024); diff --git a/src/presets/ssr-auth-chain-a.js b/src/presets/ssr-auth-chain-a.js index 2caff21..8a88382 100644 --- a/src/presets/ssr-auth-chain-a.js +++ b/src/presets/ssr-auth-chain-a.js @@ -16,8 +16,8 @@ import SsrAuthChainPreset from './ssr-auth-chain'; */ export default class SsrAuthChainAPreset extends SsrAuthChainPreset { - constructor(params) { - super(params); + constructor(props) { + super(props); this._salt = 'auth_chain_a'; } diff --git a/src/presets/ssr-auth-chain-b.js b/src/presets/ssr-auth-chain-b.js index fb2a902..95c0bc1 100644 --- a/src/presets/ssr-auth-chain-b.js +++ b/src/presets/ssr-auth-chain-b.js @@ -41,8 +41,8 @@ export default class SsrAuthChainBPreset extends SsrAuthChainPreset { _data_size_list2 = []; - constructor(params) { - super(params); + constructor(props) { + super(props); this._salt = 'auth_chain_b'; } diff --git a/src/presets/ssr-auth-chain.js b/src/presets/ssr-auth-chain.js index 3030ba2..70b41ed 100644 --- a/src/presets/ssr-auth-chain.js +++ b/src/presets/ssr-auth-chain.js @@ -111,9 +111,9 @@ export function xorshift128plus() { */ export default class SsrAuthChainPreset extends IPreset { - static clientId = null; + _clientId = null; - static connectionId = null; + _connectionId = null; _userKey = null; @@ -143,13 +143,9 @@ export default class SsrAuthChainPreset extends IPreset { _adBuf = null; - static onInit() { - SsrAuthChainPreset.clientId = crypto.randomBytes(4); - SsrAuthChainPreset.connectionId = getRandomInt(0, 0x00ffffff); - } - - constructor() { - super(); + onInit() { + this._clientId = crypto.randomBytes(4); + this._connectionId = getRandomInt(0, 0x00ffffff); this._rngClient = xorshift128plus(); this._rngServer = xorshift128plus(); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); @@ -179,7 +175,8 @@ export default class SsrAuthChainPreset extends IPreset { } createRequest() { - const {clientId, connectionId} = SsrAuthChainPreset; + const clientId = this._clientId; + const connectionId = this._connectionId; const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key'); const iv = this.readProperty('ss-stream-cipher', 'iv'); @@ -203,9 +200,9 @@ export default class SsrAuthChainPreset extends IPreset { if (connectionId > 0xff000000) { connection_id = getRandomInt(0, 0x00ffffff); client_id = crypto.randomBytes(4); - SsrAuthChainPreset.connectionId = connection_id; + this._connectionId = connection_id; } else { - connection_id = ++SsrAuthChainPreset.connectionId; + connection_id = ++this._connectionId; } const overhead = ntb(this._overhead, 2, BYTE_ORDER_LE); @@ -232,17 +229,17 @@ export default class SsrAuthChainPreset extends IPreset { createChunks(buffer) { const userKey = this._userKey; - const max_payload_size = SsrAuthChainPreset.config.is_client ? 2800 : (this._tcpMss - this._overhead); + const max_payload_size = this._config.is_client ? 2800 : (this._tcpMss - this._overhead); return getChunks(buffer, max_payload_size).map((payload) => { let _payload = payload; - if (SsrAuthChainPreset.config.is_server && this._encodeChunkId === 1) { + if (this._config.is_server && this._encodeChunkId === 1) { _payload = Buffer.concat([ntb(this._tcpMss, 2, BYTE_ORDER_LE), payload]); } const rc4_enc_payload = this._cipher.update(_payload); - const hash = SsrAuthChainPreset.config.is_client ? this._lastClientHash : this._lastServerHash; + const hash = this._config.is_client ? this._lastClientHash : this._lastServerHash; const size = rc4_enc_payload.length ^ hash.slice(-2).readUInt16LE(0); // generate two pieces of random bytes - const rng = SsrAuthChainPreset.config.is_client ? this._rngClient : this._rngServer; + const rng = this._config.is_client ? this._rngClient : this._rngServer; const random_bytes_len = this.getRandomBytesLengthForTcp(hash, _payload.length, rng); const random_bytes = crypto.randomBytes(random_bytes_len); const random_divide_pos = random_bytes_len > 0 ? rng.next().mod(8589934609).mod(random_bytes_len).toNumber() : 0; @@ -253,7 +250,7 @@ export default class SsrAuthChainPreset extends IPreset { const hmac_key = Buffer.concat([userKey, ntb(this._encodeChunkId, 4, BYTE_ORDER_LE)]); const chunk_hmac = hmac('md5', hmac_key, chunk); chunk = Buffer.concat([chunk, chunk_hmac.slice(0, 2)]); - if (SsrAuthChainPreset.config.is_client) { + if (this._config.is_client) { this._lastClientHash = chunk_hmac; } else { this._lastServerHash = chunk_hmac; @@ -350,9 +347,9 @@ export default class SsrAuthChainPreset extends IPreset { if (buffer.length < 2 || this._adBuf === null) { return; // too short to get size } - const hash = SsrAuthChainPreset.config.is_client ? this._lastServerHash : this._lastClientHash; + const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash; const payload_len = buffer.readUInt16LE(0) ^ hash.readUInt16LE(14); - const rng = SsrAuthChainPreset.config.is_client ? this._rngServer : this._rngClient; + const rng = this._config.is_client ? this._rngServer : this._rngClient; const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng); const chunk_size = 2 + random_bytes_len + payload_len + 2; if (chunk_size >= 4096) { @@ -376,9 +373,9 @@ export default class SsrAuthChainPreset extends IPreset { return fail(`unexpected chunk hmac, chunk=${dumpHex(chunk)}`); } // drop random_bytes, get encrypted payload - const hash = SsrAuthChainPreset.config.is_client ? this._lastServerHash : this._lastClientHash; + const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash; const payload_len = chunk.readUInt16LE(0) ^ hash.readUInt16LE(14); - const rng = SsrAuthChainPreset.config.is_client ? this._rngServer : this._rngClient; + const rng = this._config.is_client ? this._rngServer : this._rngClient; const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng); let enc_payload = null; if (random_bytes_len > 0) { @@ -390,12 +387,12 @@ export default class SsrAuthChainPreset extends IPreset { // decrypt payload let payload = this._decipher.update(enc_payload); // update hash - if (SsrAuthChainPreset.config.is_client) { + if (this._config.is_client) { this._lastServerHash = new_hash; } else { this._lastClientHash = new_hash; } - if (SsrAuthChainPreset.config.is_client && this._decodeChunkId === 1) { + if (this._config.is_client && this._decodeChunkId === 1) { this._tcpMss = payload.readUInt16LE(0); payload = payload.slice(2); } diff --git a/src/presets/tracker.js b/src/presets/tracker.js index a582205..2c4074f 100644 --- a/src/presets/tracker.js +++ b/src/presets/tracker.js @@ -95,7 +95,7 @@ export default class TrackerPreset extends IPreset { if (strs.length > TRACK_MAX_SIZE) { strs = strs.slice(0, perSize).concat([' ... ']).concat(strs.slice(-perSize)); } - const summary = TrackerPreset.config.is_client ? `out/in = ${up}/${dp}, ${ub}b/${db}b` : `in/out = ${dp}/${up}, ${db}b/${ub}b`; + const summary = this._config.is_client ? `out/in = ${up}/${dp}, ${ub}b/${db}b` : `in/out = ${dp}/${up}, ${db}b/${ub}b`; logger.info(`[tracker:${this._transport}] summary(${summary}) abstract(${strs.join(' ')})`); } diff --git a/src/presets/v2ray-vmess.js b/src/presets/v2ray-vmess.js index 9693090..e59cbd1 100644 --- a/src/presets/v2ray-vmess.js +++ b/src/presets/v2ray-vmess.js @@ -159,13 +159,8 @@ function createChacha20Poly1305Key(key) { */ export default class V2rayVmessPreset extends IPresetAddressing { - static uuid = null; - - static security = null; - - static userHashCache = [ - // {timestamp, authInfo} - ]; + _uuid = null; + _security = null; _atyp = null; _host = null; // buffer @@ -192,7 +187,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { _cipherNonce = 0; _decipherNonce = 0; - static checkParams({id, security = 'aes-128-gcm'}) { + static onCheckParams({id, security = 'aes-128-gcm'}) { if (Buffer.from(id.split('-').join(''), 'hex').length !== 16) { throw Error('id is not a valid uuid'); } @@ -202,17 +197,16 @@ export default class V2rayVmessPreset extends IPresetAddressing { } } - static onInit({id, security = 'aes-128-gcm'}) { - V2rayVmessPreset.uuid = Buffer.from(id.split('-').join(''), 'hex'); - if (V2rayVmessPreset.config.is_client) { - V2rayVmessPreset.security = securityTypes[security]; - } - setInterval(() => V2rayVmessPreset.updateAuthCache(), 1e3); - V2rayVmessPreset.updateAuthCache(); + static onCache(_, store) { + setInterval(() => V2rayVmessPreset.updateAuthCache(store), 1e3); + V2rayVmessPreset.updateAuthCache(store); } - static updateAuthCache() { - const items = this.userHashCache; + static updateAuthCache(store) { + const items = store.userHashCache || [ + // {timestamp, authInfo}, + // ... + ]; const now = getCurrentTimestampInt(); let from = now - TIME_TOLERANCE; const to = now + TIME_TOLERANCE; @@ -224,15 +218,18 @@ export default class V2rayVmessPreset extends IPresetAddressing { } for (let ts = from; ts <= to; ++ts) { // account auth info, 16 bytes - const uuid = this.uuid; + const uuid = this._uuid; const authInfo = hmac('md5', uuid, ntb(ts, 8)); newItems.push({timestamp: ts, authInfo: authInfo}); } - this.userHashCache = newItems; + store.userHashCache = newItems; } - constructor() { - super(); + onInit({id, security = 'aes-128-gcm'}) { + this._uuid = Buffer.from(id.split('-').join(''), 'hex'); + if (this._config.is_client) { + this._security = securityTypes[security]; + } this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf.on('data', this.onChunkReceived.bind(this)); } @@ -254,7 +251,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { } onNotified(action) { - if (V2rayVmessPreset.config.is_client && action.type === CONNECT_TO_REMOTE) { + if (this._config.is_client && action.type === CONNECT_TO_REMOTE) { const {host, port} = action.payload; const type = getAddrType(host); this._atyp = type; @@ -269,7 +266,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { beforeOut({buffer}) { if (!this._isHeaderSent) { this._isHeaderSent = true; - const header = V2rayVmessPreset.config.is_client ? this.createRequestHeader() : this.createResponseHeader(); + const header = this._config.is_client ? this.createRequestHeader() : this.createResponseHeader(); const chunks = this.getBufferChunks(buffer); return Buffer.concat([header, ...chunks]); } else { @@ -307,7 +304,8 @@ export default class V2rayVmessPreset extends IPresetAddressing { return fail(`fail to parse request header: ${buffer.toString('hex')}`); } - const {uuid, userHashCache} = V2rayVmessPreset; + const uuid = this._uuid; + const {userHashCache} = this.getStore(); // verify auth info const authInfo = buffer.slice(0, 16); @@ -409,7 +407,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { return fail('fail to verify request command'); } const data = buffer.slice(16 + plainReqHeader.length + 4); - V2rayVmessPreset.security = securityType; + this._security = securityType; this._isBroadCasting = true; this.broadcast({ @@ -448,7 +446,9 @@ export default class V2rayVmessPreset extends IPresetAddressing { this._v = rands[32]; this._opt = 0x05; - const {userHashCache, uuid} = V2rayVmessPreset; + const uuid = this._uuid; + const {userHashCache} = this.getStore(); + const {timestamp, authInfo} = userHashCache[getRandomInt(0, userHashCache.length - 1)]; // utc timestamp: Big-Endian, 8 bytes @@ -461,7 +461,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { let command = Buffer.from([ 0x01, // Ver ...this._dataEncIV, ...this._dataEncKey, this._v, this._opt, - paddingLen << 4 | V2rayVmessPreset.security, + paddingLen << 4 | this._security, 0x00, // RSV 0x01, // Cmd ...this._port, this._atyp, @@ -489,7 +489,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { getBufferChunks(buffer) { return getChunks(buffer, 0x3fff).map((chunk) => { let _chunk = chunk; - if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(V2rayVmessPreset.security)) { + if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(this._security)) { _chunk = Buffer.concat(this.encrypt(_chunk)); } let _len = _chunk.length; @@ -514,7 +514,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { } onChunkReceived(chunk, {next, fail}) { - if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(V2rayVmessPreset.security)) { + if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(this._security)) { const tag = chunk.slice(-16); const data = this.decrypt(chunk.slice(2, -16), tag); if (data === null) { @@ -526,7 +526,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { } encrypt(plaintext) { - const {security} = V2rayVmessPreset; + const security = this._security; const nonce = Buffer.concat([ntb(this._cipherNonce), this._dataEncIV.slice(2, 12)]); let ciphertext = null; let tag = null; @@ -548,7 +548,7 @@ export default class V2rayVmessPreset extends IPresetAddressing { } decrypt(ciphertext, tag) { - const {security} = V2rayVmessPreset; + const security = this._security; const nonce = Buffer.concat([ntb(this._decipherNonce), this._dataDecIV.slice(2, 12)]); if (security === SECURITY_TYPE_AES_128_GCM) { const decipher = crypto.createDecipheriv('aes-128-gcm', this._dataDecKey, nonce); diff --git a/src/transports/defs.js b/src/transports/defs.js index 7698be1..c15f544 100644 --- a/src/transports/defs.js +++ b/src/transports/defs.js @@ -5,12 +5,13 @@ import EventEmitter from 'events'; class Bound extends EventEmitter { _ctx = null; + _config = null; - constructor({context, config}) { + constructor({config, context}) { super(); - this._ctx = context; this._config = config; + this._ctx = context; } get ctx() { diff --git a/test/common/preset-runner.js b/test/common/preset-runner.js index 0085947..c807a75 100644 --- a/test/common/preset-runner.js +++ b/test/common/preset-runner.js @@ -1,19 +1,15 @@ import EventEmitter from 'events'; -import {getPresetClassByName} from '../../src/presets'; import {PIPE_ENCODE, PIPE_DECODE} from '../../src/constants'; import {Middleware} from '../../src/core/middleware'; - export class PresetRunner extends EventEmitter { + _config = null; constructor({name, params = {}}, config = {}) { super(); this._config = config; - const clazz = getPresetClassByName(name); - clazz.config = config; - clazz.checkParams(params); - this.middleware = new Middleware({name, params}, config); + this.middleware = new Middleware({config, preset: {name, params}}); } notify(action) { @@ -29,8 +25,6 @@ export class PresetRunner extends EventEmitter { data = Buffer.from(data); } return new Promise((resolve, reject) => { - this.middleware.on('post_1', resolve); - this.middleware.on('post_-1', resolve); this.middleware.on('fail', reject); this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action)); this.middleware.write({ @@ -47,8 +41,6 @@ export class PresetRunner extends EventEmitter { data = Buffer.from(data); } return new Promise((resolve, reject) => { - this.middleware.on('post_1', resolve); - this.middleware.on('post_-1', resolve); this.middleware.on('fail', reject); this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action)); this.middleware.write({ diff --git a/test/presets/__snapshots__/access-control.test.js.snap b/test/presets/__snapshots__/access-control.test.js.snap deleted file mode 100644 index 8af6b2a..0000000 --- a/test/presets/__snapshots__/access-control.test.js.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`running on both client and server 1`] = ` -Object { - "data": Array [ - 49, - 50, - ], - "type": "Buffer", -} -`; - -exports[`running on both client and server 2`] = ` -Object { - "data": Array [ - 51, - 52, - ], - "type": "Buffer", -} -`; - -exports[`running on both client and server 3`] = ` -Object { - "data": Array [ - 53, - 54, - ], - "type": "Buffer", -} -`; - -exports[`running on both client and server 4`] = ` -Object { - "data": Array [ - 55, - 56, - ], - "type": "Buffer", -} -`; diff --git a/test/presets/access-control.test.js b/test/presets/access-control.test.js deleted file mode 100644 index 446b91c..0000000 --- a/test/presets/access-control.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import path from 'path'; -import { - PRESET_FAILED, - CONNECT_TO_REMOTE, - CONNECTION_CREATED, - CONNECTION_CLOSED -} from '../../src/presets'; -import {PresetRunner, sleep} from '../common'; - -test('running on both client and server', async () => { - const runner = new PresetRunner({ - name: 'access-control', - params: { - acl: path.join(__dirname, 'acl.txt') - } - }, { - is_client: true, - is_server: false - }); - - await sleep(20); - - const actionPayload = { - payload: { - host: 'example.com', - port: 443 - } - }; - runner.notify({type: CONNECT_TO_REMOTE, ...actionPayload}); - runner.notify({type: CONNECTION_CREATED, ...actionPayload}); - - expect(await runner.forward('12')).toMatchSnapshot(); - expect(await runner.forward('34')).toMatchSnapshot(); - - expect(await runner.backward('56')).toMatchSnapshot(); - expect(await runner.backward('78')).toMatchSnapshot(); - - runner.notify({type: PRESET_FAILED}); - runner.notify({type: CONNECTION_CLOSED}); -}); diff --git a/test/presets/defs.test.js b/test/presets/defs.test.js index 32aec9f..afe4a7e 100644 --- a/test/presets/defs.test.js +++ b/test/presets/defs.test.js @@ -1,4 +1,4 @@ -import {IPreset, IPresetStatic, checkPresetClass} from '../defs'; +import {IPreset, checkPresetClass} from '../../src/presets/defs'; test('IPreset#onNotified', () => { const preset = new IPreset(); @@ -10,11 +10,6 @@ test('IPreset#onDestroy', () => { expect(preset.onDestroy()).toBe(undefined); }); -test('IPresetStatic#constructor', () => { - expect(() => new IPresetStatic()).not.toThrow(); - expect(() => new IPresetStatic()).toThrow(); -}); - test('should return false if class is not a function', () => { expect(checkPresetClass(null)).toBe(false); }); diff --git a/test/presets/index.test.js b/test/presets/index.test.js index 1761985..7a5f290 100644 --- a/test/presets/index.test.js +++ b/test/presets/index.test.js @@ -1,4 +1,4 @@ -import {getPresetClassByName} from '../index'; +import {getPresetClassByName} from '../../src/presets'; test('should return a preset class', () => { expect(getPresetClassByName('ss-base')).toBeDefined(); diff --git a/test/presets/v2ray-vmess.test.js b/test/presets/v2ray-vmess.test.js deleted file mode 100644 index 4e649ab..0000000 --- a/test/presets/v2ray-vmess.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import {CONNECT_TO_REMOTE} from '../../src/presets'; -import {PresetRunner} from '../common'; - -test('running on client', async () => { - const runner = new PresetRunner({ - name: 'v2ray-vmess', - params: { - id: 'a3482e88-686a-4a58-8126-99c9df64b7bf', - security: 'aes-128-gcm' - } - }, { - is_client: true, - is_server: false - }); - - runner.notify({ - type: CONNECT_TO_REMOTE, - payload: { - host: 'example.com', - port: 443 - } - }); - - const packet_1 = await runner.forward('12'); - const packet_2 = await runner.forward('34'); - - expect(packet_1.length).toBeGreaterThanOrEqual(50); - expect(packet_2.length).toBeGreaterThanOrEqual(20); - - // fail on wrong data - await expect(runner.backward(Buffer.alloc(35))).rejects.toBeDefined(); - - runner.destroy(); -}); - -test('running on server', async () => { - const runner = new PresetRunner({ - name: 'v2ray-vmess', - params: { - id: 'a3482e88-686a-4a58-8126-99c9df64b7bf' - } - }, { - is_client: false, - is_server: true - }); - - // fail on wrong data - await expect(runner.backward(Buffer.alloc(35))).rejects.toBeDefined(); - - runner.destroy(); -});