"use strict";
// Copyright 2022 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.PluggableAuthHandler = void 0;
const pluggable_auth_client_1 = require("./pluggable-auth-client");
const executable_response_1 = require("./executable-response");
const childProcess = require("child_process");
const fs = require("fs");
/**
 * A handler used to retrieve 3rd party token responses from user defined
 * executables and cached file output for the PluggableAuthClient class.
 */
class PluggableAuthHandler {
    /**
     * Instantiates a PluggableAuthHandler instance using the provided
     * PluggableAuthHandlerOptions object.
     */
    constructor(options) {
        if (!options.command) {
            throw new Error('No command provided.');
        }
        this.commandComponents = PluggableAuthHandler.parseCommand(options.command);
        this.timeoutMillis = options.timeoutMillis;
        if (!this.timeoutMillis) {
            throw new Error('No timeoutMillis provided.');
        }
        this.outputFile = options.outputFile;
    }
    /**
     * Calls user provided executable to get a 3rd party subject token and
     * returns the response.
     * @param envMap a Map of additional Environment Variables required for
     *   the executable.
     * @return A promise that resolves with the executable response.
     */
    retrieveResponseFromExecutable(envMap) {
        return new Promise((resolve, reject) => {
            // Spawn process to run executable using added environment variables.
            const child = childProcess.spawn(this.commandComponents[0], this.commandComponents.slice(1), {
                env: { ...process.env, ...Object.fromEntries(envMap) },
            });
            let output = '';
            // Append stdout to output as executable runs.
            child.stdout.on('data', (data) => {
                output += data;
            });
            // Append stderr as executable runs.
            child.stderr.on('data', (err) => {
                output += err;
            });
            // Set up a timeout to end the child process and throw an error.
            const timeout = setTimeout(() => {
                // Kill child process and remove listeners so 'close' event doesn't get
                // read after child process is killed.
                child.removeAllListeners();
                child.kill();
                return reject(new Error('The executable failed to finish within the timeout specified.'));
            }, this.timeoutMillis);
            child.on('close', (code) => {
                // Cancel timeout if executable closes before timeout is reached.
                clearTimeout(timeout);
                if (code === 0) {
                    // If the executable completed successfully, try to return the parsed response.
                    try {
                        const responseJson = JSON.parse(output);
                        const response = new executable_response_1.ExecutableResponse(responseJson);
                        return resolve(response);
                    }
                    catch (error) {
                        if (error instanceof executable_response_1.ExecutableResponseError) {
                            return reject(error);
                        }
                        return reject(new executable_response_1.ExecutableResponseError(`The executable returned an invalid response: ${output}`));
                    }
                }
                else {
                    return reject(new pluggable_auth_client_1.ExecutableError(output, code.toString()));
                }
            });
        });
    }
    /**
     * Checks user provided output file for response from previous run of
     * executable and return the response if it exists, is formatted correctly, and is not expired.
     */
    async retrieveCachedResponse() {
        if (!this.outputFile || this.outputFile.length === 0) {
            return undefined;
        }
        let filePath;
        try {
            filePath = await fs.promises.realpath(this.outputFile);
        }
        catch (_a) {
            // If file path cannot be resolved, return undefined.
            return undefined;
        }
        if (!(await fs.promises.lstat(filePath)).isFile()) {
            // If path does not lead to file, return undefined.
            return undefined;
        }
        const responseString = await fs.promises.readFile(filePath, {
            encoding: 'utf8',
        });
        if (responseString === '') {
            return undefined;
        }
        try {
            const responseJson = JSON.parse(responseString);
            const response = new executable_response_1.ExecutableResponse(responseJson);
            // Check if response is successful and unexpired.
            if (response.isValid()) {
                return new executable_response_1.ExecutableResponse(responseJson);
            }
            return undefined;
        }
        catch (error) {
            if (error instanceof executable_response_1.ExecutableResponseError) {
                throw error;
            }
            throw new executable_response_1.ExecutableResponseError(`The output file contained an invalid response: ${responseString}`);
        }
    }
    /**
     * Parses given command string into component array, splitting on spaces unless
     * spaces are between quotation marks.
     */
    static parseCommand(command) {
        // Split the command into components by splitting on spaces,
        // unless spaces are contained in quotation marks.
        const components = command.match(/(?:[^\s"]+|"[^"]*")+/g);
        if (!components) {
            throw new Error(`Provided command: "${command}" could not be parsed.`);
        }
        // Remove quotation marks from the beginning and end of each component if they are present.
        for (let i = 0; i < components.length; i++) {
            if (components[i][0] === '"' && components[i].slice(-1) === '"') {
                components[i] = components[i].slice(1, -1);
            }
        }
        return components;
    }
}
exports.PluggableAuthHandler = PluggableAuthHandler;
