refactor(core): robust design for middlewares

close: #40
This commit is contained in:
Micooz 2017-04-13 16:43:54 +08:00
parent 31ac8aff46
commit 0079380d36
15 changed files with 108 additions and 259 deletions

44
bin/bootstrap.js vendored

@ -7,22 +7,13 @@ const packageJson = require('../package.json');
const BOOTSTRAP_TYPE_SERVER = 1;
const version = packageJson.version;
const usage = '--host <host> --port <port> --key <key> [...]';
const usage = '--config <file> --host <host> --port <port> --key <key> [...]';
const options = [
['-c, --config [file]', 'a json format file for configuration', ''],
['-c, --config <file>', 'a json/js format file for configuration', ''],
['--host <host>', 'an ip address or a hostname to bind, default: \'localhost\'', 'localhost'],
['--port <port>', 'where to listen on, default: 1080', 1080],
['--servers [servers]', 'a list of servers used by client, split by comma, default: \'\'', (value) => value.split(','), []],
['--key <key>', 'a key for encryption and decryption'],
['--frame [frame]', 'a preset used in frame middleware, default: \'origin\'', 'origin'],
['--frame-params [crypto-params]', 'parameters for frame preset, default: \'\'', ''],
['--crypto [crypto]', 'a preset used in crypto middleware, default: \'\'', ''],
['--crypto-params [crypto-params]', 'parameters for crypto, default: \'aes-256-cfb\'', 'aes-256-cfb'],
['--protocol [protocol]', 'a preset used in protocol middleware, default: \'aead\'', 'aead'],
['--protocol-params [protocol-params]', 'parameters for protocol, default: \'aes-256-gcm,ss-subkey\'', 'aes-256-gcm,ss-subkey'],
['--obfs [obfs]', 'a preset used in obfs middleware, default: \'\'', ''],
['--obfs-params [obfs-params]', 'parameters for obfs, default: \'\'', ''],
['--redirect [redirect]', 'redirect stream to here when any preset fail to process, default: \'\'', ''],
['--log-level [log-level]', 'log level, default: \'silly\'', 'silly'],
['--timeout [timeout]', 'time to close connection if inactive, default: 600', 600],
@ -35,13 +26,8 @@ const examples = `
Examples:
As simple as possible:
$ blinksocks client -c config.json --watch
To start a server:
$ blinksocks server --host 0.0.0.0 --port 7777 --key password
To start a client:
$ blinksocks client --host localhost --port 1080 --key password --servers=node1.test.com:7777,node2.test.com:7777
$ blinksocks client -c config.js
$ blinksocks server -c config.js
`;
/**
@ -52,16 +38,7 @@ const examples = `
*/
function obtainConfig(type, options) {
// CLI options should be able to overwrite options specified in --config
const {host, servers, key, quiet} = options;
const {frame, crypto, protocol, obfs, redirect} = options;
// renames
const [frame_params, crypto_params, protocol_params, obfs_params] = [
options.frameParams,
options.cryptoParams,
options.protocolParams,
options.obfsParams
];
const {host, servers, key, presets, redirect, quiet} = options;
// pre-process
const [port, log_level, timeout, watch, profile] = [
@ -78,14 +55,7 @@ function obtainConfig(type, options) {
port,
servers,
key,
frame,
frame_params,
crypto,
crypto_params,
protocol,
protocol_params,
obfs,
obfs_params,
presets,
redirect,
log_level,
timeout,
@ -159,7 +129,7 @@ module.exports = function (type, {Hub, Config}) {
app.run();
process.on('SIGINT', () => app.onClose());
} catch (err) {
console.error(err.message);
console.error(err);
process.exit(-1);
}
};

@ -21,14 +21,14 @@ module.exports = {
"host": "localhost",
"port": 1080,
"servers": [],
"key": "${random('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+<>?:|{}ABCDEFGHIJKLMNOPQRSTUVWXYZ', 16)}",
"key": "${random('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+<>?:|{}-=[];,./ABCDEFGHIJKLMNOPQRSTUVWXYZ', 16)}",
"presets": [{
"name": "origin",
"name": "ss-base",
"params": {}
}, {
"name": "aead",
"name": "ss-aead-cipher",
"params": {
"method": "aes-128-gcm",
"method": "aes-256-gcm",
"info": "ss-subkey"
}
}],
@ -41,8 +41,4 @@ module.exports = {
const file = 'blinksocks.config.js';
fs.writeFile(file, js, function (err) {
if (err) {
throw err;
}
});
fs.writeFileSync(file, js);

@ -15,21 +15,7 @@ export class Config {
static key;
static frame;
static frame_params;
static crypto;
static crypto_params;
static protocol;
static protocol_params;
static obfs;
static obfs_params;
static presets;
static redirect;
@ -105,57 +91,33 @@ export class Config {
this.key = json.key;
// frame & frame_params
// presets & presets' parameters
if (typeof json.frame !== 'string') {
throw Error('\'frame\' must be a string');
if (!Array.isArray(json.presets)) {
throw Error('\'presets\' must be an array');
}
if (typeof json.frame_params !== 'string') {
throw Error('\'frame_params\' must be a string');
if (json.presets.length < 1) {
throw Error('\'presets\' must contain at least one preset');
}
this.frame = json.frame || 'origin';
this.frame_params = json.frame_params;
for (const preset of json.presets) {
const {name, params} = preset;
// crypto & crypto_params
if (typeof name === 'undefined') {
throw Error('\'preset.name\' must be a string');
}
if (typeof json.crypto !== 'string') {
throw Error('\'crypto\' must be a string');
// 1. check for the existence of the preset
const ps = require(`../presets/${preset.name}`).default;
// 2. check parameters, but ignore the first preset
if (name !== json.presets[0].name) {
delete new ps(params || {});
}
}
if (typeof json.crypto_params !== 'string') {
throw Error('\'crypto_params\' must be a string');
}
this.crypto = json.crypto || 'none';
this.crypto_params = json.crypto_params;
// protocol & protocol_params
if (typeof json.protocol !== 'string') {
throw Error('\'protocol\' must be a string');
}
if (typeof json.protocol_params !== 'string') {
throw Error('\'protocol_params\' must be a string');
}
this.protocol = json.protocol || 'none';
this.protocol_params = json.protocol_params;
// obfs & obfs_params
if (typeof json.obfs !== 'string') {
throw Error('\'obfs\' must be a string');
}
if (typeof json.obfs_params !== 'string') {
throw Error('\'obfs_params\' must be a string');
}
this.obfs = json.obfs || 'none';
this.obfs_params = json.obfs_params;
this.presets = json.presets;
// redirect
@ -211,17 +173,7 @@ export class Config {
global.__KEY__ = this.key;
global.__FRAME__ = this.frame;
global.__FRAME_PARAMS__ = this.frame_params;
global.__CRYPTO__ = this.crypto;
global.__CRYPTO_PARAMS__ = this.crypto_params;
global.__PROTOCOL__ = this.protocol;
global.__PROTOCOL_PARAMS__ = this.protocol_params;
global.__OBFS__ = this.obfs;
global.__OBFS_PARAMS__ = this.obfs_params;
global.__PRESETS__ = this.presets;
global.__REDIRECT__ = this.redirect;
global.__TIMEOUT__ = this.timeout;

@ -4,11 +4,6 @@ import logger from 'winston';
export const MIDDLEWARE_DIRECTION_UPWARD = 0;
export const MIDDLEWARE_DIRECTION_DOWNWARD = 1;
export const MIDDLEWARE_TYPE_FRAME = 0;
export const MIDDLEWARE_TYPE_CRYPTO = 1;
export const MIDDLEWARE_TYPE_PROTOCOL = 2;
export const MIDDLEWARE_TYPE_OBFS = 3;
/**
* abstraction of middleware
*/
@ -69,23 +64,14 @@ export class Middleware extends EventEmitter {
/**
* create an instance of Middleware
* @param type
* @param props
* @param name
* @param params
* @returns {Middleware}
*/
export function createMiddleware(type, props = []) {
const [preset, params] = {
[MIDDLEWARE_TYPE_FRAME]: [`frame/${__FRAME__}`, __FRAME_PARAMS__],
[MIDDLEWARE_TYPE_CRYPTO]: [`crypto/${__CRYPTO__}`, __CRYPTO_PARAMS__],
[MIDDLEWARE_TYPE_PROTOCOL]: [`protocol/${__PROTOCOL__}`, __PROTOCOL_PARAMS__],
[MIDDLEWARE_TYPE_OBFS]: [`obfs/${__OBFS__}`, __OBFS_PARAMS__]
}[type];
export function createMiddleware(name, params = {}) {
try {
const ImplClass = require(`../presets/${preset}`).default;
const _params = Array.isArray(params) ? params : params.split(',').filter((param) => param.length > 0);
const impl = new ImplClass(...props.concat(_params));
const ImplClass = require(`../presets/${name}`).default;
const impl = new ImplClass(params);
checkMiddleware(ImplClass.name, impl);

@ -3,7 +3,7 @@ import {
MIDDLEWARE_DIRECTION_UPWARD,
MIDDLEWARE_DIRECTION_DOWNWARD
} from './middleware';
import {PROCESSING_FAILED} from '../presets/actions';
import {PROCESSING_FAILED} from '../presets/defs';
export class Pipe extends EventEmitter {

@ -8,10 +8,6 @@ import {Profile} from './profile';
import {
MIDDLEWARE_DIRECTION_UPWARD,
MIDDLEWARE_DIRECTION_DOWNWARD,
MIDDLEWARE_TYPE_FRAME,
MIDDLEWARE_TYPE_CRYPTO,
MIDDLEWARE_TYPE_PROTOCOL,
MIDDLEWARE_TYPE_OBFS,
createMiddleware
} from './middleware';
@ -19,7 +15,7 @@ import {Utils} from '../utils';
import {
SOCKET_CONNECT_TO_DST,
PROCESSING_FAILED
} from '../presets/actions';
} from '../presets/defs';
import {
UdpRequestMessage
@ -252,12 +248,12 @@ export class Socket {
}
};
this._pipe = new Pipe(pipeProps);
this._pipe.setMiddlewares(MIDDLEWARE_DIRECTION_UPWARD, [
createMiddleware(MIDDLEWARE_TYPE_FRAME, [addr]),
createMiddleware(MIDDLEWARE_TYPE_CRYPTO),
createMiddleware(MIDDLEWARE_TYPE_PROTOCOL),
createMiddleware(MIDDLEWARE_TYPE_OBFS),
]);
this._pipe.setMiddlewares(MIDDLEWARE_DIRECTION_UPWARD,
__PRESETS__.map((preset, i) => createMiddleware(preset.name, {
...preset.params,
...(i === 0 ? addr : {})
}))
);
this._pipe.on(`next_${MIDDLEWARE_DIRECTION_UPWARD}`, (buf) => this.send(buf, __IS_CLIENT__));
this._pipe.on(`next_${MIDDLEWARE_DIRECTION_DOWNWARD}`, (buf) => this.send(buf, __IS_SERVER__));
}

@ -1,2 +0,0 @@
export const SOCKET_CONNECT_TO_DST = 'socket/connect/to/dst';
export const PROCESSING_FAILED = 'processing/failed';

@ -1,21 +0,0 @@
import {IPreset} from '../interface';
export default class NoneCrypto extends IPreset {
clientOut({buffer}) {
return buffer;
}
serverIn({buffer}) {
return buffer;
}
serverOut({buffer}) {
return buffer;
}
clientIn({buffer}) {
return buffer;
}
}

@ -1,3 +1,6 @@
export const SOCKET_CONNECT_TO_DST = 'socket/connect/to/dst';
export const PROCESSING_FAILED = 'processing/failed';
export class IPreset {
/**

@ -1,7 +1,7 @@
import fs from 'fs';
import readline from 'readline';
import crypto from 'crypto';
import {IPreset} from '../interface';
import {IPreset} from './defs';
class Faker {
@ -61,11 +61,15 @@ class Faker {
* Wrap packet with pre-shared HTTP header.
*
* @params
* file (String): A text file which contains several HTTP header paris.
* file: A text file which contains several HTTP header paris.
*
* @examples
* "obfs": "http"
* "obfs_params": "http-fake.txt"
* {
* "name": "http",
* "params": {
* "file": "http-fake.txt"
* }
* }
*
* @protocol
*
@ -83,7 +87,7 @@ class Faker {
* | Variable |
* +----------------------------+
*/
export default class HttpObfs extends IPreset {
export default class HttpPreset extends IPreset {
_isHandshakeDone = false;
@ -91,10 +95,10 @@ export default class HttpObfs extends IPreset {
_response = null;
constructor(file) {
constructor({file}) {
super();
if (typeof file === 'undefined') {
throw Error('\'obfs_params\' requires at least one parameter.');
throw Error('\'file\' parameter is required.');
}
this._file = file;
}

@ -1,21 +0,0 @@
import {IPreset} from '../interface';
export default class NoneObfs extends IPreset {
clientOut({buffer}) {
return buffer;
}
serverIn({buffer}) {
return buffer;
}
serverOut({buffer}) {
return buffer;
}
clientIn({buffer}) {
return buffer;
}
}

@ -1,21 +0,0 @@
import {IPreset} from '../interface';
export default class NoneProtocol extends IPreset {
clientOut({buffer}) {
return buffer;
}
serverIn({buffer}) {
return buffer;
}
serverOut({buffer}) {
return buffer;
}
clientIn({buffer}) {
return buffer;
}
}

@ -1,6 +1,6 @@
import crypto from 'crypto';
import {IPreset} from '../interface';
import {Utils, BYTE_ORDER_LE, AdvancedBuffer} from '../../utils';
import {IPreset} from './defs';
import {Utils, BYTE_ORDER_LE, AdvancedBuffer} from '../utils';
const NONCE_LEN = 12;
const TAG_LEN = 16;
@ -72,12 +72,17 @@ function HKDF(salt, ikm, info, length) {
* AEAD ciphers simultaneously provide confidentiality, integrity, and authenticity.
*
* @params
* cipher: The encryption/decryption method.
* method: The encryption/decryption method.
* info: An info for HKDF.
*
* @examples
* "protocol": "aead"
* "protocol_params": "aes-128-gcm,ss-subkey"
* {
* "name": "ss-aead-cipher",
* "params": {
* "method": "aes-128-gcm",
* "info": "ss-subkey"
* }
* }
*
* @protocol
*
@ -115,7 +120,7 @@ function HKDF(salt, ikm, info, length) {
* https://nodejs.org/dist/latest-v6.x/docs/api/crypto.html#crypto_cipher_getauthtag
* https://nodejs.org/dist/latest-v6.x/docs/api/crypto.html#crypto_decipher_setauthtag_buffer
*/
export default class AeadProtocol extends IPreset {
export default class SSAeadCipherPreset extends IPreset {
_cipherName = '';
@ -131,15 +136,15 @@ export default class AeadProtocol extends IPreset {
_adBuf = null;
constructor(cipher, info) {
constructor({method, info}) {
super();
if (typeof cipher === 'undefined' || cipher === '') {
throw Error('\'protocol_params\' requires [cipher] parameter.');
if (typeof method === 'undefined' || method === '') {
throw Error('\'method\' must be set.');
}
if (!ciphers.includes(cipher)) {
throw Error(`cipher \'${cipher}\' is not supported.`);
if (!ciphers.includes(method)) {
throw Error(`method \'${method}\' is not supported.`);
}
this._cipherName = cipher;
this._cipherName = method;
this._info = Buffer.from(info);
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this));

@ -1,15 +1,13 @@
import net from 'net';
import ip from 'ip';
import logger from 'winston';
import {IPreset} from '../interface';
import {SOCKET_CONNECT_TO_DST} from '../actions';
import {Utils} from '../../utils';
import {IPreset, SOCKET_CONNECT_TO_DST} from './defs';
import {Utils} from '../utils';
import {
ATYP_V4,
ATYP_V6,
ATYP_DOMAIN
} from '../../proxies/common';
} from '../proxies/common';
/**
* @description
@ -19,8 +17,10 @@ import {
* no
*
* @examples
* "frame": "origin"
* "frame_params": ""
* {
* "name": "ss-base",
* "params": {}
* }
*
* @protocol
*
@ -43,7 +43,7 @@ import {
* 2. When ATYP is 0x03, DST.ADDR[0] is len(DST.ADDR).
* 3. When ATYP is 0x04, DST.ADDR is a 16 bytes ipv6 address.
*/
export default class OriginFrame extends IPreset {
export default class SSBasePreset extends IPreset {
_isHandshakeDone = false;
@ -78,18 +78,14 @@ export default class OriginFrame extends IPreset {
}
}
serverIn({buffer, next, broadcast}) {
serverIn({buffer, next, broadcast, fail}) {
if (!this._isHandshakeDone) {
if (buffer.length < 7) {
logger.error(`invalid length: ${buffer.length}`);
fail(`invalid length: ${buffer.length}`);
return;
}
const atyp = buffer[0];
if (![ATYP_DOMAIN, ATYP_V4, ATYP_V6].includes(atyp)) {
logger.error(`invalid atyp: ${atyp}`);
return;
}
let addr, port; // string
let offset = 3;
@ -102,7 +98,7 @@ export default class OriginFrame extends IPreset {
break;
case ATYP_V6:
if (buffer.length < 19) {
logger.error(`invalid length: ${buffer.length}`);
fail(`invalid length: ${buffer.length}`);
return;
}
addr = ip.toString(buffer.slice(1, 16));
@ -112,7 +108,7 @@ export default class OriginFrame extends IPreset {
case ATYP_DOMAIN:
const domainLen = buffer[1];
if (buffer.length < domainLen + 4) {
logger.error(`invalid length: ${buffer.length}`);
fail(`invalid length: ${buffer.length}`);
return;
}
addr = buffer.slice(2, 2 + domainLen).toString();
@ -120,7 +116,7 @@ export default class OriginFrame extends IPreset {
offset += domainLen + 1;
break;
default:
logger.error(`invalid atyp: ${atyp}`);
fail(`invalid atyp: ${atyp}`);
return;
}

@ -1,6 +1,6 @@
import crypto from 'crypto';
import {IPreset} from '../interface';
import {Utils} from '../../utils';
import {IPreset} from './defs';
import {Utils} from '../utils';
const IV_LEN = 16;
@ -20,11 +20,15 @@ const ciphers = [
* Perform encrypt/decrypt using Node.js 'crypto' module(OpenSSL wrappers).
*
* @params
* cipher (String): Which cipher is picked from OpenSSL library.
* method: A cipher picked from OpenSSL library.
*
* @examples
* "crypto": "openssl"
* "crypto_params": "aes-256-cfb"
* {
* "name": "ss-stream-cipher",
* "params": {
* "method": "aes-256-cfb"
* }
* }
*
* @protocol
*
@ -52,7 +56,7 @@ const ciphers = [
* https://www.openssl.org/docs/man1.0.2/crypto/EVP_BytesToKey.html
* https://github.com/shadowsocks/shadowsocks/blob/master/shadowsocks/cryptor.py#L53
*/
export default class OpenSSLCrypto extends IPreset {
export default class SSStreamCipherPreset extends IPreset {
_cipherName = '';
@ -62,16 +66,18 @@ export default class OpenSSLCrypto extends IPreset {
_decipher = null;
constructor(cipherName) {
constructor({method}) {
super();
if (typeof cipherName !== 'string' || cipherName === '') {
throw Error('\'crypto_params\' requires [cipher] parameter.');
if (typeof method !== 'string' || method === '') {
throw Error('\'method\' must be set');
}
if (!ciphers.includes(cipherName)) {
throw Error(`cipher \'${cipherName}\' is not supported.`);
if (!ciphers.includes(method)) {
throw Error(`method \'${method}\' is not supported.`);
}
this._cipherName = method;
if (global.__KEY__) {
this._key = Utils.EVP_BytesToKey(__KEY__, this._cipherName.split('-')[1] / 8, IV_LEN);
}
this._cipherName = cipherName;
this._key = Utils.EVP_BytesToKey(__KEY__, this._cipherName.split('-')[1] / 8, IV_LEN);
}
beforeOut({buffer}) {