src,test: isolate config among presets
This commit is contained in:
parent
89b8392eb3
commit
4b7c2e091c
@ -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();
|
||||
});
|
||||
|
@ -15,17 +15,18 @@ function loadFileSync(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,6 +51,10 @@ 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);
|
||||
this.local_protocol = protocol.slice(0, -1);
|
||||
@ -127,13 +132,17 @@ export class Config {
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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': <preset> */);
|
||||
|
||||
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.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' : '');
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
// <host:port>: <rule>
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
// <host>: <count>
|
||||
};
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
);
|
||||
|
@ -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)}`);
|
||||
|
@ -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')}`);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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')}`);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(' ')})`);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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({
|
||||
|
@ -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",
|
||||
}
|
||||
`;
|
@ -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});
|
||||
});
|
@ -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);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {getPresetClassByName} from '../index';
|
||||
import {getPresetClassByName} from '../../src/presets';
|
||||
|
||||
test('should return a preset class', () => {
|
||||
expect(getPresetClassByName('ss-base')).toBeDefined();
|
||||
|
@ -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();
|
||||
});
|
Loading…
Reference in New Issue
Block a user