UNPKG

wechaty-puppet-service

Version:
359 lines 16.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GrpcManager = void 0; /** * Wechaty Open Source Software - https://github.com/wechaty * * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and * Wechaty Contributors <https://github.com/wechaty>. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const util_1 = __importDefault(require("util")); const events_1 = __importDefault(require("events")); const crypto_1 = __importDefault(require("crypto")); const wechaty_puppet_1 = require("wechaty-puppet"); const wechaty_grpc_1 = require("wechaty-grpc"); const gerror_1 = require("gerror"); const wechaty_token_1 = require("wechaty-token"); const config_js_1 = require("../config.js"); const mod_js_1 = require("../auth/mod.js"); const grpc_js_js_1 = require("../auth/grpc-js.js"); const ca_js_1 = require("../auth/ca.js"); /** * Huan(202108): register `wechaty` schema for gRPC service discovery * so that we can use `wechaty:///SNI_UUIDv4` for gRPC address * * See: https://github.com/wechaty/wechaty-puppet-service/issues/155 */ wechaty_token_1.WechatyResolver.setup(); class GrpcManager extends events_1.default { options; _client; get client() { if (!this._client) { throw new Error('NO CLIENT'); } return this._client; } eventStream; /** * gRPC settings */ caCert; disableTls; endpoint; serverName; token; constructor(options) { super(); this.options = options; wechaty_puppet_1.log.verbose('GrpcManager', 'constructor(%s)', JSON.stringify(options)); this.caCert = Buffer.from(config_js_1.envVars.WECHATY_PUPPET_SERVICE_TLS_CA_CERT(this.options.tls?.caCert) || ca_js_1.TLS_CA_CERT); wechaty_puppet_1.log.verbose('GrpcManager', 'constructor() tlsRootCert(hash): "%s"', crypto_1.default.createHash('sha256') .update(this.caCert) .digest('hex')); /** * Token will be used in the gRPC resolver (in endpoint) */ this.token = new wechaty_token_1.WechatyToken(config_js_1.envVars.WECHATY_PUPPET_SERVICE_TOKEN(this.options.token)); wechaty_puppet_1.log.verbose('GrpcManager', 'constructor() token: "%s"', this.token); this.endpoint = config_js_1.envVars.WECHATY_PUPPET_SERVICE_ENDPOINT(this.options.endpoint) /** * Wechaty Token Discovery-able URL * See: wechaty-token / https://github.com/wechaty/wechaty-puppet-service/issues/155 */ || [ 'wechaty://', config_js_1.envVars.WECHATY_PUPPET_SERVICE_AUTHORITY(this.options.authority), '/', this.token, ].join(''); wechaty_puppet_1.log.verbose('GrpcManager', 'constructor() endpoint: "%s"', this.endpoint); /** * Disable TLS */ this.disableTls = config_js_1.envVars.WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT(this.options.tls?.disable); wechaty_puppet_1.log.verbose('GrpcManager', 'constructor() disableTls: "%s"', this.disableTls); /** * for Node.js TLS SNI * https://en.wikipedia.org/wiki/Server_Name_Indication */ const serverNameIndication = config_js_1.envVars.WECHATY_PUPPET_SERVICE_TLS_SERVER_NAME(this.options.tls?.serverName) /** * Huan(202108): we use SNI from token * if there is * neither override from environment variable: `WECHATY_PUPPET_SERVICE_TLS_SERVER_NAME` * nor set from: `options.tls.serverName` */ || this.token.sni; if (!serverNameIndication) { throw new Error([ 'Wechaty Puppet Service requires a SNI as prefix of the token from version 0.30 and later.', `You can add the "${ca_js_1.TLS_INSECURE_SERVER_CERT_COMMON_NAME}_" prefix to your token`, `like: "${ca_js_1.TLS_INSECURE_SERVER_CERT_COMMON_NAME}_${this.token}"`, 'and try again.', ].join('\n')); } this.serverName = serverNameIndication; wechaty_puppet_1.log.verbose('GrpcManager', 'constructor() serverName(SNI): "%s"', this.serverName); } async start() { wechaty_puppet_1.log.verbose('GrpcManager', 'start()'); /** * 1. Init grpc client */ wechaty_puppet_1.log.verbose('GrpcManager', 'start() initializing client ...'); await this.initClient(); wechaty_puppet_1.log.verbose('GrpcManager', 'start() initializing client ... done'); /** * 2. Connect to stream */ wechaty_puppet_1.log.verbose('GrpcManager', 'start() starting stream ...'); await this.startStream(); wechaty_puppet_1.log.verbose('GrpcManager', 'start() starting stream ... done'); /** * 3. Start the puppet */ wechaty_puppet_1.log.verbose('GrpcManager', 'start() calling grpc server: start() ...'); await util_1.default.promisify(this.client.start .bind(this.client))(new wechaty_grpc_1.puppet.StartRequest()); wechaty_puppet_1.log.verbose('GrpcManager', 'start() calling grpc server: start() ... done'); wechaty_puppet_1.log.verbose('GrpcManager', 'start() ... done'); } async stop() { wechaty_puppet_1.log.verbose('GrpcManager', 'stop()'); /** * 1. Disconnect from stream */ wechaty_puppet_1.log.verbose('GrpcManager', 'stop() stop stream ...'); this.stopStream(); wechaty_puppet_1.log.verbose('GrpcManager', 'stop() stop stream ... done'); /** * 2. Stop the puppet */ try { wechaty_puppet_1.log.verbose('GrpcManager', 'stop() stop client ...'); await util_1.default.promisify(this.client.stop .bind(this.client))(new wechaty_grpc_1.puppet.StopRequest()); wechaty_puppet_1.log.verbose('GrpcManager', 'stop() stop client ... done'); } catch (e) { this.emit('error', e); } /** * 3. Destroy grpc client */ try { wechaty_puppet_1.log.verbose('GrpcManager', 'stop() destroy client ...'); this.destroyClient(); wechaty_puppet_1.log.verbose('GrpcManager', 'stop() destroy client ... done'); } catch (e) { this.emit('error', e); } wechaty_puppet_1.log.verbose('GrpcManager', 'stop() ... done'); } async initClient() { wechaty_puppet_1.log.verbose('GrpcManager', 'initClient()'); /** * Huan(202108): for maximum compatible with the non-tls community servers/clients, * we introduced the WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_{SERVER,CLIENT} environment variables. * if it has been set, then we will run under HTTP instead of HTTPS */ let credential; if (this.disableTls) { wechaty_puppet_1.log.warn('GrpcManager', 'initClient() TLS: disabled (INSECURE)'); credential = wechaty_grpc_1.grpc.credentials.createInsecure(); } else { wechaty_puppet_1.log.verbose('GrpcManager', 'initClient() TLS: enabled'); const callCred = (0, mod_js_1.callCredToken)(this.token.token); const channelCred = wechaty_grpc_1.grpc.credentials.createSsl(this.caCert); const combCreds = wechaty_grpc_1.grpc.credentials.combineChannelCredentials(channelCred, callCred); credential = combCreds; } const clientOptions = { ...config_js_1.GRPC_OPTIONS, 'grpc.ssl_target_name_override': this.serverName, }; { // Deprecated: this block will be removed after Dec 21, 2022. /** * Huan(202108): `grpc.default_authority` is a workaround * for compatiblity with the non-tls community servers/clients. * * See: https://github.com/wechaty/wechaty-puppet-service/pull/78 */ const grpcDefaultAuthority = this.token.token; clientOptions['grpc.default_authority'] = grpcDefaultAuthority; } if (this._client) { wechaty_puppet_1.log.warn('GrpcManager', 'initClient() this.#client exists? Old client has been dropped.'); this._client = undefined; } this._client = new wechaty_grpc_1.puppet.PuppetClient(this.endpoint, credential, clientOptions); wechaty_puppet_1.log.verbose('GrpcManager', 'initClient() ... done'); } destroyClient() { wechaty_puppet_1.log.verbose('GrpcManager', 'destroyClient()'); if (!this._client) { wechaty_puppet_1.log.warn('GrpcManager', 'destroyClient() this.#client not exist'); return; } const client = this._client; /** * Huan(202108): we should set `this.client` to `undefined` at the current event loop * to prevent the future usage of the old client. */ this._client = undefined; try { client.close(); } catch (e) { wechaty_puppet_1.log.error('GrpcManager', 'destroyClient() client.close() rejection: %s\n%s', e && e.message, e.stack); } } async startStream() { wechaty_puppet_1.log.verbose('GrpcManager', 'startStream()'); if (this.eventStream) { wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() this.eventStream exists, dropped.'); this.eventStream = undefined; } wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() grpc -> event() ...'); const eventStream = this.client.event(new wechaty_grpc_1.puppet.EventRequest()); wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() grpc -> event() ... done'); /** * Store the event data from the stream when we test connection, * and re-emit the event data when we have finished testing the connection */ let peekedData; /** * Huan(202108): future must be placed before other listenser registration * because the on('data') will start drain the stream */ const future = new Promise((resolve, reject) => eventStream /** * Huan(202108): we need a `heartbeat` event to confirm the connection is alive * for our wechaty-puppet-service server code, when the gRPC event stream is opened, * it will emit a `heartbeat` event as early as possible. * * However, the `heartbeat` event is not guaranteed to be emitted, * if the puppet service provider is coming from the community, like: * - paimon * * So we also need a timeout for compatible with those providers * in case of they are not following this special protocol. */ .once('data', (resp) => { peekedData = resp; resolve(); }) /** * Any of the following events will be emitted means that there's a problem. */ .once('cancel', reject) .once('end', reject) .once('error', reject) /** * The `status` event is import when we connect a gRPC stream. * * Huan(202108): according to the unit tests (tests/grpc-client.spec.ts) * 1. If client TLS is not ok (client no-tls but server TLS is required) * then status will be: * { code: 14, details: 'Connection dropped' } * 2. If client TLS is ok but the client token is invalid, * then status will be: * { code: 16, details: 'Invalid Wechaty TOKEN "0.XXX"' } */ .once('status', status => { // console.info('once(status)', status) status.code === grpc_js_js_1.GrpcStatus.OK ? resolve() : reject(new Error('once(status)')); })); /** * Huan(202108): the `heartbeat` event is not guaranteed to be emitted * if a puppet service provider is coming from the community, it might not follow the protocol specification. * So we need a timeout for compatible with those providers */ wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ...'); try { await (0, gerror_1.timeoutPromise)(future, 5 * 1000); // 5 seconds } catch (_) { wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... timeout'); } wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... data peeked'); /** * Bridge the events * Huan(202108): adding the below event listeners * must be after the `await future` above, * so that if there's any `error` event, * it will be triggered already. */ wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() initializing event stream ...'); eventStream .on('cancel', (...args) => this.emit('cancel', ...args)) .on('data', (...args) => this.emit('data', ...args)) .on('end', (...args) => this.emit('end', ...args)) .on('error', (...args) => this.emit('error', ...args)) .on('metadata', (...args) => this.emit('metadata', ...args)) .on('status', (...args) => this.emit('status', ...args)); this.eventStream = eventStream; wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() initializing event stream ... done'); /** * Re-emit the peeked data if there's any */ if (peekedData) { wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() sending back the peeked data ...'); this.emit('data', peekedData); peekedData = undefined; wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() sending back the peeked data ... done'); } wechaty_puppet_1.log.verbose('GrpcManager', 'startStream() ... done'); } stopStream() { wechaty_puppet_1.log.verbose('GrpcManager', 'stopStream()'); if (!this.eventStream) { wechaty_puppet_1.log.verbose('GrpcManager', 'stopStream() no eventStream when stop, skip destroy.'); return; } /** * Huan(202108): we should set `this.eventStream` to `undefined` at the current event loop * to prevent the future usage of the old eventStream. */ const eventStream = this.eventStream; this.eventStream = undefined; /** * Huan(202003): * destroy() will be enough to terminate a stream call. * cancel() is not needed. */ // this.eventStream.cancel() try { wechaty_puppet_1.log.verbose('GrpcManager', 'stopStream() destroying event stream ...'); eventStream.destroy(); wechaty_puppet_1.log.verbose('GrpcManager', 'stopStream() destroying event stream ... done'); } catch (e) { this.emit('error', e); } } } exports.GrpcManager = GrpcManager; //# sourceMappingURL=grpc-manager.js.map