"use strict"; /** * Copyright 2020 Google LLC * * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GoogleProtoFilesRoot = exports.GrpcClient = exports.ClientStub = void 0; const grpcProtoLoader = require("@grpc/proto-loader"); const child_process_1 = require("child_process"); const fs = require("fs"); const google_auth_library_1 = require("google-auth-library"); const grpc = require("@grpc/grpc-js"); const os = require("os"); const path_1 = require("path"); const path = require("path"); const protobuf = require("protobufjs"); const objectHash = require("object-hash"); const gax = require("./gax"); const googleProtoFilesDir = path.join(__dirname, '..', '..', 'build', 'protos'); // INCLUDE_DIRS is passed to @grpc/proto-loader const INCLUDE_DIRS = []; INCLUDE_DIRS.push(googleProtoFilesDir); // COMMON_PROTO_FILES logic is here for protobufjs loads (see // GoogleProtoFilesRoot below) const commonProtoFiles = require("./protosList.json"); // use the correct path separator for the OS we are running on const COMMON_PROTO_FILES = commonProtoFiles.map(file => file.replace(/[/\\]/g, path.sep)); /* * Async version of readFile. * * @returns {Promise} Contents of file at path. */ async function readFileAsync(path) { return new Promise((resolve, reject) => { fs.readFile(path, 'utf8', (err, content) => { if (err) return reject(err); else resolve(content); }); }); } /* * Async version of execFile. * * @returns {Promise} stdout from command execution. */ async function execFileAsync(command, args) { return new Promise((resolve, reject) => { (0, child_process_1.execFile)(command, args, (err, stdout) => { if (err) return reject(err); else resolve(stdout); }); }); } class ClientStub extends grpc.Client { } exports.ClientStub = ClientStub; class GrpcClient { /** * Key for proto cache map. We are doing our best to make sure we respect * the options, so if the same proto file is loaded with different set of * options, the cache won't be used. Since some of the options are * Functions (e.g. `enums: String` - see below in `loadProto()`), * they will be omitted from the cache key. If the cache breaks anything * for you, use the `ignoreCache` parameter of `loadProto()` to disable it. */ static protoCacheKey(filename, options) { if (!filename || (Array.isArray(filename) && (filename.length === 0 || !filename[0]))) { return undefined; } return JSON.stringify(filename) + ' ' + JSON.stringify(options); } /** * In rare cases users might need to deallocate all memory consumed by loaded protos. * This method will delete the proto cache content. */ static clearProtoCache() { GrpcClient.protoCache.clear(); } /** * A class which keeps the context of gRPC and auth for the gRPC. * * @param {Object=} options - The optional parameters. It will be directly * passed to google-auth-library library, so parameters like keyFile or * credentials will be valid. * @param {Object=} options.auth - An instance of google-auth-library. * When specified, this auth instance will be used instead of creating * a new one. * @param {Object=} options.grpc - When specified, this will be used * for the 'grpc' module in this context. By default, it will load the grpc * module in the standard way. * @constructor */ constructor(options = {}) { var _a; this.auth = options.auth || new google_auth_library_1.GoogleAuth(options); this.fallback = false; const minimumVersion = 10; const major = Number((_a = process.version.match(/^v(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]); if (Number.isNaN(major) || major < minimumVersion) { const errorMessage = `Node.js v${minimumVersion}.0.0 is a minimum requirement. To learn about legacy version support visit: ` + 'https://github.com/googleapis/google-cloud-node#supported-nodejs-versions'; throw new Error(errorMessage); } if ('grpc' in options) { this.grpc = options.grpc; this.grpcVersion = ''; } else { this.grpc = grpc; this.grpcVersion = require('@grpc/grpc-js/package.json').version; } } /** * Creates a gRPC credentials. It asks the auth data if necessary. * @private * @param {Object} opts - options values for configuring credentials. * @param {Object=} opts.sslCreds - when specified, this is used instead * of default channel credentials. * @return {Promise} The promise which will be resolved to the gRPC credential. */ async _getCredentials(opts) { if (opts.sslCreds) { return opts.sslCreds; } const grpc = this.grpc; const sslCreds = opts.cert && opts.key ? grpc.credentials.createSsl(null, Buffer.from(opts.key), Buffer.from(opts.cert)) : grpc.credentials.createSsl(); const client = await this.auth.getClient(); const credentials = grpc.credentials.combineChannelCredentials(sslCreds, grpc.credentials.createFromGoogleCredential(client)); return credentials; } static defaultOptions() { // This set of @grpc/proto-loader options // 'closely approximates the existing behavior of grpc.load' const includeDirs = INCLUDE_DIRS.slice(); const options = { keepCase: false, longs: String, enums: String, defaults: true, oneofs: true, includeDirs, }; return options; } /** * Loads the gRPC service from the proto file(s) at the given path and with the * given options. Caches the loaded protos so the subsequent loads don't do * any disk reads. * @param filename The path to the proto file(s). * @param options Options for loading the proto file. * @param ignoreCache Defaults to `false`. Set it to `true` if the caching logic * incorrectly decides that the options object is the same, or if you want to * re-read the protos from disk for any other reason. */ loadFromProto(filename, options, ignoreCache = false) { const cacheKey = GrpcClient.protoCacheKey(filename, options); let grpcPackage = cacheKey ? GrpcClient.protoCache.get(cacheKey) : undefined; if (ignoreCache || !grpcPackage) { const packageDef = grpcProtoLoader.loadSync(filename, options); grpcPackage = this.grpc.loadPackageDefinition(packageDef); if (cacheKey) { GrpcClient.protoCache.set(cacheKey, grpcPackage); } } return grpcPackage; } /** * Load gRPC proto service from a filename looking in googleapis common protos * when necessary. Caches the loaded protos so the subsequent loads don't do * any disk reads. * @param {String} protoPath - The directory to search for the protofile. * @param {String|String[]} filename - The filename(s) of the proto(s) to be loaded. * If omitted, protoPath will be treated as a file path to load. * @param ignoreCache Defaults to `false`. Set it to `true` if the caching logic * incorrectly decides that the options object is the same, or if you want to * re-read the protos from disk for any other reason. * @return {Object} The gRPC loaded result (the toplevel namespace * object). */ loadProto(protoPath, filename, ignoreCache = false) { if (!filename) { filename = path.basename(protoPath); protoPath = path.dirname(protoPath); } if (Array.isArray(filename) && filename.length === 0) { return {}; } const options = GrpcClient.defaultOptions(); options.includeDirs.unshift(protoPath); return this.loadFromProto(filename, options, ignoreCache); } static _resolveFile(protoPath, filename) { if (fs.existsSync(path.join(protoPath, filename))) { return path.join(protoPath, filename); } else if (COMMON_PROTO_FILES.indexOf(filename) > -1) { return path.join(googleProtoFilesDir, filename); } throw new Error(filename + ' could not be found in ' + protoPath); } loadProtoJSON(json, ignoreCache = false) { const hash = objectHash(JSON.stringify(json)).toString(); const cached = GrpcClient.protoCache.get(hash); if (cached && !ignoreCache) { return cached; } const options = GrpcClient.defaultOptions(); const packageDefinition = grpcProtoLoader.fromJSON(json, options); const grpcPackage = this.grpc.loadPackageDefinition(packageDefinition); GrpcClient.protoCache.set(hash, grpcPackage); return grpcPackage; } metadataBuilder(headers) { const Metadata = this.grpc.Metadata; const baseMetadata = new Metadata(); for (const key in headers) { const value = headers[key]; if (Array.isArray(value)) { value.forEach(v => baseMetadata.add(key, v)); } else { baseMetadata.set(key, `${value}`); } } return function buildMetadata(abTests, moreHeaders) { // TODO: bring the A/B testing info into the metadata. let copied = false; let metadata = baseMetadata; if (moreHeaders) { for (const key in moreHeaders) { if (key.toLowerCase() !== 'x-goog-api-client') { if (!copied) { copied = true; metadata = metadata.clone(); } const value = moreHeaders[key]; if (Array.isArray(value)) { value.forEach(v => metadata.add(key, v)); } else { metadata.set(key, `${value}`); } } } } return metadata; }; } /** * A wrapper of {@link constructSettings} function under the gRPC context. * * Most of parameters are common among constructSettings, please take a look. * @param {string} serviceName - The fullly-qualified name of the service. * @param {Object} clientConfig - A dictionary of the client config. * @param {Object} configOverrides - A dictionary of overriding configs. * @param {Object} headers - A dictionary of additional HTTP header name to * its value. * @return {Object} A mapping of method names to CallSettings. */ constructSettings(serviceName, clientConfig, configOverrides, headers) { return gax.constructSettings(serviceName, clientConfig, configOverrides, this.grpc.status, { metadataBuilder: this.metadataBuilder(headers) }); } /** * Creates a gRPC stub with current gRPC and auth. * @param {function} CreateStub - The constructor function of the stub. * @param {Object} options - The optional arguments to customize * gRPC connection. This options will be passed to the constructor of * gRPC client too. * @param {string} options.servicePath - The name of the server of the service. * @param {number} options.port - The port of the service. * @param {grpcTypes.ClientCredentials=} options.sslCreds - The credentials to be used * to set up gRPC connection. * @param {string} defaultServicePath - The default service path. * @return {Promise} A promise which resolves to a gRPC stub instance. */ async createStub(CreateStub, options, customServicePath) { // The following options are understood by grpc-gcp and need a special treatment // (should be passed without a `grpc.` prefix) const grpcGcpOptions = [ 'grpc.callInvocationTransformer', 'grpc.channelFactoryOverride', 'grpc.gcpApiConfig', ]; const [cert, key] = await this._detectClientCertificate(options, options.universeDomain); const servicePath = this._mtlsServicePath(options.servicePath, customServicePath, cert && key); const opts = Object.assign({}, options, { cert, key, servicePath }); const serviceAddress = servicePath + ':' + opts.port; if (!options.universeDomain) { options.universeDomain = 'googleapis.com'; } if (options.universeDomain) { const universeFromAuth = await this.auth.getUniverseDomain(); if (universeFromAuth && options.universeDomain !== universeFromAuth) { throw new Error(`The configured universe domain (${options.universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` + "If you haven't configured the universe domain explicitly, googleapis.com is the default."); } } const creds = await this._getCredentials(opts); const grpcOptions = {}; // @grpc/grpc-js limits max receive/send message length starting from v0.8.0 // https://github.com/grpc/grpc-node/releases/tag/%40grpc%2Fgrpc-js%400.8.0 // To keep the existing behavior and avoid libraries breakage, we pass -1 there as suggested. grpcOptions['grpc.max_receive_message_length'] = -1; grpcOptions['grpc.max_send_message_length'] = -1; grpcOptions['grpc.initial_reconnect_backoff_ms'] = 1000; Object.keys(opts).forEach(key => { const value = options[key]; // the older versions had a bug which required users to call an option // grpc.grpc.* to make it actually pass to gRPC as grpc.*, let's handle // this here until the next major release if (key.startsWith('grpc.grpc.')) { key = key.replace(/^grpc\./, ''); } if (key.startsWith('grpc.')) { if (grpcGcpOptions.includes(key)) { key = key.replace(/^grpc\./, ''); } grpcOptions[key] = value; } if (key.startsWith('grpc-node.')) { grpcOptions[key] = value; } }); const stub = new CreateStub(serviceAddress, creds, grpcOptions); return stub; } /** * Detect mTLS client certificate based on logic described in * https://google.aip.dev/auth/4114. * * @param {object} [options] - The configuration object. * @returns {Promise} Resolves array of strings representing cert and key. */ async _detectClientCertificate(opts, universeDomain) { var _a; const certRegex = /(?-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/s; const keyRegex = /(?-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----)/s; // If GOOGLE_API_USE_CLIENT_CERTIFICATE is true...: if (typeof process !== 'undefined' && ((_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.GOOGLE_API_USE_CLIENT_CERTIFICATE) === 'true') { if (universeDomain && universeDomain !== 'googleapis.com') { throw new Error('mTLS is not supported outside of googleapis.com universe domain.'); } if ((opts === null || opts === void 0 ? void 0 : opts.cert) && (opts === null || opts === void 0 ? void 0 : opts.key)) { return [opts.cert, opts.key]; } // If context aware metadata exists, run the cert provider command, // parse the output to extract cert and key, and use this cert/key. const metadataPath = (0, path_1.join)(os.homedir(), '.secureConnect', 'context_aware_metadata.json'); const metadata = JSON.parse(await readFileAsync(metadataPath)); if (!metadata.cert_provider_command) { throw Error('no cert_provider_command found'); } const stdout = await execFileAsync(metadata.cert_provider_command[0], metadata.cert_provider_command.slice(1)); const matchCert = stdout.toString().match(certRegex); const matchKey = stdout.toString().match(keyRegex); if (!((matchCert === null || matchCert === void 0 ? void 0 : matchCert.groups) && (matchKey === null || matchKey === void 0 ? void 0 : matchKey.groups))) { throw Error('unable to parse certificate and key'); } else { return [matchCert.groups.cert, matchKey.groups.key]; } } // If GOOGLE_API_USE_CLIENT_CERTIFICATE is not set or false, // use no cert or key: return [undefined, undefined]; } /** * Return service path, taking into account mTLS logic. * See: https://google.aip.dev/auth/4114 * * @param {string|undefined} servicePath - The path of the service. * @param {string|undefined} customServicePath - Did the user provide a custom service URL. * @param {boolean} hasCertificate - Was a certificate found. * @returns {string} The DNS address for this service. */ _mtlsServicePath(servicePath, customServicePath, hasCertificate) { var _a, _b; // If user provides a custom service path, return the current service // path and do not attempt to add mtls subdomain: if (customServicePath || !servicePath) return servicePath; if (typeof process !== 'undefined' && ((_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.GOOGLE_API_USE_MTLS_ENDPOINT) === 'never') { // It was explicitly asked that mtls endpoint not be used: return servicePath; } else if ((typeof process !== 'undefined' && ((_b = process === null || process === void 0 ? void 0 : process.env) === null || _b === void 0 ? void 0 : _b.GOOGLE_API_USE_MTLS_ENDPOINT) === 'always') || hasCertificate) { // Either auto-detect or explicit setting of endpoint: return servicePath.replace('googleapis.com', 'mtls.googleapis.com'); } return servicePath; } /** * Creates a 'bytelength' function for a given proto message class. * * See {@link BundleDescriptor} about the meaning of the return value. * * @param {function} message - a constructor function that is generated by * protobuf.js. Assumes 'encoder' field in the message. * @return {function(Object):number} - a function to compute the byte length * for an object. */ static createByteLengthFunction(message) { return gax.createByteLengthFunction(message); } } exports.GrpcClient = GrpcClient; GrpcClient.protoCache = new Map(); class GoogleProtoFilesRoot extends protobuf.Root { constructor(...args) { super(...args); } // Causes the loading of an included proto to check if it is a common // proto. If it is a common proto, use the bundled proto. resolvePath(originPath, includePath) { originPath = path.normalize(originPath); includePath = path.normalize(includePath); // Fully qualified paths don't need to be resolved. if (path.isAbsolute(includePath)) { if (!fs.existsSync(includePath)) { throw new Error('The include `' + includePath + '` was not found.'); } return includePath; } if (COMMON_PROTO_FILES.indexOf(includePath) > -1) { return path.join(googleProtoFilesDir, includePath); } return GoogleProtoFilesRoot._findIncludePath(originPath, includePath); } static _findIncludePath(originPath, includePath) { originPath = path.normalize(originPath); includePath = path.normalize(includePath); let current = originPath; let found = fs.existsSync(path.join(current, includePath)); while (!found && current.length > 0) { current = current.substring(0, current.lastIndexOf(path.sep)); found = fs.existsSync(path.join(current, includePath)); } if (!found) { throw new Error('The include `' + includePath + '` was not found.'); } return path.join(current, includePath); } } exports.GoogleProtoFilesRoot = GoogleProtoFilesRoot; //# sourceMappingURL=grpc.js.map