UNPKG

kui-shell

Version:

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

226 lines (191 loc) 6.8 kB
/* * 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. */ const debug = require('debug')('proxy/exec') const { dirname, join } = require('path') const { exec, spawn } = require('child_process') const express = require('express') const { v4: uuid } = require('uuid') const { parse: parseCookie } = require('cookie') const sessionKey = 'kui_websocket_auth' const mainPath = join(dirname(require.resolve('@kui-shell/core')), 'main/main.js') const { main: wssMain } = require('@kui-shell/plugin-bash-like') const { StdioChannelWebsocketSide } = require('@kui-shell/plugin-bash-like') process.on('uncaughtException', async err => { debug('uncaughtException') debug(err) console.error(err.toString()) process.exit(1) }) process.on('exit', code => { debug('proxy exiting', code) }) async function allocateUser() { debug('allocateUser') const uid = undefined const gid = undefined // inherit HOME if we haven't otherwise decided to use a specific // uid/gid for this tenant const HOME = uid === undefined && process.env.HOME return { uid, gid, HOME } } /** thin wrapper on child_process.exec */ function main(cmdline, execOptions, server, port, host, existingSession, locale) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const { uid, gid, HOME } = existingSession || (await allocateUser()) const options = { uid, gid, cwd: execOptions.cwd || '/', env: Object.assign(execOptions.env || {}, { TRAVIS_HOME: process.env.TRAVIS_HOME, HOME, LOCALE: locale, DEBUG: process.env.DEBUG, DEVMODE: true, TRAVIS_JOB_ID: process.env.TRAVIS_JOB_ID, KUI_HEADLESS: true, KUI_REPL_MODE: 'stdout', KUI_EXEC_OPTIONS: JSON.stringify(execOptions) }) } const wsOpen = cmdline === 'bash websocket open' if (wsOpen) { // N is the random identifier for this connection const N = uuid() const session = existingSession || { uid, gid, token: uuid() // use a different uuid for the session cookie } const sessionToken = Buffer.from(JSON.stringify(session)).toString('base64') const cookie = { key: sessionKey, session } const { wss } = await wssMain(N, server, port, cookie) debug('spawning subprocess') const child = spawn(process.argv[0], [mainPath, 'bash', 'websocket', 'stdio'], options) child.on('error', err => { console.error('error spawning subprocess', err) reject(err) }) child.on('exit', code => { debug('subprocess exit', code) }) const channel = new StdioChannelWebsocketSide(wss) await channel.init(child, process.env.KUI_HEARTBEAT_INTERVAL || 30000) channel.once('closed', () => { debug('channel closed') }) channel.once('open', () => { debug('channel open') const proto = process.env.KUI_USE_HTTP === 'true' ? 'ws' : 'wss' resolve({ type: 'object', cookie: { key: sessionKey, value: sessionToken, path: `/bash/${N}` }, response: { url: `${proto}://${host}/bash/${N}` } }) }) } else { debug('using plain exec', cmdline, options) exec(`${process.argv[0]} "${mainPath}" ${cmdline}`, options, (err, stdout, stderr) => { if (stderr) { console.error(stderr) } if (err) { reject(err) } else { debug('stdout', stdout) try { resolve(JSON.parse(stdout)) } catch (err) { resolve({ type: 'string', response: stdout }) } } }) } }) } /** * * @param server an https server * @param port the port on which that server is listening * */ module.exports = (server, port) => { debug('initializing proxy executor', port) const exec = commandExtractor => async function(req, res) { // debug('hostname', req.hostname) // debug('headers', req.headers) try { const { command, execOptions = {} } = commandExtractor(req) debug('command', command) // so that our catch (err) below is used upon command execution failure execOptions.rethrowErrors = true // parse the user's locale const locale = req.headers['accept-language'] && req.headers['accept-language'].split(',')[0] /* if (execOptions && execOptions.credentials) { // FIXME this should not be a global setValidCredentials(execOptions.credentials) } */ /* const execOptionsWithServer = Object.assign({}, execOptions, { server, port, host: req.headers.host }) */ const sessionToken = parseCookie(req.headers.cookie || '')[sessionKey] const session = sessionToken && JSON.parse(Buffer.from(sessionToken, 'base64').toString('utf-8')) const { type, cookie, response } = await main( command, execOptions, server, port, req.headers.host, session, locale ) if (cookie) { res.header('Access-Control-Allow-Credentials', 'true') res.cookie(cookie.key, cookie.value, { httpOnly: true, // clients are not allowed to read this cookie secure: process.env.KUI_USE_HTTP !== 'true', // https required? path: cookie.path // lock down the cookie to this channel's path }) } const code = response.code || response.statusCode || 200 res.status(code).json({ type, response }) } catch (err) { console.error('exception in command execution', err.code, err.message, err) const possibleCode = err.code || err.statusCode const code = possibleCode && typeof possibleCode === 'number' ? possibleCode : 500 res.status(code).send(err.message || err) } } const router = express.Router() /** GET exec */ router.get('/:command', exec(req => req.params)) /** POST exec */ router.post('/', exec(req => req.body)) return router }