refactor(utils): bring blinksocks-utils back to blinksocks

This commit is contained in:
Micooz 2017-06-11 22:55:46 +08:00
parent 77a085d44a
commit fffca228e7
14 changed files with 676 additions and 9 deletions

@ -26,10 +26,10 @@
"precommit": "npm test"
},
"dependencies": {
"blinksocks-utils": "^0.0.3",
"commander": "^2.9.0",
"ip": "^1.1.5",
"lodash.isequal": "^4.5.0",
"urijs": "^1.18.10",
"winston": "^2.3.1"
},
"devDependencies": {

@ -1,5 +1,5 @@
import ip from 'ip';
import {parseURI} from 'blinksocks-utils';
import {parseURI} from '../utils';
import {
IdentifierMessage,

@ -1,7 +1,7 @@
import dns from 'dns';
import fs from 'fs';
import net from 'net';
import {isValidPort} from 'blinksocks-utils';
import {isValidPort} from '../utils';
import {BLINKSOCKS_DIR, LOG_DIR, DEFAULT_LOG_LEVEL} from './constants';
/**

@ -1,6 +1,6 @@
import net from 'net';
import isEqual from 'lodash.isequal';
import {getRandomInt} from 'blinksocks-utils';
import {getRandomInt} from '../utils';
import logger from './logger';
import {Config} from './config';
import {ClientProxy} from './client-proxy';

@ -1,4 +1,4 @@
import {numberToBuffer} from 'blinksocks-utils';
import {numberToBuffer} from '../../utils';
import SSBasePreset from '../ss-base';
describe('SSBasePreset#constructor', function () {

@ -6,7 +6,7 @@ import {
getRandomInt,
getRandomChunks,
AdvancedBuffer
} from 'blinksocks-utils';
} from '../utils';
import {IPreset} from './defs';
const TLS_STAGE_HELLO = 1;

@ -6,7 +6,7 @@ import {
numberToBuffer,
BYTE_ORDER_LE,
AdvancedBuffer
} from 'blinksocks-utils';
} from '../utils';
import {IPreset} from './defs';
const NONCE_LEN = 12;

@ -1,5 +1,5 @@
import ip from 'ip';
import {isValidHostname, numberToBuffer} from 'blinksocks-utils';
import {isValidHostname, numberToBuffer} from '../utils';
import {IPreset, SOCKET_CONNECT_TO_DST} from './defs';
const ATYP_V4 = 0x01;

@ -1,5 +1,5 @@
import crypto from 'crypto';
import {EVP_BytesToKey} from 'blinksocks-utils';
import {EVP_BytesToKey} from '../utils';
import {IPreset} from './defs';
const IV_LEN = 16;

@ -0,0 +1,77 @@
import {AdvancedBuffer} from '../advanced-buffer';
describe('AdvancedBuffer#constructor', function () {
it('should throw when options is not given', function () {
expect(() => new AdvancedBuffer()).toThrow();
});
it('should throw when getPacketLength not Function', function () {
expect(() => new AdvancedBuffer({getPacketLength: null})).toThrow();
});
});
describe('AdvancedBuffer#put', function () {
it('should throw when pass a non-buffer to put() ', function () {
const buffer = new AdvancedBuffer({
getPacketLength: () => 0
});
expect(() => buffer.put()).toThrow();
});
it('should leave 0xff', function () {
const buffer = new AdvancedBuffer({
getPacketLength: (chunk) => {
return (chunk.length < 2) ? 0 : chunk.readUInt16BE(0);
}
});
const callback = jest.fn();
buffer.on('data', callback);
buffer.put(Buffer.from([0x00, 0x02])); // emit
buffer.put(Buffer.from([0x00]));
buffer.put(Buffer.from([0x02, 0x00])); // emit
buffer.put(Buffer.from([0x03]));
buffer.put(Buffer.from([0x00, 0xff])); // emit
expect(buffer.final().equals(Buffer.from([0xff]))).toBeTruthy();
expect(callback).toHaveBeenCalledTimes(3);
});
it('should drop the first byte', function () {
let dropped = false;
const buffer = new AdvancedBuffer({
getPacketLength: (chunk) => {
if (!dropped) {
dropped = true;
return chunk.slice(1);
} else {
return chunk.length > 1 ? chunk.readUInt16BE(0) : 0;
}
}
});
const callback = jest.fn();
buffer.on('data', callback);
buffer.put(Buffer.from([0xff, 0x00, 0x02])); // emit
buffer.put(Buffer.from([0x00]));
buffer.put(Buffer.from([0x02, 0x00])); // emit
buffer.put(Buffer.from([0x03]));
buffer.put(Buffer.from([0x00, 0xff])); // emit
expect(buffer.final().equals(Buffer.from([0xff]))).toBeTruthy();
expect(callback).toHaveBeenCalledTimes(3);
});
it('should drop buffer', function () {
const buffer = new AdvancedBuffer({
getPacketLength: () => -1
});
const callback = jest.fn();
buffer.on('data', callback);
buffer.put(Buffer.from([0x00]));
expect(buffer.final().equals(Buffer.alloc(0))).toBeTruthy();
});
});

@ -0,0 +1,222 @@
import ip from 'ip';
import {
numberToBuffer,
parseURI,
getRandomInt,
getRandomChunks,
getChunks,
getUTC,
hexStringToBuffer,
isValidHostname,
isValidPort,
md5,
hmac,
EVP_BytesToKey,
HKDF,
BYTE_ORDER_LE
} from '../common';
describe('numberToBuffer', function () {
it('should return <Buffer 01, 02> in big-endian when pass 258', function () {
expect(numberToBuffer(258).equals(Buffer.from([0x01, 0x02]))).toBe(true);
});
it('should return <Buffer 02, 01> in little-endian when pass 258', function () {
expect(numberToBuffer(258, 2, BYTE_ORDER_LE).equals(Buffer.from([0x02, 0x01]))).toBe(true);
});
it('should throw when len < 1', function () {
expect(() => numberToBuffer(255, 0)).toThrow();
});
it('should throw when pass an out of range number', function () {
expect(() => numberToBuffer(65535 + 1, 2)).toThrow();
});
});
describe('parseURI', function () {
it('should return expected object', function () {
let addr = parseURI('http://bing.com');
expect(addr).toMatchObject({
type: 3,
host: Buffer.from('bing.com'),
port: numberToBuffer(80)
});
addr = parseURI('bing.com');
expect(addr).toMatchObject({
type: 3,
host: Buffer.from('bing.com'),
port: numberToBuffer(80)
});
addr = parseURI('bing.com:443');
expect(addr).toMatchObject({
type: 3,
host: Buffer.from('bing.com'),
port: numberToBuffer(443)
});
addr = parseURI('https://bing.com');
expect(addr).toMatchObject({
type: 3,
host: Buffer.from('bing.com'),
port: numberToBuffer(443)
});
addr = parseURI('192.168.1.1:443');
expect(addr).toMatchObject({
type: 1,
host: ip.toBuffer('192.168.1.1'),
port: numberToBuffer(443)
});
addr = parseURI('https://[::1]:8080');
expect(addr).toMatchObject({
type: 4,
host: ip.toBuffer('::1'),
port: numberToBuffer(8080)
});
addr = parseURI('[::1]:443');
expect(addr).toMatchObject({
type: 4,
host: ip.toBuffer('::1'),
port: numberToBuffer(443)
});
});
});
describe('getRandomInt', function () {
it('should return a number', function () {
const number = getRandomInt(1, 2);
expect(number).toBeGreaterThanOrEqual(1);
expect(number).toBeLessThanOrEqual(2);
});
});
describe('getRandomChunks', function () {
it('should return expected random chunks', function () {
const chunks = getRandomChunks([1, 2, 3], 2, 2);
expect(chunks[0]).toEqual([1, 2]);
expect(chunks[1]).toEqual([3]);
});
});
describe('getChunks', function () {
it('should return expected chunks', function () {
const chunks = getChunks([1, 2, 3], 2);
expect(chunks[0]).toEqual([1, 2]);
expect(chunks[1]).toEqual([3]);
});
});
describe('getUTC', function () {
it('should return 4 bytes', function () {
const utc = getUTC();
expect(utc.length).toBe(4);
});
});
describe('hexStringToBuffer', function () {
it('should return expected buffer', function () {
const buffer = hexStringToBuffer('abcd');
expect(buffer.equals(Buffer.from([0xab, 0xcd]))).toBe(true);
});
});
describe('isValidHostname', function () {
it('should return false', function () {
expect(isValidHostname('')).toBe(false);
});
it('should return false', function () {
expect(isValidHostname('a.')).toBe(false);
});
it('should return false', function () {
expect(isValidHostname(`${'a'.repeat(64)}.com`)).toBe(false);
});
it('should return true', function () {
expect(isValidHostname(`${'a'.repeat(63)}.com`)).toBe(true);
});
});
describe('isValidPort', function () {
it('should return false', function () {
expect(isValidPort('')).toBe(false);
});
it('should return false', function () {
expect(isValidPort(-1)).toBe(false);
});
it('should return true', function () {
expect(isValidPort(80)).toBe(true);
});
});
describe('md5', function () {
it('should return expected buffer', function () {
const src = Buffer.from([1, 2, 3, 4]);
const dst = Buffer.from('08d6c05a21512a79a1dfeb9d2a8f262f', 'hex');
expect(md5(src).equals(dst)).toBe(true);
});
});
describe('hmac', function () {
it('should return expected buffer', function () {
const src = Buffer.from([1, 2, 3, 4]);
const dst = Buffer.from('7f8adea19a1ac02186fa895af72a7fa1', 'hex');
expect(hmac('md5', '', src).equals(dst)).toBe(true);
});
});
describe('EVP_BytesToKey', function () {
it('should return true', function () {
const password = Buffer.from('password');
const keyLen = 16;
const ivLen = 16;
const dst = Buffer.from('5f4dcc3b5aa765d61d8327deb882cf99', 'hex');
expect(EVP_BytesToKey(password, keyLen, ivLen).equals(dst)).toBe(true);
});
});
describe('HKDF', function () {
it('should return expected buffer', function () {
const hash = 'md5';
const salt = Buffer.alloc(0);
const ikm = Buffer.from([1, 2, 3, 4]);
const info = Buffer.alloc(0);
const length = 16;
const dst = Buffer.from('160ade10f83c4275fca1c8cd0583e4e6', 'hex');
expect(HKDF(hash, salt, ikm, info, length).equals(dst)).toBe(true);
});
});

@ -0,0 +1,119 @@
import EventEmitter from 'events';
/**
* Provide a mechanism for dealing with packet sticking and incomplete packet
* when receiving data from a socket in a long connection over TCP.
*
* @glossary
*
* [0xff, 0x00, 0x04, 0xff, ...] = packet
* | |
* +--------chunk---------+
*
* @options
* getPacketLength (Function): how to interpret the bytes to a number
*
* @methods
* .on('data', callback)
* .put(chunk);
*
* @examples
* const buffer = new AdvancedBuffer({
* getPacketLength: (bytes) => 0 // default
* });
*
* buffer.on('data', (all) => {
* // all = [0, 2]
* });
*
* buffer.put(Buffer.from([0, 2]));
* buffer.put(Buffer.from([0]))
* buffer.put...
*/
export class AdvancedBuffer extends EventEmitter {
// native Buffer instance to store our data
_buffer = Buffer.alloc(0);
_getPacketLength = null;
_nextLength = 0;
constructor(options = {}) {
super();
if (typeof options.getPacketLength !== 'function') {
throw Error('options.getPacketLength should be a function');
}
this._getPacketLength = options.getPacketLength;
}
/**
* put incoming chunk to the buffer, then digest them
* @param chunk{Buffer}
* @param args
*/
put(chunk, ...args) {
if (!(chunk instanceof Buffer)) {
throw Error('chunk must be a Buffer');
}
this._buffer = this._digest(Buffer.concat([this._buffer, chunk]), ...args);
}
/**
* get the rest of data in the buffer
* @returns {Buffer}
*/
final() {
return this._buffer;
}
/**
* digest a buffer, emit an event if a complete packet was resolved
* @param buffer{Buffer}: a buffer to be digested
* @param args
* @returns {Buffer}
*/
_digest(buffer, ...args) {
const expectLen = this._nextLength || this._getPacketLength(buffer, ...args);
if (expectLen === 0 || typeof expectLen === 'undefined') {
return buffer; // continue to put
}
if (expectLen === -1) {
return Buffer.alloc(0); // drop this one
}
if (expectLen instanceof Buffer) {
return this._digest(expectLen, ...args); // start from the new point
}
// luckily: <- [chunk]
if (buffer.length === expectLen) {
this.emit('data', buffer, ...args);
this._nextLength = 0;
return Buffer.alloc(0);
}
// incomplete packet: <- [chu]
if (buffer.length < expectLen) {
// prevent redundant calling to getPacketLength()
this._nextLength = expectLen;
// continue to put
return buffer;
}
// packet sticking: <- [chunk][chunk][chu...
if (buffer.length > expectLen) {
this.emit('data', buffer.slice(0, expectLen), ...args);
// note that each chunk has probably different length
this._nextLength = 0;
// digest buffer recursively
return this._digest(buffer.slice(expectLen), ...args);
}
}
}

247
src/utils/common.js Normal file

@ -0,0 +1,247 @@
import net from 'net';
import crypto from 'crypto';
import ip from 'ip';
import url from 'urijs';
export const ATYP_V4 = 1;
export const ATYP_DOMAIN = 3;
export const ATYP_V6 = 4;
export const BYTE_ORDER_BE = 0;
export const BYTE_ORDER_LE = 1;
/**
* convert a number to a buffer with specified length in specified byte order
* @param num
* @param len
* @param byteOrder
* @returns {Buffer}
*/
export function numberToBuffer(num, len = 2, byteOrder = BYTE_ORDER_BE) {
if (len < 1) {
throw Error('len must be greater than 0');
}
const isOutOfRange = num > parseInt(`0x${'ff'.repeat(len)}`);
if (isOutOfRange) {
throw Error(`Number ${num} is too long to store in a '${len}' length buffer`);
}
const buf = Buffer.alloc(len);
if (byteOrder === BYTE_ORDER_BE) {
buf.writeUIntBE(num, 0, len);
} else {
buf.writeUIntLE(num, 0, len);
}
return buf;
}
/**
* convert a http(s) url to an address with type, host and port
* @param uri
* @returns {{type: Number, host: Buffer, port: Buffer}}
*/
export function parseURI(uri) {
let _uri = uri;
let _port = null;
if (_uri.startsWith('http://')) {
_uri = _uri.substr(7);
_port = 80;
}
if (_uri.startsWith('https://')) {
_uri = _uri.substr(8);
_port = 443;
}
const parts = {};
url.parseHost(_uri, parts);
const {hostname, port} = parts;
const addrType = net.isIP(hostname) ? (net.isIPv4(hostname) ? ATYP_V4 : ATYP_V6) : ATYP_DOMAIN;
return {
type: addrType,
host: net.isIP(hostname) ? ip.toBuffer(hostname) : Buffer.from(hostname),
port: numberToBuffer(port || _port || 80)
};
}
/**
* returns a random integer in [min, max].
* @param min
* @param max
* @returns {Number}
*/
export function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.ceil(max);
return Math.floor(crypto.randomBytes(1)[0] / 0xff * (max - min + 1)) + min;
}
/**
* split buffer into chunks, each chunk size is picked randomly from [min, max]
* @param buffer
* @param min
* @param max
* @returns {Array<Buffer>}
*/
export function getRandomChunks(buffer, min, max) {
const totalLen = buffer.length;
const bufs = [];
let ptr = 0;
while (ptr < totalLen - 1) {
const offset = getRandomInt(min, max);
bufs.push(buffer.slice(ptr, ptr + offset));
ptr += offset;
}
if (ptr < totalLen) {
bufs.push(buffer.slice(ptr));
}
return bufs;
}
/**
* split buffer into chunks, the max chunk size is maxSize
* @param buffer
* @param maxSize
* @returns {Array<Buffer>}
*/
export function getChunks(buffer, maxSize) {
const totalLen = buffer.length;
const bufs = [];
let ptr = 0;
while (ptr < totalLen - 1) {
bufs.push(buffer.slice(ptr, ptr + maxSize));
ptr += maxSize;
}
if (ptr < totalLen) {
bufs.push(buffer.slice(ptr));
}
return bufs;
}
/**
* return UTC timestamp as buffer
* @returns {Buffer}
*/
export function getUTC() {
const ts = Math.floor((new Date()).getTime() / 1e3);
return numberToBuffer(ts, 4, BYTE_ORDER_BE);
}
/**
* convert string to buffer
* @param str
* @returns {Buffer}
*/
export function hexStringToBuffer(str) {
return Buffer.from(str, 'hex');
}
/**
* verify hostname
*
* @param hostname
* @returns {boolean}
*
* @reference
* http://stackoverflow.com/questions/1755144/how-to-validate-domain-name-in-php
*/
export function isValidHostname(hostname) {
// overall length check
if (hostname.length < 1 || hostname.length > 253) {
return false;
}
// valid chars check
if (/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i.test(hostname) === false) {
return false;
}
// length of each label
if (/^[^.]{1,63}(\.[^.]{1,63})*$/.test(hostname) === false) {
return false;
}
return true;
}
/**
* whether a port is valid or not
* @param port
* @returns {boolean}
*/
export function isValidPort(port) {
if (typeof port !== 'number') {
return false;
}
if (port < 0 || port > 65535) {
return false;
}
return true;
}
/**
* md5 message digest
* @param buffer
* @returns {*}
*/
export function md5(buffer) {
const md5 = crypto.createHash('md5');
md5.update(buffer);
return md5.digest();
}
/**
* calculate the HMAC from key and message
* @param algorithm
* @param key
* @param buffer
* @returns {Buffer}
*/
export function hmac(algorithm, key, buffer) {
const hmac = crypto.createHmac(algorithm, key);
return hmac.update(buffer).digest();
}
/**
* EVP_BytesToKey with the digest algorithm set to MD5, one iteration, and no salt
*
* @algorithm
* D_i = HASH^count(D_(i-1) || data || salt)
*/
export function EVP_BytesToKey(password, keyLen, ivLen) {
let _data = Buffer.from(password);
let i = 0;
const bufs = [];
while (Buffer.concat(bufs).length < (keyLen + ivLen)) {
if (i > 0) {
_data = Buffer.concat([bufs[i - 1], Buffer.from(password)]);
}
bufs.push(md5(_data));
i += 1;
}
return Buffer.concat(bufs).slice(0, keyLen);
}
/**
* HMAC-based Extract-and-Expand Key Derivation Function
* @param hash, the message digest algorithm
* @param salt, a non-secret random value
* @param ikm, input keying material
* @param info, optional context and application specific information
* @param length, length of output keying material in octets
* @returns {Buffer}
*/
export function HKDF(hash, salt, ikm, info, length) {
// Step 1: "extract" to fixed length pseudo-random key(prk)
const prk = hmac(hash, salt, ikm);
// Step 2: "expand" prk to several pseudo-random keys(okm)
let t = Buffer.alloc(0);
let okm = Buffer.alloc(0);
for (let i = 0; i < Math.ceil(length / prk.length); ++i) {
t = hmac(hash, prk, Buffer.concat([t, info, Buffer.alloc(1, i + 1)]));
okm = Buffer.concat([okm, t]);
}
// Step 3: crop okm to desired length
return okm.slice(0, length);
}

2
src/utils/index.js Normal file

@ -0,0 +1,2 @@
export * from './common';
export * from './advanced-buffer';