Initial commit

This commit is contained in:
2021-11-01 20:12:55 +01:00
commit b2a04ba399
20 changed files with 9715 additions and 0 deletions

6
backend/src/config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
port: process.env.PORT || 1234,
dev: process.env.DEV || false,
containerBinary: 'podman',
containerLabel: 'codebox-worker'
}

34
backend/src/containers.js Normal file
View File

@@ -0,0 +1,34 @@
import { execFile as execFileC } from 'child_process'
import { promisify } from 'util'
import config from './config.js'
import pty from 'node-pty'
const execFile = promisify(execFileC)
const exec = async (binary, args = []) => {
return (await execFile(binary, args)).stdout
}
export async function containerExists (containerId) {
return (await runningContainers()).some(c => c.Id === containerId)
}
export async function runningContainers () {
return JSON.parse((await exec(config.containerBinary,
['ps', '--filter', `label=${config.containerLabel}`, '--format=json']
)))
}
export function getContainerShell (containerId, shell = 'sh') {
return pty.spawn(config.containerBinary, ['exec', '-it', containerId, shell], {
name: 'xterm-color',
cols: 30,
rows: 40,
})
}
export function startContainer (image = 'alpine', cmd = ['sh', '-c', 'while true; do sleep 1d; done']) {
console.log(['run', '-d', '-l', config.containerLabel, image, ...cmd])
return exec(config.containerBinary, ['run', '--rm', '-d', '-l', config.containerLabel, image, ...cmd])
}

14
backend/src/cors.js Normal file
View File

@@ -0,0 +1,14 @@
export default (app, origin = 'http://localhost:8080') => {
app.use((req, res, next) => {
res
.header('Access-Control-Allow-Origin', origin)
.header('Access-Control-Allow-Method', 'POST')
.header('Access-Control-Allow-Headers', 'content-type')
next()
})
app.options('*', (req, res) => {
res.status(204). send()
})
}

51
backend/src/http.js Normal file
View File

@@ -0,0 +1,51 @@
import { runningContainers, startContainer } from './containers.js'
import bodyParser from 'body-parser'
import config from './config.js'
import express from 'express'
import cors from './cors.js'
import http from 'http'
export default (sessions) => {
const app = express()
app.use(bodyParser.json())
if (config.dev) {
cors(app)
}
app.get('/containers', async (req, res) => {
return res.json(
(await runningContainers())
.map(c => ({ id: c.Id, image: c.Image, labels: c.Labels }))
)
})
app.post('/containers', async (req, res) => {
if ((req.body.image && typeof req.body.image !== 'string') || (req.body.cmd && Array.isArray(req.body.cmd))) {
return res.send('invalid arguments').status(401)
}
const container = await startContainer(req.body.image, req.body.cmd)
res.send(container)
})
app.post('/containers/:container/:session/resize', (req, res) => {
const session = sessions[req.params.session]
if (!session || session.container !== req.params.container) {
return res.send('invalid session').status(401)
}
if (!req.body.cols || !req.body.rows) {
return res.send('missing arguments').status(401)
}
session.term.resize(req.body.cols, req.body.rows)
res.send('ok')
})
return http.createServer(app)
}

14
backend/src/setup.js Normal file
View File

@@ -0,0 +1,14 @@
import http from './http.js'
import websocket from './websocket.js'
import config from './config.js'
export default () => {
const sessions = {}
const server = http(sessions)
websocket(server, sessions)
server.listen(config.port)
return { sessions, server }
}

63
backend/src/websocket.js Normal file
View File

@@ -0,0 +1,63 @@
import { containerExists, getContainerShell } from './containers.js'
import { WebSocketServer } from 'ws'
export default (server, sessions) => {
const wss = new WebSocketServer({ noServer: true })
server.on('upgrade', async (request, socket, head) => {
const forbidden = () => {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
}
const path = request.url.substr(1).split('/')
if (path.length !== 3 || path[0] !== 'ws') {
return forbidden()
}
const [_, container, sessionId] = path
const session = sessions[sessionId]
if (session && session.container !== container) {
console.log('wrong session')
return forbidden()
}
if (!(await containerExists(container))) {
console.log('no container')
return forbidden()
}
if (!session) {
sessions[sessionId] = { container }
}
request.session = sessions[sessionId]
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request);
});
})
wss.on('connection', (ws, req) => {
if (!req.session.term) {
req.session.term = getContainerShell(req.session.container)
}
ws.on('message', message => {
const decoded = message.toString()
req.session.term.write(decoded)
})
req.session.term.onData(data => {
ws.send(data)
})
req.session.term.onExit(exit => {
ws.send(`Process terminated with code ${exit.exitCode}`)
ws.close()
})
})
}