wechaty-puppet-service
Version:
Puppet Service for Wechaty
359 lines • 16.3 kB
JavaScript
"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