2017-06-11 10:44:19 +00:00
|
|
|
import dns from 'dns';
|
2016-12-09 14:54:58 +00:00
|
|
|
import fs from 'fs';
|
2017-08-15 15:05:32 +00:00
|
|
|
import path from 'path';
|
2017-08-14 07:54:19 +00:00
|
|
|
import os from 'os';
|
2017-06-11 10:44:19 +00:00
|
|
|
import net from 'net';
|
2017-08-18 07:11:49 +00:00
|
|
|
import isPlainObject from 'lodash.isplainobject';
|
|
|
|
import {getBehaviourClassByName, BEHAVIOUR_EVENT_ON_PRESET_FAILED, behaviourEvents} from '../behaviours';
|
2017-08-16 03:48:33 +00:00
|
|
|
import {getPresetClassByName} from '../presets';
|
2017-08-18 07:11:49 +00:00
|
|
|
import {isValidHostname, isValidPort, Logger} from '../utils';
|
2017-08-02 08:14:04 +00:00
|
|
|
import {DNS_DEFAULT_EXPIRE} from './dns-cache';
|
2016-12-09 14:54:58 +00:00
|
|
|
|
2017-08-16 02:06:54 +00:00
|
|
|
export const DEFAULT_LOG_LEVEL = 'info';
|
2017-08-18 07:11:49 +00:00
|
|
|
export const DEFAULT_BEHAVIOURS = {
|
|
|
|
[BEHAVIOUR_EVENT_ON_PRESET_FAILED]: {
|
2017-08-20 09:13:25 +00:00
|
|
|
'name': 'random-timeout',
|
|
|
|
'params': {
|
|
|
|
'min': 10,
|
|
|
|
'max': 40
|
|
|
|
}
|
2017-08-18 07:11:49 +00:00
|
|
|
}
|
|
|
|
};
|
2017-06-10 06:52:38 +00:00
|
|
|
|
2016-12-09 14:54:58 +00:00
|
|
|
export class Config {
|
|
|
|
|
2017-06-02 09:49:36 +00:00
|
|
|
static validate(json) {
|
2017-08-18 07:11:49 +00:00
|
|
|
if (!isPlainObject(json)) {
|
|
|
|
throw Error('invalid configuration file');
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 15:29:18 +00:00
|
|
|
// host
|
2016-12-09 14:54:58 +00:00
|
|
|
if (typeof json.host !== 'string' || json.host === '') {
|
|
|
|
throw Error('\'host\' must be provided and is not empty');
|
|
|
|
}
|
|
|
|
|
2017-02-05 15:29:18 +00:00
|
|
|
// port
|
2017-04-23 11:47:05 +00:00
|
|
|
if (!isValidPort(json.port)) {
|
2017-03-29 08:17:09 +00:00
|
|
|
throw Error('\'port\' is invalid');
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
2017-08-18 07:11:49 +00:00
|
|
|
// behaviours
|
|
|
|
if (json.behaviours !== undefined) {
|
|
|
|
if (!isPlainObject(json.behaviours)) {
|
|
|
|
throw Error('\'behaviours\' is invalid');
|
|
|
|
}
|
|
|
|
const events = Object.keys(json.behaviours);
|
|
|
|
for (const event of events) {
|
|
|
|
if (!behaviourEvents.includes(event)) {
|
|
|
|
throw Error(`unrecognized behaviour event: "${event}"`);
|
|
|
|
}
|
|
|
|
const {name, params} = json.behaviours[event];
|
|
|
|
if (typeof name !== 'string') {
|
|
|
|
throw Error('\'behaviours[].name\' must be a string');
|
|
|
|
}
|
|
|
|
if (name === '') {
|
|
|
|
throw Error('\'behaviours[].name\' cannot be empty');
|
|
|
|
}
|
|
|
|
if (params !== undefined && !isPlainObject(params)) {
|
|
|
|
throw Error('\'behaviours[].params\' must be an plain object');
|
|
|
|
}
|
|
|
|
const behaviour = getBehaviourClassByName(name);
|
|
|
|
delete new behaviour(params || {});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-09 15:22:47 +00:00
|
|
|
// servers
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.servers !== undefined) {
|
2017-02-05 15:29:18 +00:00
|
|
|
|
2017-03-09 15:22:47 +00:00
|
|
|
if (!Array.isArray(json.servers)) {
|
2017-03-24 09:09:37 +00:00
|
|
|
throw Error('\'servers\' must be provided as an array');
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
2017-04-16 09:38:57 +00:00
|
|
|
const servers = json.servers.filter((server) => server.enabled === true);
|
|
|
|
|
|
|
|
if (servers.length < 1) {
|
|
|
|
throw Error('\'servers\' must have at least one enabled item');
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
2017-03-09 15:22:47 +00:00
|
|
|
|
2017-06-02 09:49:36 +00:00
|
|
|
servers.forEach(this.validateServer);
|
2017-02-05 15:29:18 +00:00
|
|
|
|
2017-04-15 09:01:34 +00:00
|
|
|
} else {
|
2017-06-02 09:49:36 +00:00
|
|
|
this.validateServer(json);
|
2017-02-23 13:39:56 +00:00
|
|
|
}
|
|
|
|
|
2017-04-12 08:04:19 +00:00
|
|
|
// timeout
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.timeout !== undefined) {
|
2017-08-15 15:05:32 +00:00
|
|
|
if (typeof json.timeout !== 'number') {
|
|
|
|
throw Error('\'timeout\' must be a number');
|
|
|
|
}
|
|
|
|
if (json.timeout < 1) {
|
|
|
|
throw Error('\'timeout\' must be greater than 0');
|
|
|
|
}
|
|
|
|
if (json.timeout < 60) {
|
|
|
|
console.warn(`==> [config] 'timeout' is too short, is ${json.timeout}s expected?`);
|
|
|
|
}
|
2017-04-12 08:04:19 +00:00
|
|
|
}
|
|
|
|
|
2017-08-15 15:05:32 +00:00
|
|
|
// log_path
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.log_path !== undefined) {
|
2017-08-15 15:05:32 +00:00
|
|
|
if (typeof json.log_path !== 'string') {
|
|
|
|
throw Error('\'log_path\' must be a string');
|
|
|
|
}
|
2017-04-12 08:04:19 +00:00
|
|
|
}
|
|
|
|
|
2017-08-15 15:05:32 +00:00
|
|
|
// log_level
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.log_level !== undefined) {
|
2017-08-15 15:05:32 +00:00
|
|
|
const levels = ['error', 'warn', 'info', 'verbose', 'debug', 'silly'];
|
|
|
|
if (!levels.includes(json.log_level)) {
|
|
|
|
throw Error(`'log_level' must be one of [${levels.toString()}]`);
|
|
|
|
}
|
2017-04-12 08:04:19 +00:00
|
|
|
}
|
2017-08-02 08:14:04 +00:00
|
|
|
|
2017-08-14 07:54:19 +00:00
|
|
|
// workers
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.workers !== undefined) {
|
2017-08-14 07:54:19 +00:00
|
|
|
if (typeof json.workers !== 'number') {
|
|
|
|
throw Error('\'workers\' must be a number');
|
|
|
|
}
|
2017-08-14 09:25:00 +00:00
|
|
|
if (json.workers < 0) {
|
|
|
|
throw Error('\'workers\' must be an integer');
|
2017-08-14 07:54:19 +00:00
|
|
|
}
|
|
|
|
if (json.workers > os.cpus().length) {
|
|
|
|
console.warn(`==> [config] 'workers' is greater than the number of cpus, is ${json.workers} workers expected?`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-16 02:06:54 +00:00
|
|
|
// dns
|
|
|
|
if (json.dns !== undefined) {
|
|
|
|
if (!Array.isArray(json.dns)) {
|
|
|
|
throw Error('\'dns\' must be an array');
|
|
|
|
}
|
|
|
|
for (const ip of json.dns) {
|
|
|
|
if (!net.isIP(ip)) {
|
|
|
|
throw Error(`"${ip}" is not an ip address`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-02 08:14:04 +00:00
|
|
|
// dns_expire
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.dns_expire !== undefined) {
|
2017-08-02 08:14:04 +00:00
|
|
|
if (typeof json.dns_expire !== 'number') {
|
|
|
|
throw Error('\'dns_expire\' must be a number');
|
|
|
|
}
|
|
|
|
if (json.dns_expire < 0) {
|
|
|
|
throw Error('\'dns_expire\' must be greater or equal to 0');
|
|
|
|
}
|
|
|
|
if (json.dns_expire > 24 * 60 * 60) {
|
|
|
|
console.warn(`==> [config] 'dns_expire' is too long, is ${json.dns_expire}s expected?`);
|
|
|
|
}
|
|
|
|
}
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
2017-06-02 09:49:36 +00:00
|
|
|
static validateServer(server) {
|
2017-04-20 09:20:42 +00:00
|
|
|
// transport
|
2017-08-16 02:06:54 +00:00
|
|
|
if (server.transport !== undefined) {
|
2017-08-15 15:05:32 +00:00
|
|
|
if (typeof server.transport !== 'string') {
|
|
|
|
throw Error('\'server.transport\' must be a string');
|
|
|
|
}
|
|
|
|
if (!['tcp', 'udp'].includes(server.transport.toLowerCase())) {
|
|
|
|
throw Error('\'server.transport\' must be one of "tcp" or "udp"');
|
|
|
|
}
|
2017-04-20 09:20:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// host
|
2017-08-18 07:11:49 +00:00
|
|
|
if (!isValidHostname(server.host)) {
|
|
|
|
throw Error('\'server.host\' is invalid');
|
2017-04-20 09:20:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// port
|
2017-04-23 11:47:05 +00:00
|
|
|
if (!isValidPort(server.port)) {
|
2017-04-20 09:20:42 +00:00
|
|
|
throw Error('\'server.port\' is invalid');
|
|
|
|
}
|
|
|
|
|
2017-04-15 09:01:34 +00:00
|
|
|
// key
|
2017-08-16 02:06:54 +00:00
|
|
|
if (typeof server.key !== 'string' || server.key === '') {
|
|
|
|
throw Error('\'server.key\' must be a non-empty string');
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
2017-02-26 08:10:12 +00:00
|
|
|
|
2017-06-02 09:49:36 +00:00
|
|
|
// presets
|
2017-04-15 09:01:34 +00:00
|
|
|
if (!Array.isArray(server.presets)) {
|
2017-04-20 09:20:42 +00:00
|
|
|
throw Error('\'server.presets\' must be an array');
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
2017-02-26 08:10:12 +00:00
|
|
|
|
2017-04-15 09:01:34 +00:00
|
|
|
if (server.presets.length < 1) {
|
2017-04-20 09:20:42 +00:00
|
|
|
throw Error('\'server.presets\' must contain at least one preset');
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
2017-03-29 08:17:09 +00:00
|
|
|
|
2017-06-02 09:49:36 +00:00
|
|
|
// presets[].parameters
|
2017-04-15 09:01:34 +00:00
|
|
|
for (const preset of server.presets) {
|
|
|
|
const {name, params} = preset;
|
|
|
|
|
2017-08-18 07:11:49 +00:00
|
|
|
if (typeof name !== 'string') {
|
2017-04-20 09:20:42 +00:00
|
|
|
throw Error('\'server.presets[].name\' must be a string');
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (name === '') {
|
2017-04-20 09:20:42 +00:00
|
|
|
throw Error('\'server.presets[].name\' cannot be empty');
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
|
|
|
|
2017-08-16 02:06:54 +00:00
|
|
|
if (params !== undefined) {
|
2017-08-18 07:11:49 +00:00
|
|
|
if (!isPlainObject(params)) {
|
2017-08-16 02:06:54 +00:00
|
|
|
throw Error('\'server.presets[].params\' must be an plain object');
|
2017-08-09 06:39:31 +00:00
|
|
|
}
|
2017-06-02 09:49:36 +00:00
|
|
|
}
|
|
|
|
|
2017-04-15 09:01:34 +00:00
|
|
|
// 1. check for the existence of the preset
|
2017-08-16 03:48:33 +00:00
|
|
|
const ps = getPresetClassByName(preset.name);
|
2017-04-15 09:01:34 +00:00
|
|
|
|
2017-08-10 14:07:15 +00:00
|
|
|
// 2. check parameters
|
|
|
|
delete new ps(params || {});
|
2017-04-15 09:01:34 +00:00
|
|
|
}
|
2017-06-02 09:49:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static init(json) {
|
|
|
|
this.validate(json);
|
|
|
|
|
|
|
|
global.__LOCAL_HOST__ = json.host;
|
|
|
|
global.__LOCAL_PORT__ = json.port;
|
|
|
|
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.servers !== undefined) {
|
2017-06-10 06:52:38 +00:00
|
|
|
global.__SERVERS__ = json.servers.filter((server) => server.enabled);
|
2017-06-02 09:49:36 +00:00
|
|
|
global.__IS_CLIENT__ = true;
|
|
|
|
} else {
|
|
|
|
global.__IS_CLIENT__ = false;
|
|
|
|
this.initServer(json);
|
|
|
|
}
|
|
|
|
|
2017-06-10 06:52:38 +00:00
|
|
|
global.__IS_SERVER__ = !global.__IS_CLIENT__;
|
2017-08-15 15:05:32 +00:00
|
|
|
global.__TIMEOUT__ = (json.timeout !== undefined) ? json.timeout * 1e3 : 600 * 1e3;
|
2017-08-14 09:39:37 +00:00
|
|
|
global.__WORKERS__ = (json.workers !== undefined) ? json.workers : 0;
|
2017-08-02 08:14:04 +00:00
|
|
|
global.__DNS_EXPIRE__ = (json.dns_expire !== undefined) ? json.dns_expire * 1e3 : DNS_DEFAULT_EXPIRE;
|
2017-06-02 09:49:36 +00:00
|
|
|
global.__ALL_CONFIG__ = json;
|
2017-06-11 10:44:19 +00:00
|
|
|
|
2017-08-18 07:11:49 +00:00
|
|
|
// dns
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.dns !== undefined && json.dns.length > 0) {
|
2017-06-11 10:44:19 +00:00
|
|
|
global.__DNS__ = json.dns;
|
|
|
|
dns.setServers(json.dns);
|
|
|
|
}
|
2017-08-18 07:11:49 +00:00
|
|
|
|
|
|
|
// log_path & log_level
|
|
|
|
const absolutePath = path.resolve(process.cwd(), json.log_path || '.');
|
|
|
|
const isFile = fs.statSync(absolutePath).isFile();
|
|
|
|
global.__LOG_PATH__ = isFile ? absolutePath : path.join(absolutePath, `bs-${__IS_CLIENT__ ? 'client' : 'server'}.log`);
|
|
|
|
global.__LOG_LEVEL__ = (json.log_level !== undefined) ? json.log_level : DEFAULT_LOG_LEVEL;
|
|
|
|
Logger.init({file: __LOG_PATH__, level: __LOG_LEVEL__});
|
|
|
|
|
|
|
|
// behaviours
|
|
|
|
const behaviours = {
|
|
|
|
...DEFAULT_BEHAVIOURS,
|
|
|
|
...(json.behaviours !== undefined ? json.behaviours : {})
|
|
|
|
};
|
|
|
|
const events = Object.keys(behaviours);
|
|
|
|
global.__BEHAVIOURS__ = {};
|
|
|
|
for (const ev of events) {
|
|
|
|
const clazz = getBehaviourClassByName(behaviours[ev].name);
|
|
|
|
global.__BEHAVIOURS__[ev] = new clazz(behaviours[ev].params || {});
|
|
|
|
}
|
2017-06-02 09:49:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static initServer(server) {
|
|
|
|
this.validateServer(server);
|
2017-04-15 09:01:34 +00:00
|
|
|
|
2017-08-15 15:05:32 +00:00
|
|
|
global.__TRANSPORT__ = (server.transport !== undefined) ? server.transport : 'tcp';
|
2017-06-02 09:49:36 +00:00
|
|
|
global.__SERVER_HOST__ = server.host;
|
|
|
|
global.__SERVER_PORT__ = server.port;
|
|
|
|
global.__KEY__ = server.key;
|
2017-04-15 09:01:34 +00:00
|
|
|
global.__PRESETS__ = server.presets;
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|