/*
 * Copyright 2019 IBM Corporation
 *
 * 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.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import Debug from 'debug';
import * as fs from 'fs';
import { promisify } from 'util';
import { join } from 'path';
import { exec } from 'child_process';
import { createServer } from 'https';
import { parse as parseCookie } from 'cookie';
import { StdioChannelKuiSide } from './stdio-channel';
const debug = Debug('plugins/bash-like/pty/server');
let portRange = 8083;
const servers = [];
const verifySession = (expectedCookie) => {
    return ({ req }, cb) => {
        const cookies = parseCookie(req.headers.cookie || '');
        const sessionToken = cookies[expectedCookie.key];
        if (sessionToken) {
            try {
                const actualSession = JSON.parse(Buffer.from(sessionToken, 'base64').toString('utf-8'));
                if (actualSession.token === expectedCookie.session.token) {
                    cb(true); // eslint-disable-line standard/no-callback-literal
                    return;
                }
                else {
                    console.error('token found, but mismatched values', expectedCookie, actualSession);
                }
            }
            catch (err) {
                console.error('error parsing session token', sessionToken, err);
            }
        }
        // intentional fall-through for invalid session
        console.error('invalid session for websocket upgrade', expectedCookie, cookies[expectedCookie.key], cookies);
        cb(false, 401, 'Invalid authorization for websocket upgrade'); // eslint-disable-line standard/no-callback-literal
    };
};
/**
 * Allocate a port
 *
 */
const getPort = () => 
// eslint-disable-next-line no-async-promise-executor
new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
    const { createServer } = yield import('net');
    const iter = () => {
        const port = portRange;
        portRange += 1;
        const server = createServer();
        server.listen(port, () => {
            server.once('close', function () {
                resolve(port);
            });
            server.close();
        });
        server.on('error', (err) => {
            if (err.code === 'EADDRINUSE') {
                iter();
            }
            else {
                reject(err);
            }
        });
    };
    iter();
}));
// these bits are to avoid macOS garbage; those lines marked with //* here:
// $ bash -i -l -c ls
// * Restored session: Tue Apr  2 19:24:55 EDT 2019
//  [[ VALID OUTPUT ]]
// * Saving session...
// * ...saving history...
// * ...completed.
const touch = (filename) => {
    const open = promisify(fs.open);
    const close = promisify(fs.close);
    return open(filename, 'w').then(close);
};
let cacheHasBashSessionsDisable;
const BSD = () => join(process.env.HOME, '.bash_sessions_disable');
const enableBashSessions = () => __awaiter(void 0, void 0, void 0, function* () {
    yield promisify(fs.unlink)(BSD());
});
export const disableBashSessions = () => __awaiter(void 0, void 0, void 0, function* () {
    if (process.platform === 'darwin') {
        if (cacheHasBashSessionsDisable === undefined) {
            // eslint-disable-next-line node/no-deprecated-api
            cacheHasBashSessionsDisable = yield promisify(fs.exists)(BSD());
        }
        if (!cacheHasBashSessionsDisable) {
            yield touch(BSD());
            return enableBashSessions;
        }
    }
    return () => __awaiter(void 0, void 0, void 0, function* () {
        /* no-op */
    });
});
let cachedLoginShell;
export const getLoginShell = () => {
    return new Promise((resolve, reject) => {
        if (cachedLoginShell) {
            debug('returning cached login shell', cachedLoginShell);
            resolve(cachedLoginShell);
        }
        else if (process.env.SHELL) {
            // Note how we intentionally assume bash here, even on windows
            resolve(process.env.SHELL);
        }
        else {
            const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
            if (process.env.TRAVIS_JOB_ID !== undefined || process.platform === 'win32') {
                debug('using defaultShell for travis');
                cachedLoginShell = defaultShell;
                resolve(cachedLoginShell);
            }
            else {
                try {
                    exec(`${defaultShell} -l -c "echo $SHELL"`, (err, stdout, stderr) => __awaiter(void 0, void 0, void 0, function* () {
                        if (err) {
                            console.error('error in getLoginShell subroutine', err);
                            if (stderr) {
                                console.error(stderr);
                            }
                            reject(err);
                        }
                        else {
                            cachedLoginShell = stdout.trim() || defaultShell;
                            debug('login shell', cachedLoginShell);
                            resolve(cachedLoginShell);
                        }
                    }));
                }
                catch (err) {
                    console.error('error in exec of getLoginShell subroutine', err);
                    resolve(defaultShell);
                }
            }
        }
    });
};
export function getShellOpts() {
    return __awaiter(this, void 0, void 0, function* () {
        const kuirc = (yield import('./kuirc')).default;
        const bashShellOpts = process.platform === 'win32' ? undefined : ['--rcfile', yield kuirc, '-i', '-c', '--'];
        const { shellExe: s, shellOpts: o } = yield import('@kui-shell/client/config.d/proxy.json').catch(() => {
            return { shellExe: '', shellOpts: [] };
        });
        const shellExe = s || (process.platform === 'win32' ? 'powershell.exe' : '/bin/bash');
        const shellOpts = o && Array.isArray(o) && o.length > 0 ? o : process.platform === 'win32' ? [] : bashShellOpts;
        return {
            shellExe,
            shellOpts
        };
    });
}
/**
 * Use precomputed shell aliases
 *
 */
let shellAliases = {};
export function setShellAliases(aliases) {
    shellAliases = aliases;
}
/**
 *
 *
 */
export const onConnection = (exitNow, uid, gid) => (ws) => __awaiter(void 0, void 0, void 0, function* () {
    debug('onConnection', uid, gid, ws);
    // for now, we need to use a dynamic import here, because the plugin
    // compiler does not work versus node-pty's eager loading of the
    // native modules -- we compile the native modules against electron,
    // but the plugin compiler uses the platform nodejs :(
    const { spawn } = yield import('node-pty-prebuilt-multiarch');
    // re: importing node-pty twice: this is clumsy because typescript
    // doesn't support module imports for dynamic imports, and node-pty
    // exports IPty under a module of its creation
    // @see https://github.com/microsoft/TypeScript/issues/22445
    const shells = {};
    // For all websocket data send it to the shell
    ws.on('message', (data) => __awaiter(void 0, void 0, void 0, function* () {
        try {
            const msg = JSON.parse(data);
            switch (msg.type) {
                case 'xon': {
                    const shell = msg.uuid && (yield shells[msg.uuid]);
                    if (shell) {
                        const RESUME = '\x11'; // this is XON
                        shell.write(RESUME);
                    }
                    break;
                }
                case 'xoff': {
                    const shell = msg.uuid && (yield shells[msg.uuid]);
                    if (shell) {
                        const PAUSE = '\x13'; // this is XOFF
                        shell.write(PAUSE);
                    }
                    break;
                }
                case 'kill': {
                    const shell = msg.uuid && (yield shells[msg.uuid]);
                    if (shell) {
                        shell.kill(msg.signal || 'SIGHUP');
                        return exitNow(msg.exitCode || 0);
                    }
                    break;
                }
                case 'exit':
                    return exitNow(msg.exitCode || 0);
                case 'request': {
                    const { internalBeCarefulExec: exec } = yield import('@kui-shell/core');
                    if (msg.env) {
                        process.env = msg.env;
                    }
                    const terminate = (str) => {
                        ws.send(str);
                        // ws.send(`___kui_exit___ ${msg.uuid}`)
                    };
                    try {
                        const response = yield exec(msg.cmdline, Object.assign({}, msg.execOptions, { rethrowErrors: true }));
                        debug('got response');
                        terminate(JSON.stringify({
                            type: 'object',
                            uuid: msg.uuid,
                            response
                        }));
                    }
                    catch (error) {
                        debug('got error', error.message);
                        const err = error;
                        terminate(JSON.stringify({
                            type: 'object',
                            uuid: msg.uuid,
                            response: {
                                code: err.code || err.statusCode,
                                statusCode: err.statusCode,
                                message: err.message,
                                stack: err.stack
                            }
                        }));
                    }
                    break;
                }
                case 'exec': {
                    // eslint-disable-next-line no-async-promise-executor
                    shells[msg.uuid] = new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
                        try {
                            const env = Object.assign({}, msg.env || process.env, { KUI: 'true' });
                            if (process.env.DEBUG && (!msg.env || !msg.env.DEBUG)) {
                                // don't pass DEBUG unless the user asked for it!
                                delete env.DEBUG;
                            }
                            const end = msg.cmdline.indexOf(' ');
                            const cmd = msg.cmdline.slice(0, end < 0 ? msg.cmdline.length : end); // FIXME quoted first arg
                            const aliasedCmd = shellAliases[cmd];
                            const cmdline = aliasedCmd ? msg.cmdline.replace(new RegExp(`^${cmd}`), aliasedCmd) : msg.cmdline;
                            const { shellExe, shellOpts } = yield getShellOpts();
                            let shell = spawn(shellExe, shellOpts.concat([cmdline]), {
                                uid,
                                gid,
                                name: 'xterm-color',
                                rows: msg.rows,
                                cols: msg.cols,
                                cwd: msg.cwd || process.cwd(),
                                env
                            });
                            // termios.setattr(shell['_fd'], { lflag: { ECHO: false } })
                            // send all PTY data out to the websocket client
                            shell.on('data', (data) => {
                                ws.send(JSON.stringify({ type: 'data', data, uuid: msg.uuid }));
                            });
                            shell.on('exit', (exitCode) => {
                                shell = undefined;
                                if (msg.uuid)
                                    delete shells[msg.uuid];
                                ws.send(JSON.stringify({ type: 'exit', exitCode, uuid: msg.uuid }));
                                // exitNow(exitCode)
                            });
                            ws.send(JSON.stringify({ type: 'state', state: 'ready', uuid: msg.uuid }));
                            resolve(shell);
                        }
                        catch (err) {
                            console.error('could not exec', err);
                            reject(err);
                        }
                    }));
                    break;
                }
                case 'data':
                    try {
                        const shell = msg.uuid && (yield shells[msg.uuid]);
                        if (shell) {
                            return shell.write(msg.data);
                        }
                        else {
                            console.error('could not write to the shell, as we had no uuid, or no matching shell instance', msg.uuid, msg.data);
                        }
                    }
                    catch (err) {
                        console.error('could not write to the shell', err);
                    }
                    break;
                case 'resize':
                    try {
                        const shell = msg.uuid && (yield shells[msg.uuid]);
                        if (shell) {
                            return shell.resize(msg.cols, msg.rows);
                        }
                        else {
                            console.error('could not resize pty, as we had no uuid, or no matching shell instance', msg.uuid);
                        }
                    }
                    catch (err) {
                        console.error(`error in resize ${msg.cols} ${msg.rows}`);
                        console.error('could not resize pty', err);
                    }
                    break;
            }
        }
        catch (err) {
            console.error(err);
        }
    }));
});
/**
 * If we haven't been given an https server instance, create one
 *
 */
const createDefaultServer = () => {
    return createServer({
        key: fs.readFileSync('.keys/key.pem', 'utf8'),
        cert: fs.readFileSync('.keys/cert.pem', 'utf8'),
        passphrase: process.env.PASSPHRASE,
        requestCert: false,
        rejectUnauthorized: false
    });
};
/**
 * Spawn the shell
 * vague origins: http://krasimirtsonev.com/blog/article/meet-evala-your-terminal-in-the-browser-extension
 */
let cachedWss;
let cachedPort;
export const main = (N, server, preexistingPort, expectedCookie) => __awaiter(void 0, void 0, void 0, function* () {
    if (cachedWss) {
        return cachedPort;
    }
    else {
        const WebSocket = yield import('ws');
        // eslint-disable-next-line no-async-promise-executor
        return new Promise((resolve) => __awaiter(void 0, void 0, void 0, function* () {
            const idx = servers.length;
            const cleanupCallback = yield disableBashSessions();
            const exitNow = (exitCode) => __awaiter(void 0, void 0, void 0, function* () {
                yield cleanupCallback(exitCode);
                const { wss, server } = servers.splice(idx, 1)[0];
                wss.close();
                if (server) {
                    server.close();
                }
                // process.exit(exitCode)
            });
            if (preexistingPort) {
                // if we were given a session cookie, then use the
                // verifyClient functionality of WebSocket.Server to enforce
                // the session's validity
                const wss = new WebSocket.Server({
                    noServer: true,
                    verifyClient: expectedCookie && verifySession(expectedCookie)
                });
                servers.push({ wss });
                // only handle upgrades for the "N" index we own
                const doUpgrade = (request, socket, head) => {
                    const match = request.url.match(/\/bash\/([0-9a-z-]+)/);
                    const yourN = match && match[1]; // do we own this upgrade?
                    if (yourN === N) {
                        server.removeListener('upgrade', doUpgrade);
                        wss.handleUpgrade(request, socket, head, function done(ws) {
                            wss.emit('connection', ws, request);
                        });
                    }
                };
                server.on('upgrade', doUpgrade);
                resolve({ wss, port: cachedPort, exitNow });
            }
            else {
                cachedPort = yield getPort();
                const server = createDefaultServer();
                server.listen(cachedPort, () => __awaiter(void 0, void 0, void 0, function* () {
                    const wss = (cachedWss = new WebSocket.Server({ server }));
                    servers.push({ wss: cachedWss, server });
                    resolve({ wss, port: cachedPort, exitNow });
                }));
            }
        })).then(({ wss, port, exitNow }) => {
            if (!expectedCookie) {
                debug('listening for connection');
                wss.on('connection', onConnection(exitNow, expectedCookie && expectedCookie.session.uid, expectedCookie && expectedCookie.session.gid));
            }
            return { wss, port };
        });
    }
});
/**
 * Register command handlers
 *
 */
// const children = []
export default (commandTree) => {
    commandTree.listen('/bash/websocket/stdio', () => 
    // eslint-disable-next-line no-async-promise-executor
    new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
        try {
            yield new StdioChannelKuiSide().init(() => {
                console.error('done with stdiochannel');
                resolve();
            });
        }
        catch (err) {
            reject(err);
        }
    })), { requiresLocal: true });
    // Notes: this is a placeholder, simplify to make the command
    // resolver happy; the real implementation is to be found in the
    // proxy server, e.g. packages/proxy/app/routes/exec.js
    commandTree.listen('/bash/websocket/open', () => {
        throw new Error('Unsupported operation');
    }, { requiresLocal: true });
};
//# sourceMappingURL=server.js.map