grpc.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. "use strict";
  2. /**
  3. * Copyright 2020 Google LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. Object.defineProperty(exports, "__esModule", { value: true });
  18. exports.GoogleProtoFilesRoot = exports.GrpcClient = exports.ClientStub = void 0;
  19. const grpcProtoLoader = require("@grpc/proto-loader");
  20. const child_process_1 = require("child_process");
  21. const fs = require("fs");
  22. const google_auth_library_1 = require("google-auth-library");
  23. const grpc = require("@grpc/grpc-js");
  24. const os = require("os");
  25. const path_1 = require("path");
  26. const path = require("path");
  27. const protobuf = require("protobufjs");
  28. const objectHash = require("object-hash");
  29. const gax = require("./gax");
  30. const googleProtoFilesDir = path.join(__dirname, '..', '..', 'build', 'protos');
  31. // INCLUDE_DIRS is passed to @grpc/proto-loader
  32. const INCLUDE_DIRS = [];
  33. INCLUDE_DIRS.push(googleProtoFilesDir);
  34. // COMMON_PROTO_FILES logic is here for protobufjs loads (see
  35. // GoogleProtoFilesRoot below)
  36. const commonProtoFiles = require("./protosList.json");
  37. // use the correct path separator for the OS we are running on
  38. const COMMON_PROTO_FILES = commonProtoFiles.map(file => file.replace(/[/\\]/g, path.sep));
  39. /*
  40. * Async version of readFile.
  41. *
  42. * @returns {Promise} Contents of file at path.
  43. */
  44. async function readFileAsync(path) {
  45. return new Promise((resolve, reject) => {
  46. fs.readFile(path, 'utf8', (err, content) => {
  47. if (err)
  48. return reject(err);
  49. else
  50. resolve(content);
  51. });
  52. });
  53. }
  54. /*
  55. * Async version of execFile.
  56. *
  57. * @returns {Promise} stdout from command execution.
  58. */
  59. async function execFileAsync(command, args) {
  60. return new Promise((resolve, reject) => {
  61. (0, child_process_1.execFile)(command, args, (err, stdout) => {
  62. if (err)
  63. return reject(err);
  64. else
  65. resolve(stdout);
  66. });
  67. });
  68. }
  69. class ClientStub extends grpc.Client {
  70. }
  71. exports.ClientStub = ClientStub;
  72. class GrpcClient {
  73. /**
  74. * Key for proto cache map. We are doing our best to make sure we respect
  75. * the options, so if the same proto file is loaded with different set of
  76. * options, the cache won't be used. Since some of the options are
  77. * Functions (e.g. `enums: String` - see below in `loadProto()`),
  78. * they will be omitted from the cache key. If the cache breaks anything
  79. * for you, use the `ignoreCache` parameter of `loadProto()` to disable it.
  80. */
  81. static protoCacheKey(filename, options) {
  82. if (!filename ||
  83. (Array.isArray(filename) && (filename.length === 0 || !filename[0]))) {
  84. return undefined;
  85. }
  86. return JSON.stringify(filename) + ' ' + JSON.stringify(options);
  87. }
  88. /**
  89. * In rare cases users might need to deallocate all memory consumed by loaded protos.
  90. * This method will delete the proto cache content.
  91. */
  92. static clearProtoCache() {
  93. GrpcClient.protoCache.clear();
  94. }
  95. /**
  96. * A class which keeps the context of gRPC and auth for the gRPC.
  97. *
  98. * @param {Object=} options - The optional parameters. It will be directly
  99. * passed to google-auth-library library, so parameters like keyFile or
  100. * credentials will be valid.
  101. * @param {Object=} options.auth - An instance of google-auth-library.
  102. * When specified, this auth instance will be used instead of creating
  103. * a new one.
  104. * @param {Object=} options.grpc - When specified, this will be used
  105. * for the 'grpc' module in this context. By default, it will load the grpc
  106. * module in the standard way.
  107. * @constructor
  108. */
  109. constructor(options = {}) {
  110. var _a;
  111. this.auth = options.auth || new google_auth_library_1.GoogleAuth(options);
  112. this.fallback = false;
  113. const minimumVersion = 10;
  114. const major = Number((_a = process.version.match(/^v(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]);
  115. if (Number.isNaN(major) || major < minimumVersion) {
  116. const errorMessage = `Node.js v${minimumVersion}.0.0 is a minimum requirement. To learn about legacy version support visit: ` +
  117. 'https://github.com/googleapis/google-cloud-node#supported-nodejs-versions';
  118. throw new Error(errorMessage);
  119. }
  120. if ('grpc' in options) {
  121. this.grpc = options.grpc;
  122. this.grpcVersion = '';
  123. }
  124. else {
  125. this.grpc = grpc;
  126. this.grpcVersion = require('@grpc/grpc-js/package.json').version;
  127. }
  128. }
  129. /**
  130. * Creates a gRPC credentials. It asks the auth data if necessary.
  131. * @private
  132. * @param {Object} opts - options values for configuring credentials.
  133. * @param {Object=} opts.sslCreds - when specified, this is used instead
  134. * of default channel credentials.
  135. * @return {Promise} The promise which will be resolved to the gRPC credential.
  136. */
  137. async _getCredentials(opts) {
  138. if (opts.sslCreds) {
  139. return opts.sslCreds;
  140. }
  141. const grpc = this.grpc;
  142. const sslCreds = opts.cert && opts.key
  143. ? grpc.credentials.createSsl(null, Buffer.from(opts.key), Buffer.from(opts.cert))
  144. : grpc.credentials.createSsl();
  145. const client = await this.auth.getClient();
  146. const credentials = grpc.credentials.combineChannelCredentials(sslCreds, grpc.credentials.createFromGoogleCredential(client));
  147. return credentials;
  148. }
  149. static defaultOptions() {
  150. // This set of @grpc/proto-loader options
  151. // 'closely approximates the existing behavior of grpc.load'
  152. const includeDirs = INCLUDE_DIRS.slice();
  153. const options = {
  154. keepCase: false,
  155. longs: String,
  156. enums: String,
  157. defaults: true,
  158. oneofs: true,
  159. includeDirs,
  160. };
  161. return options;
  162. }
  163. /**
  164. * Loads the gRPC service from the proto file(s) at the given path and with the
  165. * given options. Caches the loaded protos so the subsequent loads don't do
  166. * any disk reads.
  167. * @param filename The path to the proto file(s).
  168. * @param options Options for loading the proto file.
  169. * @param ignoreCache Defaults to `false`. Set it to `true` if the caching logic
  170. * incorrectly decides that the options object is the same, or if you want to
  171. * re-read the protos from disk for any other reason.
  172. */
  173. loadFromProto(filename, options, ignoreCache = false) {
  174. const cacheKey = GrpcClient.protoCacheKey(filename, options);
  175. let grpcPackage = cacheKey
  176. ? GrpcClient.protoCache.get(cacheKey)
  177. : undefined;
  178. if (ignoreCache || !grpcPackage) {
  179. const packageDef = grpcProtoLoader.loadSync(filename, options);
  180. grpcPackage = this.grpc.loadPackageDefinition(packageDef);
  181. if (cacheKey) {
  182. GrpcClient.protoCache.set(cacheKey, grpcPackage);
  183. }
  184. }
  185. return grpcPackage;
  186. }
  187. /**
  188. * Load gRPC proto service from a filename looking in googleapis common protos
  189. * when necessary. Caches the loaded protos so the subsequent loads don't do
  190. * any disk reads.
  191. * @param {String} protoPath - The directory to search for the protofile.
  192. * @param {String|String[]} filename - The filename(s) of the proto(s) to be loaded.
  193. * If omitted, protoPath will be treated as a file path to load.
  194. * @param ignoreCache Defaults to `false`. Set it to `true` if the caching logic
  195. * incorrectly decides that the options object is the same, or if you want to
  196. * re-read the protos from disk for any other reason.
  197. * @return {Object<string, *>} The gRPC loaded result (the toplevel namespace
  198. * object).
  199. */
  200. loadProto(protoPath, filename, ignoreCache = false) {
  201. if (!filename) {
  202. filename = path.basename(protoPath);
  203. protoPath = path.dirname(protoPath);
  204. }
  205. if (Array.isArray(filename) && filename.length === 0) {
  206. return {};
  207. }
  208. const options = GrpcClient.defaultOptions();
  209. options.includeDirs.unshift(protoPath);
  210. return this.loadFromProto(filename, options, ignoreCache);
  211. }
  212. static _resolveFile(protoPath, filename) {
  213. if (fs.existsSync(path.join(protoPath, filename))) {
  214. return path.join(protoPath, filename);
  215. }
  216. else if (COMMON_PROTO_FILES.indexOf(filename) > -1) {
  217. return path.join(googleProtoFilesDir, filename);
  218. }
  219. throw new Error(filename + ' could not be found in ' + protoPath);
  220. }
  221. loadProtoJSON(json, ignoreCache = false) {
  222. const hash = objectHash(JSON.stringify(json)).toString();
  223. const cached = GrpcClient.protoCache.get(hash);
  224. if (cached && !ignoreCache) {
  225. return cached;
  226. }
  227. const options = GrpcClient.defaultOptions();
  228. const packageDefinition = grpcProtoLoader.fromJSON(json, options);
  229. const grpcPackage = this.grpc.loadPackageDefinition(packageDefinition);
  230. GrpcClient.protoCache.set(hash, grpcPackage);
  231. return grpcPackage;
  232. }
  233. metadataBuilder(headers) {
  234. const Metadata = this.grpc.Metadata;
  235. const baseMetadata = new Metadata();
  236. for (const key in headers) {
  237. const value = headers[key];
  238. if (Array.isArray(value)) {
  239. value.forEach(v => baseMetadata.add(key, v));
  240. }
  241. else {
  242. baseMetadata.set(key, `${value}`);
  243. }
  244. }
  245. return function buildMetadata(abTests, moreHeaders) {
  246. // TODO: bring the A/B testing info into the metadata.
  247. let copied = false;
  248. let metadata = baseMetadata;
  249. if (moreHeaders) {
  250. for (const key in moreHeaders) {
  251. if (key.toLowerCase() !== 'x-goog-api-client') {
  252. if (!copied) {
  253. copied = true;
  254. metadata = metadata.clone();
  255. }
  256. const value = moreHeaders[key];
  257. if (Array.isArray(value)) {
  258. value.forEach(v => metadata.add(key, v));
  259. }
  260. else {
  261. metadata.set(key, `${value}`);
  262. }
  263. }
  264. }
  265. }
  266. return metadata;
  267. };
  268. }
  269. /**
  270. * A wrapper of {@link constructSettings} function under the gRPC context.
  271. *
  272. * Most of parameters are common among constructSettings, please take a look.
  273. * @param {string} serviceName - The fullly-qualified name of the service.
  274. * @param {Object} clientConfig - A dictionary of the client config.
  275. * @param {Object} configOverrides - A dictionary of overriding configs.
  276. * @param {Object} headers - A dictionary of additional HTTP header name to
  277. * its value.
  278. * @return {Object} A mapping of method names to CallSettings.
  279. */
  280. constructSettings(serviceName, clientConfig, configOverrides, headers) {
  281. return gax.constructSettings(serviceName, clientConfig, configOverrides, this.grpc.status, { metadataBuilder: this.metadataBuilder(headers) });
  282. }
  283. /**
  284. * Creates a gRPC stub with current gRPC and auth.
  285. * @param {function} CreateStub - The constructor function of the stub.
  286. * @param {Object} options - The optional arguments to customize
  287. * gRPC connection. This options will be passed to the constructor of
  288. * gRPC client too.
  289. * @param {string} options.servicePath - The name of the server of the service.
  290. * @param {number} options.port - The port of the service.
  291. * @param {grpcTypes.ClientCredentials=} options.sslCreds - The credentials to be used
  292. * to set up gRPC connection.
  293. * @param {string} defaultServicePath - The default service path.
  294. * @return {Promise} A promise which resolves to a gRPC stub instance.
  295. */
  296. async createStub(CreateStub, options, customServicePath) {
  297. // The following options are understood by grpc-gcp and need a special treatment
  298. // (should be passed without a `grpc.` prefix)
  299. const grpcGcpOptions = [
  300. 'grpc.callInvocationTransformer',
  301. 'grpc.channelFactoryOverride',
  302. 'grpc.gcpApiConfig',
  303. ];
  304. const [cert, key] = await this._detectClientCertificate(options, options.universeDomain);
  305. const servicePath = this._mtlsServicePath(options.servicePath, customServicePath, cert && key);
  306. const opts = Object.assign({}, options, { cert, key, servicePath });
  307. const serviceAddress = servicePath + ':' + opts.port;
  308. if (!options.universeDomain) {
  309. options.universeDomain = 'googleapis.com';
  310. }
  311. if (options.universeDomain) {
  312. const universeFromAuth = await this.auth.getUniverseDomain();
  313. if (universeFromAuth && options.universeDomain !== universeFromAuth) {
  314. throw new Error(`The configured universe domain (${options.universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` +
  315. "If you haven't configured the universe domain explicitly, googleapis.com is the default.");
  316. }
  317. }
  318. const creds = await this._getCredentials(opts);
  319. const grpcOptions = {};
  320. // @grpc/grpc-js limits max receive/send message length starting from v0.8.0
  321. // https://github.com/grpc/grpc-node/releases/tag/%40grpc%2Fgrpc-js%400.8.0
  322. // To keep the existing behavior and avoid libraries breakage, we pass -1 there as suggested.
  323. grpcOptions['grpc.max_receive_message_length'] = -1;
  324. grpcOptions['grpc.max_send_message_length'] = -1;
  325. grpcOptions['grpc.initial_reconnect_backoff_ms'] = 1000;
  326. Object.keys(opts).forEach(key => {
  327. const value = options[key];
  328. // the older versions had a bug which required users to call an option
  329. // grpc.grpc.* to make it actually pass to gRPC as grpc.*, let's handle
  330. // this here until the next major release
  331. if (key.startsWith('grpc.grpc.')) {
  332. key = key.replace(/^grpc\./, '');
  333. }
  334. if (key.startsWith('grpc.')) {
  335. if (grpcGcpOptions.includes(key)) {
  336. key = key.replace(/^grpc\./, '');
  337. }
  338. grpcOptions[key] = value;
  339. }
  340. if (key.startsWith('grpc-node.')) {
  341. grpcOptions[key] = value;
  342. }
  343. });
  344. const stub = new CreateStub(serviceAddress, creds, grpcOptions);
  345. return stub;
  346. }
  347. /**
  348. * Detect mTLS client certificate based on logic described in
  349. * https://google.aip.dev/auth/4114.
  350. *
  351. * @param {object} [options] - The configuration object.
  352. * @returns {Promise} Resolves array of strings representing cert and key.
  353. */
  354. async _detectClientCertificate(opts, universeDomain) {
  355. var _a;
  356. const certRegex = /(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/s;
  357. const keyRegex = /(?<key>-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----)/s;
  358. // If GOOGLE_API_USE_CLIENT_CERTIFICATE is true...:
  359. if (typeof process !== 'undefined' &&
  360. ((_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.GOOGLE_API_USE_CLIENT_CERTIFICATE) === 'true') {
  361. if (universeDomain && universeDomain !== 'googleapis.com') {
  362. throw new Error('mTLS is not supported outside of googleapis.com universe domain.');
  363. }
  364. if ((opts === null || opts === void 0 ? void 0 : opts.cert) && (opts === null || opts === void 0 ? void 0 : opts.key)) {
  365. return [opts.cert, opts.key];
  366. }
  367. // If context aware metadata exists, run the cert provider command,
  368. // parse the output to extract cert and key, and use this cert/key.
  369. const metadataPath = (0, path_1.join)(os.homedir(), '.secureConnect', 'context_aware_metadata.json');
  370. const metadata = JSON.parse(await readFileAsync(metadataPath));
  371. if (!metadata.cert_provider_command) {
  372. throw Error('no cert_provider_command found');
  373. }
  374. const stdout = await execFileAsync(metadata.cert_provider_command[0], metadata.cert_provider_command.slice(1));
  375. const matchCert = stdout.toString().match(certRegex);
  376. const matchKey = stdout.toString().match(keyRegex);
  377. if (!((matchCert === null || matchCert === void 0 ? void 0 : matchCert.groups) && (matchKey === null || matchKey === void 0 ? void 0 : matchKey.groups))) {
  378. throw Error('unable to parse certificate and key');
  379. }
  380. else {
  381. return [matchCert.groups.cert, matchKey.groups.key];
  382. }
  383. }
  384. // If GOOGLE_API_USE_CLIENT_CERTIFICATE is not set or false,
  385. // use no cert or key:
  386. return [undefined, undefined];
  387. }
  388. /**
  389. * Return service path, taking into account mTLS logic.
  390. * See: https://google.aip.dev/auth/4114
  391. *
  392. * @param {string|undefined} servicePath - The path of the service.
  393. * @param {string|undefined} customServicePath - Did the user provide a custom service URL.
  394. * @param {boolean} hasCertificate - Was a certificate found.
  395. * @returns {string} The DNS address for this service.
  396. */
  397. _mtlsServicePath(servicePath, customServicePath, hasCertificate) {
  398. var _a, _b;
  399. // If user provides a custom service path, return the current service
  400. // path and do not attempt to add mtls subdomain:
  401. if (customServicePath || !servicePath)
  402. return servicePath;
  403. if (typeof process !== 'undefined' &&
  404. ((_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.GOOGLE_API_USE_MTLS_ENDPOINT) === 'never') {
  405. // It was explicitly asked that mtls endpoint not be used:
  406. return servicePath;
  407. }
  408. else if ((typeof process !== 'undefined' &&
  409. ((_b = process === null || process === void 0 ? void 0 : process.env) === null || _b === void 0 ? void 0 : _b.GOOGLE_API_USE_MTLS_ENDPOINT) === 'always') ||
  410. hasCertificate) {
  411. // Either auto-detect or explicit setting of endpoint:
  412. return servicePath.replace('googleapis.com', 'mtls.googleapis.com');
  413. }
  414. return servicePath;
  415. }
  416. /**
  417. * Creates a 'bytelength' function for a given proto message class.
  418. *
  419. * See {@link BundleDescriptor} about the meaning of the return value.
  420. *
  421. * @param {function} message - a constructor function that is generated by
  422. * protobuf.js. Assumes 'encoder' field in the message.
  423. * @return {function(Object):number} - a function to compute the byte length
  424. * for an object.
  425. */
  426. static createByteLengthFunction(message) {
  427. return gax.createByteLengthFunction(message);
  428. }
  429. }
  430. exports.GrpcClient = GrpcClient;
  431. GrpcClient.protoCache = new Map();
  432. class GoogleProtoFilesRoot extends protobuf.Root {
  433. constructor(...args) {
  434. super(...args);
  435. }
  436. // Causes the loading of an included proto to check if it is a common
  437. // proto. If it is a common proto, use the bundled proto.
  438. resolvePath(originPath, includePath) {
  439. originPath = path.normalize(originPath);
  440. includePath = path.normalize(includePath);
  441. // Fully qualified paths don't need to be resolved.
  442. if (path.isAbsolute(includePath)) {
  443. if (!fs.existsSync(includePath)) {
  444. throw new Error('The include `' + includePath + '` was not found.');
  445. }
  446. return includePath;
  447. }
  448. if (COMMON_PROTO_FILES.indexOf(includePath) > -1) {
  449. return path.join(googleProtoFilesDir, includePath);
  450. }
  451. return GoogleProtoFilesRoot._findIncludePath(originPath, includePath);
  452. }
  453. static _findIncludePath(originPath, includePath) {
  454. originPath = path.normalize(originPath);
  455. includePath = path.normalize(includePath);
  456. let current = originPath;
  457. let found = fs.existsSync(path.join(current, includePath));
  458. while (!found && current.length > 0) {
  459. current = current.substring(0, current.lastIndexOf(path.sep));
  460. found = fs.existsSync(path.join(current, includePath));
  461. }
  462. if (!found) {
  463. throw new Error('The include `' + includePath + '` was not found.');
  464. }
  465. return path.join(current, includePath);
  466. }
  467. }
  468. exports.GoogleProtoFilesRoot = GoogleProtoFilesRoot;
  469. //# sourceMappingURL=grpc.js.map