"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getShellOpts = getShellOpts;
exports.setShellAliases = setShellAliases;
exports.default = exports.main = exports.onConnection = exports.getLoginShell = exports.disableBashSessions = void 0;

var _debug = _interopRequireDefault(require("debug"));

var fs = _interopRequireWildcard(require("fs"));

var _util = require("util");

var _path = require("path");

var _child_process = require("child_process");

var _https = require("https");

var _cookie = require("cookie");

var _stdioChannel = require("./stdio-channel");

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
 * 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 = void 0 && (void 0).__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());
  });
};

const debug = (0, _debug.default)('plugins/bash-like/pty/server');
let portRange = 8083;
const servers = [];

const verifySession = expectedCookie => {
  return ({
    req
  }, cb) => {
    const cookies = (0, _cookie.parse)(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 Promise.resolve().then(() => require('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 = (0, _util.promisify)(fs.open);
  const close = (0, _util.promisify)(fs.close);
  return open(filename, 'w').then(close);
};

let cacheHasBashSessionsDisable;

const BSD = () => (0, _path.join)(process.env.HOME, '.bash_sessions_disable');

const enableBashSessions = () => __awaiter(void 0, void 0, void 0, function* () {
  yield (0, _util.promisify)(fs.unlink)(BSD());
});

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 (0, _util.promisify)(fs.exists)(BSD());
    }

    if (!cacheHasBashSessionsDisable) {
      yield touch(BSD());
      return enableBashSessions;
    }
  }

  return () => __awaiter(void 0, void 0, void 0, function* () {
    /* no-op */
  });
});

exports.disableBashSessions = disableBashSessions;
let cachedLoginShell;

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 {
          (0, _child_process.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);
        }
      }
    }
  });
};

exports.getLoginShell = getLoginShell;

function getShellOpts() {
  return __awaiter(this, void 0, void 0, function* () {
    const kuirc = (yield Promise.resolve().then(() => require('./kuirc'))).default;
    const bashShellOpts = process.platform === 'win32' ? undefined : ['--rcfile', yield kuirc, '-i', '-c', '--'];
    const {
      shellExe: s,
      shellOpts: o
    } = yield Promise.resolve().then(() => require('@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 = {};

function setShellAliases(aliases) {
  shellAliases = aliases;
}
/**
 *
 *
 */


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 Promise.resolve().then(() => require('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 Promise.resolve().then(() => require('@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
 *
 */


exports.onConnection = onConnection;

const createDefaultServer = () => {
  return (0, _https.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;

const main = (N, server, preexistingPort, expectedCookie) => __awaiter(void 0, void 0, void 0, function* () {
  if (cachedWss) {
    return cachedPort;
  } else {
    const WebSocket = yield Promise.resolve().then(() => require('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 = []


exports.main = main;

var _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 _stdioChannel.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
  });
};

exports.default = _default;