forked from snxraven/peardock
Ship remaining roadmap items: encrypted registry vault, peer invite/revoke, Swarm/plugins behind flags, binary streams, engine create validation, deploy rollback, schema validation, fleet/access UI, metrics, fuzz/load/soak tests, systemd packaging, and release tooling. Mark ROADMAP fully complete.
90 lines
2.7 KiB
JavaScript
90 lines
2.7 KiB
JavaScript
/**
|
|
* Restricted Docker CLI command execution.
|
|
* Set PEARDOCK_UNRESTRICTED_CLI=1 to allow broader docker CLI (still admin-gated via roles).
|
|
*/
|
|
import { spawn } from 'child_process'
|
|
import * as validation from '../utils/validation.js'
|
|
import { Pushes } from '../../shared/protocol.js'
|
|
import { Roles } from '../../shared/protocol.js'
|
|
|
|
const UNRESTRICTED =
|
|
process.env.PEARDOCK_UNRESTRICTED_CLI === '1' ||
|
|
process.env.PEARDOCK_UNRESTRICTED_CLI === 'true'
|
|
|
|
const DANGEROUS_PATTERNS = ['exec', 'run', 'rm -f', 'prune', 'system prune', 'swarm', 'plugin']
|
|
|
|
export function registerDockerCliHandlers(session) {
|
|
session.respond('dockerCommand', async (args) => {
|
|
const commandStr = validation.sanitizeString(args.data || args.command, 500)
|
|
if (!commandStr || !commandStr.startsWith('docker ')) {
|
|
throw new Error('Invalid command format')
|
|
}
|
|
|
|
if (UNRESTRICTED) {
|
|
if (session.role !== Roles.admin) {
|
|
throw new Error('Unrestricted Docker CLI requires admin role')
|
|
}
|
|
} else if (DANGEROUS_PATTERNS.some((pattern) => commandStr.includes(pattern))) {
|
|
throw new Error(
|
|
'Command not allowed for security reasons (set PEARDOCK_UNRESTRICTED_CLI=1 for admin full CLI)'
|
|
)
|
|
}
|
|
|
|
const parts = commandStr.split(' ')
|
|
const executable = parts[0]
|
|
const cmdArgs = parts.slice(1)
|
|
if (executable !== 'docker') {
|
|
throw new Error('Only docker commands are allowed')
|
|
}
|
|
|
|
const connectionId = args.connectionId
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(executable, cmdArgs)
|
|
let settled = false
|
|
|
|
child.stdout.on('data', (data) => {
|
|
session.push(Pushes.dockerOutput, {
|
|
type: 'dockerOutput',
|
|
connectionId,
|
|
data: data.toString('base64'),
|
|
encoding: 'base64',
|
|
})
|
|
})
|
|
|
|
child.stderr.on('data', (data) => {
|
|
session.push(Pushes.dockerOutput, {
|
|
type: 'dockerOutput',
|
|
connectionId,
|
|
data: Buffer.from(`[ERROR] ${data.toString()}`).toString('base64'),
|
|
encoding: 'base64',
|
|
})
|
|
})
|
|
|
|
child.on('close', (code) => {
|
|
session.push(Pushes.dockerOutput, {
|
|
type: 'dockerOutput',
|
|
connectionId,
|
|
data: `[INFO] Command exited with code ${code}`,
|
|
})
|
|
if (!settled) {
|
|
settled = true
|
|
resolve({ success: true, exitCode: code })
|
|
}
|
|
})
|
|
|
|
child.on('error', (err) => {
|
|
if (!settled) {
|
|
settled = true
|
|
reject(err)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
session.respond('dockerTerminalResize', async () => {
|
|
// No PTY for docker CLI yet — acknowledge for UI
|
|
return { success: true }
|
|
})
|
|
}
|