"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, '..', '..', '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) => {
        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 {
    /**
     * 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;
        }
    }
    /**
     * 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();
    }
    /**
     * 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<string, *>} 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);
        const servicePath = this._mtlsServicePath(options.servicePath, customServicePath, cert && key);
        const opts = Object.assign({}, options, { cert, key, servicePath });
        const serviceAddress = servicePath + ':' + opts.port;
        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) {
        var _a;
        const certRegex = /(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/s;
        const keyRegex = /(?<key>-----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 ((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 = 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 function getByteLength(obj) {
            return message.encode(obj).finish().length;
        };
    }
}
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