@cliz/inlets
Version:
Cloud Native Tunnel
217 lines (216 loc) • 9.37 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.client = void 0;
const doreamon_1 = require("@zodash/doreamon");
const websocket_1 = require("@zodash/websocket");
const hmac = require("@zodash/hmac");
const nobot_1 = require("@znode/nobot");
const url = require("url");
const semver = require("semver");
const config_1 = require("../../config");
const tunnel_1 = require("./tunnel");
const debug = require('debug')('inlets:client');
function getUpstream(type, upstreamOrPort) {
if (/^\d+$/.test(upstreamOrPort)) {
return `${type}://127.0.0.1:${upstreamOrPort}`;
}
return `${type}://${upstreamOrPort}`;
}
async function client(options) {
const logger = doreamon_1.default.logger.getLogger('client');
const { subDomain, remote = config_1.default.defaultRemote, remoteTCPPort, version, type: tunnelType, port: tunnelPort, authType, clientId, clientSecret, healthcheckInterval, } = options;
if (!/^(tcp|http)$/.test(options === null || options === void 0 ? void 0 : options.type)) {
throw new Error(`type is only allow tcp, http`);
}
if (authType === 'public' && tunnelType !== 'http') {
throw new Error(`public auth only allow http`);
}
let token = options.token;
if (authType === 'public') {
token = 'public';
}
if (!/^(\d+|.+:\d+)$/.test(options === null || options === void 0 ? void 0 : options.upstream)) {
throw new Error(`upstream is only allow port or hostname:port, such as 9000 or 127.0.0.1:9000`);
}
logger.info(`Version: ${version}`);
const authTimeout = createAuthTimeout(options.healthcheckInterval);
const reconnectTimeout = createReconnectTimeout(3000);
const [remoteHost, port] = remote.split(':');
const protocol = port === '443' ? 'wss' : 'ws';
const remoteUrl = `${protocol}://${remote}${config_1.default.wsPath}`;
const upstream = url.parse(getUpstream(options.type, options.upstream));
const socket = new websocket_1.default.Client(remoteUrl, {
autoEcho: true,
maxAutoReconnectTimes: +process.env.MAX_AUTO_RECONNECT_TIMES || 100,
});
function createReporter() {
let _sendReport = doreamon_1.default.func.throttle(nobot_1.sendMessage, 5 * 60 * 1000);
let report = (tag, error) => {
if (!(options === null || options === void 0 ? void 0 : options.reportUrl))
return;
_sendReport(options.reportUrl, {
content: [
['客户端版本', version].join(': '),
['客户端ID', clientId].join(': '),
['客户端端口', options.port].join(': '),
['当前时间', doreamon_1.default.date().format('YYYY-MM-DD HH:mm:ss')].join(': '),
['错误标签', tag].join(': '),
['错误信息', error.message].join(': '),
['错误栈', error.stack].join(': '),
],
});
};
const recreate = (url, interval) => {
if (interval) {
_sendReport = doreamon_1.default.func.throttle(nobot_1.sendMessage, interval);
}
report = (tag, error) => {
_sendReport(url, {
content: [
['客户端版本', version].join(': '),
['客户端ID', clientId].join(': '),
['客户端端口', options.port].join(': '),
['当前时间', doreamon_1.default.date().format('YYYY-MM-DD HH:mm:ss')].join(': '),
['错误标签', tag].join(': '),
['错误信息', error.message].join(': '),
['错误栈', error.stack].join(': '),
],
});
};
};
return {
report,
recreate,
};
}
const reporter = createReporter();
socket.on('error', (error) => {
var _a;
reporter.report('socket error', error);
if (/getaddrinfo ENOTFOUND/i.test(error.message)) {
return logger.error(`local network error, please check if your network is healthy`);
}
if (/ECONNREFUSED/i.test(error.message)) {
return logger.error(`cannot connect the remote: ${remote}`);
}
if (/Unexpected server response/i.test(error.message)) {
return logger.error(`remote server (${remote}) internal error: ${error.message}`);
}
console.error('unknown error:', error);
(_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
});
socket.on('connection', () => {
debug('authenticating ...');
const timestamp = Date.now();
const signedSecret = authType === 'credentials' ? clientSecret : token;
const signature = hmac.hmacSHA512(timestamp.toString(), signedSecret);
const auth = {
version,
type: tunnelType,
port: +upstream.port,
subDomain,
tunnelPort,
timestamp,
authType,
clientId,
signature,
};
socket.emit('authenticate', auth);
});
socket.on('disconnect', () => {
logger.info('Server Disconnected');
reporter.report('socket 掉线', new Error('Server Disconnected'));
reconnectTimeout.start();
});
socket.on('reconnect', () => {
logger.info('Reconnecting ...');
reconnectTimeout.cancel();
});
socket.on('warn', console.log);
socket.on('authenticate', ({ ok, message, version: serverVersion, url, config: _config }) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (!ok) {
return logger.error(message);
}
authTimeout.cancel();
logger.info('[authenticate] connected');
const config = _config;
if ('DEBUG' in process.env) {
logger.info('[authenticate] config:', config);
}
if (config === null || config === void 0 ? void 0 : config.notification) {
const url = (_c = (_b = (_a = config === null || config === void 0 ? void 0 : config.notification) === null || _a === void 0 ? void 0 : _a.alert) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : (_d = config === null || config === void 0 ? void 0 : config.notification) === null || _d === void 0 ? void 0 : _d.url;
let interval = (_g = (_f = (_e = config === null || config === void 0 ? void 0 : config.notification) === null || _e === void 0 ? void 0 : _e.alert) === null || _f === void 0 ? void 0 : _f.interval) !== null && _g !== void 0 ? _g : (_h = config === null || config === void 0 ? void 0 : config.notification) === null || _h === void 0 ? void 0 : _h.interval;
if (!interval || interval < 10 * 1000) {
interval = 5 * 60 * 1000;
}
reporter.recreate(url, interval);
}
checkIsServiceHealthyInBackground(tunnelType, remoteHost, tunnelPort, healthcheckInterval);
const clientVersion = version;
if (!semver.gte(clientVersion, serverVersion)) {
logger.warn(`Warning: Client Version(${clientVersion}) is not equal with Server(${serverVersion}), which may cause tunnel broken.`);
}
if (tunnelType === 'http') {
logger.info(`Forwarding: ${url} -> ${upstream.protocol}//${upstream.host}`);
return;
}
if (tunnelType === 'tcp') {
return;
}
throw new Error(`unknown network type: ${tunnelType}`);
});
socket.on('request', (data) => {
tunnel_1.default.http(+upstream.port, upstream.hostname, socket, data);
});
socket.on('tcp:ready', ({ host, port }) => {
logger.info(`Forwarding: tcp://${host}:${port} -> ${upstream.host}`);
});
socket.on('tcp:connect', ({ id, requestId, ip }) => {
logger.info(`[tunnel:tcp][user] connected (request id: ${requestId}, ip: ${ip})`);
tunnel_1.default.tcp({
authType,
token,
clientId,
clientSecret,
id,
requestId,
localHost: upstream.hostname,
localPort: +upstream.port,
remoteHost: remoteHost,
remotePort: remoteTCPPort,
});
});
}
exports.client = client;
function createAuthTimeout(timeout) {
const it = setTimeout(() => {
doreamon_1.default.logger.error(`authencate timeout, exit`);
process.exit(1);
}, timeout);
return {
cancel: () => {
clearTimeout(it);
},
};
}
function createReconnectTimeout(timeout) {
let it = null;
return {
start: () => {
doreamon_1.default.logger.info(`Exit process if no reconnect after ${timeout}ms`);
it = setTimeout(() => {
doreamon_1.default.logger.error(`reconnect timeout, exit`);
process.exit(1);
}, timeout);
},
cancel: () => {
if (it) {
clearTimeout(it);
it = null;
}
},
};
}
async function checkIsServiceHealthyInBackground(type, host, port, interval = 30 * 1000) {
}