src,test: isolate config among presets

This commit is contained in:
Micooz 2018-02-15 11:01:24 +08:00
parent 89b8392eb3
commit 4b7c2e091c
34 changed files with 290 additions and 968 deletions

@ -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();
});

@ -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 "<protocol>://<host>:<port>[?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=<host>:<port>" 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 "<protocol>://<host>:<port>[?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');
}

@ -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._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' : '');

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

@ -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();
});