UNPKG

@badc0d3/piece-bash-runner

Version:

Run bash commands with NFS/SMB mounting support in Activepieces

337 lines (326 loc) 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runBashCommandSandboxed = void 0; const pieces_framework_1 = require("@activepieces/pieces-framework"); const dockerode_1 = __importDefault(require("dockerode")); exports.runBashCommandSandboxed = (0, pieces_framework_1.createAction)({ name: 'run-bash-command-sandboxed', displayName: 'Run Bash Command (Sandboxed)', description: 'Execute bash commands in a Docker container with NFS/SMB mounting support. Requires Docker socket access (mount with -v /var/run/docker.sock:/var/run/docker.sock)', props: { command: pieces_framework_1.Property.LongText({ displayName: 'Bash Command', description: 'The bash command or script to execute', required: true, defaultValue: `#!/bin/bash # Example: Show system information in container echo "Container Information:" echo "====================" echo "Hostname: $(hostname)" echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)" echo "" echo "Current directory: $(pwd)" echo "Files:" ls -la echo "" echo "Disk usage:" df -h`, }), mountConfig: pieces_framework_1.Property.Object({ displayName: 'Mount Configuration (Optional)', description: 'Configure NFS or SMB mount inside container', required: false, }), mountType: pieces_framework_1.Property.StaticDropdown({ displayName: 'Mount Type', description: 'Type of network mount (leave empty for no mount)', required: false, options: { options: [ { label: 'None', value: '' }, { label: 'NFS', value: 'nfs' }, { label: 'SMB/CIFS', value: 'smb' }, ], }, defaultValue: '', }), mountSource: pieces_framework_1.Property.ShortText({ displayName: 'Mount Source', description: 'Network path (e.g., server:/path or //server/share)', required: false, }), mountPoint: pieces_framework_1.Property.ShortText({ displayName: 'Mount Point', description: 'Mount point inside container', required: false, defaultValue: '/mnt/network', }), mountOptions: pieces_framework_1.Property.ShortText({ displayName: 'Mount Options', description: 'Additional mount options', required: false, defaultValue: '', }), mountUsername: pieces_framework_1.Property.ShortText({ displayName: 'Username (SMB only)', description: 'Username for SMB authentication', required: false, }), mountPassword: pieces_framework_1.Property.ShortText({ displayName: 'Password (SMB only)', description: 'Password for SMB authentication (use secure storage in production)', required: false, }), dockerImage: pieces_framework_1.Property.ShortText({ displayName: 'Docker Image', description: 'Docker image to use (must have bash and mount utilities)', required: false, defaultValue: 'ubuntu:latest', }), timeout: pieces_framework_1.Property.Number({ displayName: 'Timeout (seconds)', description: 'Maximum execution time in seconds', required: false, defaultValue: 30, }), runAsRoot: pieces_framework_1.Property.Checkbox({ displayName: 'Run as Root', description: 'Run commands as root user instead of non-root user (less secure)', required: false, defaultValue: false, }), }, async run(context) { const { command, mountType, mountSource, mountPoint, mountOptions, mountUsername, mountPassword, dockerImage, timeout, runAsRoot } = context.propsValue; const docker = new dockerode_1.default(); const imageName = dockerImage || 'ubuntu:latest'; // Clean the user's command - remove any leading shebang const cleanedCommand = command.replace(/^#!.*\n/, '').trim(); try { // Check if image exists locally console.log(`Checking for Docker image ${imageName}...`); try { await docker.getImage(imageName).inspect(); console.log(`Image ${imageName} found locally`); } catch (error) { // Image doesn't exist locally, pull it console.log(`Image ${imageName} not found locally, pulling...`); const stream = await docker.pull(imageName); // Wait for pull to complete await new Promise((resolve, reject) => { docker.modem.followProgress(stream, (err, res) => { if (err) { reject(err); } else { console.log(`Successfully pulled ${imageName}`); resolve(res); } }, (event) => { if (event.status) { console.log(`${event.status}${event.progress ? ': ' + event.progress : ''}`); } }); }); } // Build the command with mount if needed let fullScript = ''; if (mountType && mountSource) { // Build mount command let mountCommand = ''; let installPackages = ''; if (mountType === 'nfs') { installPackages = ` # Install NFS utilities as root export DEBIAN_FRONTEND=noninteractive if command -v apt-get &>/dev/null; then timeout 30 apt-get update -qq >/dev/null 2>&1 || echo "[Warning] apt-get update failed" >&2 timeout 30 apt-get install -y --no-install-recommends nfs-common >/dev/null 2>&1 || echo "[Warning] nfs-common installation failed" >&2 elif command -v apk &>/dev/null; then apk add --no-cache nfs-utils >/dev/null 2>&1 || echo "[Warning] nfs-utils installation failed" >&2 elif command -v yum &>/dev/null; then yum install -y nfs-utils >/dev/null 2>&1 || echo "[Warning] nfs-utils installation failed" >&2 fi`; const nfsOptions = mountOptions || 'rw,sync'; mountCommand = `mount -t nfs -o ${nfsOptions} ${mountSource} ${mountPoint}`; } else if (mountType === 'smb') { installPackages = ` # Install CIFS utilities as root export DEBIAN_FRONTEND=noninteractive if command -v apt-get &>/dev/null; then timeout 30 apt-get update -qq >/dev/null 2>&1 || echo "[Warning] apt-get update failed" >&2 timeout 30 apt-get install -y --no-install-recommends cifs-utils >/dev/null 2>&1 || echo "[Warning] cifs-utils installation failed" >&2 elif command -v apk &>/dev/null; then apk add --no-cache cifs-utils >/dev/null 2>&1 || echo "[Warning] cifs-utils installation failed" >&2 elif command -v yum &>/dev/null; then yum install -y cifs-utils >/dev/null 2>&1 || echo "[Warning] cifs-utils installation failed" >&2 fi`; let smbOptions = mountOptions || 'rw'; if (mountUsername) { smbOptions += `,username=${mountUsername}`; if (mountPassword) { smbOptions += `,password=${mountPassword}`; } } mountCommand = `mount -t cifs -o ${smbOptions} ${mountSource} ${mountPoint}`; } fullScript = `#!/bin/bash # Script runs as root initially set +e ${installPackages} # Create mount point as root mkdir -p ${mountPoint} 2>/dev/null || echo "[Warning] Could not create mount point ${mountPoint}" >&2 # Mount network drive as root if ${mountCommand}; then : # Mount successful, no output else MOUNT_EXIT=$? echo "[Error] Mount failed with exit code: $MOUNT_EXIT" >&2 fi ${runAsRoot ? ` # Execute command as root cd /workspace ${cleanedCommand} ` : ` # Switch to non-root user for command execution su - activepieces << 'EOF' cd /workspace ${cleanedCommand} EOF `} # Unmount as root (after user command completes) umount ${mountPoint} 2>/dev/null || true `; } else { // No mount needed fullScript = `#!/bin/bash # No mount configuration set +e ${runAsRoot ? ` # Execute command as root cd /workspace ${cleanedCommand} ` : ` # Run command as non-root user su - activepieces << 'EOF' cd /workspace ${cleanedCommand} EOF `} `; } // Encode the script to avoid shell escaping issues const encodedScript = Buffer.from(fullScript).toString('base64'); // Create container const container = await docker.createContainer({ Image: imageName, Cmd: ['bash', '-c', ` # Running as root initially export DEBIAN_FRONTEND=noninteractive # Install necessary packages if needed if command -v apt-get &>/dev/null; then apt-get update -qq >/dev/null 2>&1 || echo "Warning: apt-get update failed" >&2 # Only install coreutils if timeout command is missing if ! command -v timeout &>/dev/null; then apt-get install -y --no-install-recommends coreutils >/dev/null 2>&1 || echo "Warning: coreutils installation failed" >&2 fi fi # Create non-root user (without sudo privileges) if ! id activepieces &>/dev/null; then groupadd -g 1001 activepieces 2>/dev/null || true useradd -u 1001 -g 1001 -m -s /bin/bash activepieces 2>/dev/null || { # If UID 1001 is taken, find next available UID for uid in {1002..1010}; do if useradd -u $uid -g activepieces -m -s /bin/bash activepieces 2>/dev/null; then break fi done } fi # Create workspace directory with proper permissions mkdir -p /workspace chown activepieces:activepieces /workspace # Decode the user script echo '${encodedScript}' | base64 -d > /tmp/user_script.sh chmod +x /tmp/user_script.sh chown activepieces:activepieces /tmp/user_script.sh # Execute the script (mount operations will be handled inside) bash /tmp/user_script.sh `], WorkingDir: '/workspace', HostConfig: { AutoRemove: true, Memory: 512 * 1024 * 1024, // 512MB CpuQuota: 50000, // 50% CPU CapAdd: ['SYS_ADMIN'], // Required for mounting SecurityOpt: ['apparmor:unconfined'], // Required for some mount operations }, AttachStdout: true, AttachStderr: true, }); // Start container const stream = await container.attach({ stream: true, stdout: true, stderr: true }); await container.start(); // Collect output let stdout = ''; let stderr = ''; stream.on('data', (chunk) => { const data = chunk.toString(); // Docker multiplexes stdout/stderr, need to parse if (chunk[0] === 1) { stdout += data.slice(8); } else if (chunk[0] === 2) { stderr += data.slice(8); } else { stdout += data; } }); // Set timeout const executionTimeout = (timeout || 30) * 1000; const timeoutHandle = setTimeout(async () => { try { await container.kill(); } catch (e) { // Container might have already stopped } }, executionTimeout); // Wait for container to finish try { await container.wait(); } finally { clearTimeout(timeoutHandle); } return { success: true, output: stdout, stdout: stdout, stderr: stderr, executionTime: new Date().toISOString(), }; } catch (error) { console.error('Docker execution error:', error); // Provide more specific error messages if (error.message?.includes('connect ENOENT')) { throw new Error('Docker socket not found. Please ensure Docker is running and the socket is mounted with -v /var/run/docker.sock:/var/run/docker.sock'); } else if (error.statusCode === 404) { throw new Error(`Docker image ${imageName} not found and could not be pulled. Please check your internet connection.`); } else { throw error; } } }, }); //# sourceMappingURL=run-bash-command-sandboxed.js.map