blinksocks/lib/core/hub.js
2019-02-13 13:18:48 +08:00

607 lines
15 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Hub = exports.CONN_STAGE_ERROR = exports.CONN_STAGE_FINISH = exports.CONN_STAGE_TRANSFER = exports.CONN_STAGE_INIT = exports.MAX_CONNECTIONS = void 0;
var _dgram = _interopRequireDefault(require("dgram"));
var _net = _interopRequireDefault(require("net"));
var _http = _interopRequireDefault(require("http"));
var _https = _interopRequireDefault(require("https"));
var _url = require("url");
var _tls = _interopRequireDefault(require("tls"));
var _ws = _interopRequireDefault(require("ws"));
var _lruCache = _interopRequireDefault(require("lru-cache"));
var _lodash = _interopRequireDefault(require("lodash.uniqueid"));
var _config = require("./config");
var _relay = require("./relay");
var _muxRelay = require("./mux-relay");
var _utils = require("../utils");
var _proxies = require("../proxies");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
const MAX_CONNECTIONS = 50;
exports.MAX_CONNECTIONS = MAX_CONNECTIONS;
const CONN_STAGE_INIT = 0;
exports.CONN_STAGE_INIT = CONN_STAGE_INIT;
const CONN_STAGE_TRANSFER = 1;
exports.CONN_STAGE_TRANSFER = CONN_STAGE_TRANSFER;
const CONN_STAGE_FINISH = 2;
exports.CONN_STAGE_FINISH = CONN_STAGE_FINISH;
const CONN_STAGE_ERROR = 3;
exports.CONN_STAGE_ERROR = CONN_STAGE_ERROR;
class Hub {
constructor(config) {
_defineProperty(this, "_config", null);
_defineProperty(this, "_tcpServer", null);
_defineProperty(this, "_udpServer", null);
_defineProperty(this, "_tcpRelays", new Map());
_defineProperty(this, "_udpRelays", null);
_defineProperty(this, "_upSpeedTester", null);
_defineProperty(this, "_dlSpeedTester", null);
_defineProperty(this, "_totalRead", 0);
_defineProperty(this, "_totalWritten", 0);
_defineProperty(this, "_connQueue", []);
_defineProperty(this, "_udpCleanerTimer", null);
_defineProperty(this, "_onRead", size => {
this._totalRead += size;
this._dlSpeedTester.feed(size);
});
_defineProperty(this, "_onWrite", size => {
this._totalWritten += size;
this._upSpeedTester.feed(size);
});
_defineProperty(this, "_onClientConnection", (conn, proxyRequest) => {
const source = this._getSourceAddress(conn);
const updateConnStatus = (event, extra) => this._updateConnStatus(event, source, extra);
_utils.logger.verbose(`[hub] [${source.host}:${source.port}] connected`);
updateConnStatus('new');
updateConnStatus('target', {
host: proxyRequest.host,
port: proxyRequest.port
});
const context = {
conn,
source
};
if (this._config.mux && this._tcpRelays.size > 0) {
const {
value: relay
} = this._tcpRelays.values().next();
relay.addInboundOnClient(context, proxyRequest);
return;
}
const relay = this._createRelay(source);
relay.__id = (0, _lodash.default)('relay_');
relay.on('_error', err => updateConnStatus('error', err.message));
relay.on('_read', this._onRead);
relay.on('_write', this._onWrite);
relay.on('close', () => {
updateConnStatus('close');
this._tcpRelays.delete(relay.__id);
});
relay.addInboundOnClient(context, proxyRequest);
this._tcpRelays.set(relay.__id, relay);
});
_defineProperty(this, "_onServerConnection", conn => {
const source = this._getSourceAddress(conn);
const updateConnStatus = (event, extra) => this._updateConnStatus(event, source, extra);
_utils.logger.verbose(`[hub] [${source.host}:${source.port}] connected`);
updateConnStatus('new');
const relay = this._createRelay(source);
relay.__id = (0, _lodash.default)('relay_');
relay.on('_error', err => updateConnStatus('error', err.message));
relay.on('_connect', targetAddress => updateConnStatus('target', targetAddress));
relay.on('_read', this._onRead);
relay.on('_write', this._onWrite);
relay.on('close', () => {
updateConnStatus('close');
this._tcpRelays.delete(relay.__id);
});
relay.addInboundOnServer({
source,
conn
});
this._tcpRelays.set(relay.__id, relay);
});
this._config = new _config.Config(config);
this._udpRelays = new _lruCache.default({
max: 500,
maxAge: 1e5,
dispose: (_, relay) => relay.destroy()
});
this._upSpeedTester = new _utils.SpeedTester();
this._dlSpeedTester = new _utils.SpeedTester();
}
async run() {
await this._config._ready;
if (this._tcpServer !== null) {
await this.terminate();
}
await this._createServer();
}
async terminate() {
this._udpRelays.reset();
this._tcpRelays.forEach(relay => relay.destroy());
this._tcpRelays.clear();
this._udpServer && this._udpServer.close();
this._tcpServer.close();
this._connQueue = [];
clearInterval(this._udpCleanerTimer);
_utils.logger.info('[hub] shutdown');
}
async getConnections() {
return new Promise((resolve, reject) => {
if (this._tcpServer) {
this._tcpServer.getConnections((err, count) => {
if (err) {
reject(err);
} else {
resolve(count);
}
});
} else {
resolve(0);
}
});
}
getTotalRead() {
return this._totalRead;
}
getTotalWritten() {
return this._totalWritten;
}
getUploadSpeed() {
return this._upSpeedTester.getSpeed();
}
getDownloadSpeed() {
return this._dlSpeedTester.getSpeed();
}
getConnStatuses() {
return this._connQueue;
}
async _createServer() {
const {
is_client,
is_server,
local_protocol,
mux
} = this._config;
if (mux) {
_utils.logger.info('[hub] multiplexing enabled');
}
if (is_client) {
this._tcpServer = await this._createServerOnClient();
} else {
this._tcpServer = await this._createServerOnServer();
}
if (is_server || ['socks', 'socks5'].includes(local_protocol)) {
this._udpServer = await this._createUdpServer();
}
}
async _createServerOnClient() {
return new Promise((resolve, reject) => {
const {
local_protocol,
local_search_params,
local_host,
local_port,
local_pathname
} = this._config;
const {
local_username: username,
local_password: password
} = this._config;
const {
https_key,
https_cert
} = this._config;
let server = null;
switch (local_protocol) {
case 'tcp':
{
const forward = local_search_params.get('forward');
const {
hostname,
port
} = new _url.URL('tcp://' + forward);
const forwardHost = hostname;
const forwardPort = +port;
server = _proxies.tcp.createServer({
forwardHost,
forwardPort
});
break;
}
case 'socks':
case 'socks5':
case 'socks4':
case 'socks4a':
server = _proxies.socks.createServer({
bindAddress: local_host,
bindPort: local_port,
username,
password
});
break;
case 'http':
case 'https':
server = _proxies.http.createServer({
secure: local_protocol === 'https',
https_key,
https_cert,
username,
password
});
break;
default:
return reject(Error(`unsupported protocol: "${local_protocol}"`));
}
const address = {
host: local_host,
port: local_port
};
server.on('proxyConnection', this._onClientConnection);
server.on('error', reject);
server.listen(address, () => {
const service = `${local_protocol}://${local_host}:${local_port}` + (local_pathname ? local_pathname : '');
_utils.logger.info(`[hub] blinksocks client is running at ${service}`);
resolve(server);
});
});
}
async _createServerOnServer() {
const {
local_protocol,
local_host,
local_port,
local_pathname,
tls_key,
tls_cert
} = this._config;
return new Promise((resolve, reject) => {
let server = null;
switch (local_protocol) {
case 'tcp':
{
server = _net.default.createServer();
server.on('connection', this._onServerConnection);
break;
}
case 'wss':
case 'ws':
{
if (local_protocol === 'wss') {
server = _https.default.createServer({
key: tls_key,
cert: tls_cert
});
} else {
server = _http.default.createServer();
}
const wss = new _ws.default.Server({
server: server,
path: local_pathname,
perMessageDeflate: false
});
wss.getConnections = wss._server.getConnections.bind(wss._server);
wss.on('connection', (ws, req) => {
ws.remoteAddress = req.connection.remoteAddress;
ws.remotePort = req.connection.remotePort;
this._onServerConnection(ws);
});
break;
}
case 'tls':
{
server = _tls.default.createServer({
key: tls_key,
cert: tls_cert
});
server.on('secureConnection', this._onServerConnection);
break;
}
case 'h2':
{
server = require('http2').createSecureServer({
key: tls_key,
cert: tls_cert
});
server.on('stream', stream => this._onServerConnection(stream));
break;
}
default:
return reject(Error(`unsupported protocol: "${local_protocol}"`));
}
const address = {
host: local_host,
port: local_port
};
server.on('error', reject);
server.listen(address, () => {
const service = `${local_protocol}://${local_host}:${local_port}` + (local_pathname ? local_pathname : '');
_utils.logger.info(`[hub] blinksocks server is running at ${service}`);
resolve(server);
});
});
}
async _createUdpServer() {
return new Promise((resolve, reject) => {
const relays = this._udpRelays;
const server = _dgram.default.createSocket('udp4');
clearInterval(this._udpCleanerTimer);
this._udpCleanerTimer = setInterval(() => relays.prune(), 5e3);
server.on('message', (msg, rinfo) => {
const {
address,
port
} = rinfo;
let proxyRequest = null;
let packet = msg;
if (this._config.is_client) {
const parsed = _proxies.socks.parseSocks5UdpRequest(msg);
if (parsed === null) {
_utils.logger.warn(`[hub] [${address}:${port}] drop invalid udp packet: ${(0, _utils.dumpHex)(msg)}`);
return;
}
const {
host,
port,
data
} = parsed;
proxyRequest = {
host,
port
};
packet = data;
}
const key = `${address}:${port}`;
let relay = relays.get(key);
if (relay === undefined) {
const source = {
host: address,
port: port
};
const context = {
conn: server,
source
};
relay = this._createUdpRelay(source);
if (this._config.is_client) {
relay.addInboundOnClient(context, proxyRequest);
} else {
relay.addInboundOnServer(context);
}
relays.set(key, relay);
}
if (relay._inbound) {
relay._inbound.onReceive(packet, rinfo);
}
});
server.on('error', reject);
if (this._config.is_client) {
server.send = (send => (data, port, host, isSs, ...args) => {
let packet = null;
if (isSs) {
packet = Buffer.from([0x00, 0x00, 0x00, ...data]);
} else {
packet = _proxies.socks.encodeSocks5UdpResponse({
host,
port,
data
});
}
send.call(server, packet, port, host, ...args);
})(server.send);
}
server.bind({
address: this._config.local_host,
port: this._config.local_port
}, () => {
const service = `udp://${this._config.local_host}:${this._config.local_port}`;
_utils.logger.info(`[hub] blinksocks udp server is running at ${service}`);
resolve(server);
});
});
}
_getSourceAddress(conn) {
let sourceHost, sourcePort;
if (conn.session) {
sourceHost = conn.session.socket.remoteAddress;
sourcePort = conn.session.socket.remotePort;
} else {
sourceHost = conn.remoteAddress;
sourcePort = conn.remotePort;
}
return {
host: sourceHost,
port: sourcePort
};
}
_createRelay(source) {
const props = {
source: source,
config: this._config,
transport: this._config.server_protocol,
presets: this._config.presets
};
return this._config.mux ? new _muxRelay.MuxRelay(props) : new _relay.Relay(props);
}
_createUdpRelay(source) {
return new _relay.Relay({
source,
config: this._config,
transport: 'udp',
presets: this._config.udp_presets
});
}
_updateConnStatus(event, source, extra = null) {
const conn = this._connQueue.find(({
sourceHost,
sourcePort
}) => source.host === sourceHost && source.port === sourcePort);
switch (event) {
case 'new':
if (this._connQueue.length > MAX_CONNECTIONS) {
this._connQueue.shift();
}
if (!conn) {
this._connQueue.push({
id: (0, _lodash.default)('conn_'),
stage: CONN_STAGE_INIT,
startTime: Date.now(),
sourceHost: source.host,
sourcePort: source.port
});
}
break;
case 'target':
if (conn) {
const target = extra;
conn.stage = CONN_STAGE_TRANSFER;
conn.targetHost = target.host;
conn.targetPort = target.port;
}
break;
case 'close':
if (conn && conn.stage !== CONN_STAGE_ERROR) {
conn.stage = CONN_STAGE_FINISH;
conn.endTime = Date.now();
}
break;
case 'error':
if (conn && conn.stage !== CONN_STAGE_FINISH) {
conn.stage = CONN_STAGE_ERROR;
conn.endTime = Date.now();
conn.message = extra;
}
break;
}
}
}
exports.Hub = Hub;