Initial commit
This commit is contained in:
6
backend/src/config.js
Normal file
6
backend/src/config.js
Normal 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
34
backend/src/containers.js
Normal 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
14
backend/src/cors.js
Normal 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
51
backend/src/http.js
Normal 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
14
backend/src/setup.js
Normal 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
63
backend/src/websocket.js
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user