UNPKG

@cliz/inlets

Version:
217 lines (216 loc) 9.37 kB
"use strict"; 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) { }