kui-shell
Version:
This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool
356 lines • 14.9 kB
JavaScript
;
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