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-20 14:01:30 +00:00
|
|
|
import winston from 'winston';
|
2017-08-18 07:11:49 +00:00
|
|
|
import isPlainObject from 'lodash.isplainobject';
|
2017-08-16 03:48:33 +00:00
|
|
|
import {getPresetClassByName} from '../presets';
|
2017-08-20 14:01:30 +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-06-10 06:52:38 +00:00
|
|
|
|
2016-12-09 14:54:58 +00:00
|
|
|
export class Config {
|
|
|
|
|
2017-08-31 06:53:45 +00:00
|
|
|
static init(json) {
|
|
|
|
this._validate(json);
|
|
|
|
|
|
|
|
global.__LOCAL_HOST__ = json.host;
|
|
|
|
global.__LOCAL_PORT__ = json.port;
|
|
|
|
|
|
|
|
if (json.servers !== undefined) {
|
|
|
|
global.__SERVERS__ = json.servers.filter((server) => server.enabled);
|
|
|
|
global.__IS_CLIENT__ = true;
|
|
|
|
global.__IS_SERVER__ = false;
|
|
|
|
} else {
|
|
|
|
global.__IS_CLIENT__ = false;
|
|
|
|
global.__IS_SERVER__ = true;
|
|
|
|
this.initServer(json);
|
|
|
|
}
|
|
|
|
|
|
|
|
global.__TIMEOUT__ = (json.timeout !== undefined) ? json.timeout * 1e3 : 600 * 1e3;
|
2017-09-05 06:36:11 +00:00
|
|
|
global.__REDIRECT__ = (json.redirect !== '') ? json.redirect : null;
|
2017-08-31 06:53:45 +00:00
|
|
|
global.__WORKERS__ = (json.workers !== undefined) ? json.workers : 0;
|
|
|
|
global.__DNS_EXPIRE__ = (json.dns_expire !== undefined) ? json.dns_expire * 1e3 : DNS_DEFAULT_EXPIRE;
|
|
|
|
global.__ALL_CONFIG__ = json;
|
|
|
|
|
|
|
|
// dns
|
|
|
|
if (json.dns !== undefined && json.dns.length > 0) {
|
|
|
|
global.__DNS__ = json.dns;
|
|
|
|
dns.setServers(json.dns);
|
|
|
|
}
|
|
|
|
|
|
|
|
// log_path & log_level
|
|
|
|
const absolutePath = path.resolve(process.cwd(), json.log_path || '.');
|
|
|
|
let isFile = false;
|
|
|
|
if (fs.existsSync(absolutePath)) {
|
|
|
|
isFile = fs.statSync(absolutePath).isFile();
|
|
|
|
} else if (path.extname(absolutePath) !== '') {
|
|
|
|
isFile = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
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.configure({
|
|
|
|
level: __LOG_LEVEL__,
|
|
|
|
transports: [
|
|
|
|
new (winston.transports.Console)({
|
|
|
|
colorize: true,
|
|
|
|
prettyPrint: true
|
|
|
|
}),
|
|
|
|
new (require('winston-daily-rotate-file'))({
|
|
|
|
filename: __LOG_PATH__,
|
2017-09-03 04:22:25 +00:00
|
|
|
level: __LOG_LEVEL__
|
2017-08-31 06:53:45 +00:00
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static initServer(server) {
|
|
|
|
global.__TRANSPORT__ = (server.transport !== undefined) ? server.transport : 'tcp';
|
2017-09-19 06:04:40 +00:00
|
|
|
if (__TRANSPORT__ === 'tls') {
|
2017-08-31 06:53:45 +00:00
|
|
|
global.__TLS_CERT__ = fs.readFileSync(path.resolve(process.cwd(), server.tls_cert));
|
|
|
|
if (__IS_SERVER__) {
|
|
|
|
global.__TLS_KEY__ = fs.readFileSync(path.resolve(process.cwd(), server.tls_key));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
global.__SERVER_HOST__ = server.host;
|
|
|
|
global.__SERVER_PORT__ = server.port;
|
|
|
|
global.__KEY__ = server.key;
|
|
|
|
global.__PRESETS__ = server.presets;
|
|
|
|
}
|
|
|
|
|
|
|
|
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-03-09 15:22:47 +00:00
|
|
|
// servers
|
2017-08-16 02:06:54 +00:00
|
|
|
if (json.servers !== undefined) {
|
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-08-31 06:53:45 +00:00
|
|
|
servers.forEach(this._validateServer);
|
2017-04-15 09:01:34 +00:00
|
|
|
} else {
|
2017-08-31 06:53:45 +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-09-05 06:36:11 +00:00
|
|
|
if (json.redirect !== undefined && json.redirect !== '') {
|
|
|
|
if (typeof json.redirect !== 'string') {
|
|
|
|
throw Error('\'redirect\' must be a string');
|
|
|
|
}
|
|
|
|
const parts = json.redirect.split(':');
|
|
|
|
if (parts.length !== 2) {
|
|
|
|
throw Error('\'redirect\' must be "<host or ip>:<port>"');
|
|
|
|
}
|
|
|
|
const [host, port] = parts;
|
|
|
|
if (!isValidHostname(host) && !net.isIP(host)) {
|
|
|
|
throw Error('\'redirect\' host is invalid');
|
|
|
|
}
|
|
|
|
if (!isValidPort(+port)) {
|
|
|
|
throw Error('\'redirect\' port is invalid');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-08-31 06:53:45 +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-09-19 06:04:40 +00:00
|
|
|
if (!['tcp', 'tls', 'websocket'].includes(server.transport)) {
|
|
|
|
throw Error('\'server.transport\' must be "tcp", "tls" or "websocket"');
|
2017-08-15 15:05:32 +00:00
|
|
|
}
|
2017-08-22 02:54:25 +00:00
|
|
|
if (server.transport === 'tls') {
|
|
|
|
if (typeof server.tls_cert !== 'string') {
|
|
|
|
throw Error('\'server.tls_key\' must be a string');
|
2017-08-21 13:09:39 +00:00
|
|
|
}
|
2017-08-22 02:54:25 +00:00
|
|
|
if (server.tls_cert === '') {
|
|
|
|
throw Error('\'server.tls_cert\' cannot be empty');
|
2017-08-21 13:09:39 +00:00
|
|
|
}
|
2017-08-15 15:05:32 +00:00
|
|
|
}
|
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-08-31 06:53:45 +00:00
|
|
|
// check for existence of the preset
|
|
|
|
const PresetClass = getPresetClassByName(preset.name);
|
|
|
|
PresetClass.checkParams(preset.params || {});
|
2017-08-21 13:09:39 +00:00
|
|
|
}
|
2016-12-09 14:54:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|