UNPKG

kui-shell

Version:

This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool

356 lines 14.9 kB
"use strict"; 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = require("debug"); const fs = require("fs"); const util_1 = require("util"); const path_1 = require("path"); const child_process_1 = require("child_process"); const https_1 = require("https"); const cookie_1 = require("cookie"); const stdio_channel_1 = require("./stdio-channel"); const debug = debug_1.default('plugins/bash-like/pty/server'); let portRange = 8083; const servers = []; const verifySession = (expectedCookie) => { return ({ req }, cb) => { const cookies = cookie_1.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); return; } else { console.error('token found, but mismatched values', expectedCookie, actualSession); } } catch (err) { console.error('error parsing session token', sessionToken, err); } } console.error('invalid session for websocket upgrade', expectedCookie, cookies[expectedCookie.key], cookies); cb(false, 401, 'Invalid authorization for websocket upgrade'); }; }; const getPort = () => 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(); })); const touch = (filename) => { const open = util_1.promisify(fs.open); const close = util_1.promisify(fs.close); return open(filename, 'w').then(close); }; let cacheHasBashSessionsDisable; const BSD = () => path_1.join(process.env.HOME, '.bash_sessions_disable'); const enableBashSessions = () => __awaiter(void 0, void 0, void 0, function* () { yield util_1.promisify(fs.unlink)(BSD()); }); exports.disableBashSessions = () => __awaiter(void 0, void 0, void 0, function* () { if (process.platform === 'darwin') { if (cacheHasBashSessionsDisable === undefined) { cacheHasBashSessionsDisable = yield util_1.promisify(fs.exists)(BSD()); } if (!cacheHasBashSessionsDisable) { yield touch(BSD()); return enableBashSessions; } } return () => __awaiter(void 0, void 0, void 0, function* () { }); }); let cachedLoginShell; exports.getLoginShell = () => { return new Promise((resolve, reject) => { if (cachedLoginShell) { debug('returning cached login shell', cachedLoginShell); resolve(cachedLoginShell); } else if (process.env.SHELL) { resolve(process.env.SHELL); } else { const defaultShell = process.platform === 'win32' ? 'cmd' : '/bin/bash'; if (process.env.TRAVIS_JOB_ID !== undefined || process.platform === 'win32') { debug('using defaultShell for travis'); cachedLoginShell = defaultShell; resolve(cachedLoginShell); } else { try { child_process_1.exec(`${defaultShell} -l -c "echo $SHELL"`, (err, stdout, stderr) => { 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); } } } }); }; let shellAliases = {}; function setShellAliases(aliases) { shellAliases = aliases; } exports.setShellAliases = setShellAliases; exports.onConnection = (exitNow, uid, gid) => (ws) => __awaiter(void 0, void 0, void 0, function* () { debug('onConnection', uid, gid, ws); const { spawn } = yield Promise.resolve().then(() => require('node-pty-prebuilt-multiarch')); let shell; ws.on('message', (data) => __awaiter(void 0, void 0, void 0, function* () { try { const msg = JSON.parse(data); switch (msg.type) { case 'exit': return exitNow(msg.exitCode); case 'request': { const { REPL: { exec } } = yield Promise.resolve().then(() => require('@kui-shell/core')); if (msg.env) { process.env = msg.env; } const terminate = (str) => { ws.send(str); }; 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, message: err.message, stack: err.stack } })); } break; } case 'exec': { const env = Object.assign({}, msg.env || process.env, { KUI: 'true' }); if (process.env.DEBUG && (!msg.env || !msg.env.DEBUG)) { delete env.DEBUG; } try { const end = msg.cmdline.indexOf(' '); const cmd = msg.cmdline.slice(0, end < 0 ? msg.cmdline.length : end); const aliasedCmd = shellAliases[cmd]; const cmdline = aliasedCmd ? msg.cmdline.replace(new RegExp(`^${cmd}`), aliasedCmd) : msg.cmdline; shell = spawn(yield exports.getLoginShell(), ['-l', '-i', '-c', '--', cmdline], { uid, gid, name: 'xterm-color', rows: msg.rows, cols: msg.cols, cwd: msg.cwd || process.cwd(), env }); shell.on('data', (data) => { ws.send(JSON.stringify({ type: 'data', data, uuid: msg.uuid })); }); shell.on('exit', (exitCode) => { shell = undefined; ws.send(JSON.stringify({ type: 'exit', exitCode, uuid: msg.uuid })); }); ws.send(JSON.stringify({ type: 'state', state: 'ready', uuid: msg.uuid })); } catch (err) { console.error('could not exec', err); } break; } case 'data': try { if (shell) { return shell.write(msg.data); } } catch (err) { console.error('could not write to the shell', err); } break; case 'resize': try { if (shell) { return shell.resize(msg.cols, msg.rows); } } catch (err) { console.error(`error in resize ${msg.cols} ${msg.rows}`); console.error('could not resize pty', err); } break; } } catch (err) { console.error(err); } })); }); const createDefaultServer = () => { return https_1.createServer({ key: fs.readFileSync('.keys/key.pem', 'utf8'), cert: fs.readFileSync('.keys/cert.pem', 'utf8'), passphrase: process.env.PASSPHRASE, requestCert: false, rejectUnauthorized: false }); }; let cachedWss; let cachedPort; exports.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')); return new Promise((resolve) => __awaiter(void 0, void 0, void 0, function* () { const idx = servers.length; const cleanupCallback = yield exports.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(); } }); if (preexistingPort) { const wss = new WebSocket.Server({ noServer: true, verifyClient: expectedCookie && verifySession(expectedCookie) }); servers.push({ wss }); const doUpgrade = (request, socket, head) => { const match = request.url.match(/\/bash\/([0-9a-z-]+)/); const yourN = match && match[1]; 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', exports.onConnection(exitNow, expectedCookie && expectedCookie.session.uid, expectedCookie && expectedCookie.session.gid)); } return { wss, port }; }); } }); let count = 0; exports.default = (commandTree) => { commandTree.listen('/bash/websocket/stdio', () => new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () { try { yield new stdio_channel_1.StdioChannelKuiSide().init(() => { console.error('done with stdiochannel'); resolve(); }); } catch (err) { reject(err); } })), { noAuthOk: true }); commandTree.listen('/bash/websocket/open', ({ execOptions }) => new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () { const N = count++; const resolveWithHost = (port) => { const host = execOptions['host'] || `localhost:${port}`; resolve(`wss://${host}/bash/${N}`); }; if (execOptions.isProxied) { return exports.main(N.toString(), execOptions['server'], execOptions['port']) .then(resolveWithHost) .catch(reject); } else { const { ipcRenderer } = yield Promise.resolve().then(() => require('electron')); if (!ipcRenderer) { const error = new Error('electron not available'); error['code'] = 127; return reject(error); } ipcRenderer.send('/exec/invoke', JSON.stringify({ module: '@kui-shell/plugin-bash-like/pty/server', hash: N })); const channel = `/exec/response/${N}`; ipcRenderer.once(channel, (event, arg) => { const message = JSON.parse(arg); if (!message.success) { reject(message.error); } else { const port = message.returnValue; resolveWithHost(port); } }); } })), { noAuthOk: true }); }; //# sourceMappingURL=server.js.map