CLI Connector Plugin ​
The CLI Connector plugin enables Raclette developers to securely integrate existing Unix command-line tools into the Raclette GUI. It is also possible to fully execute remote commands on other machines.
This expands the raclette functionality by allowing developers to not only interact with APIs but also with existing CLI tooling and use them or by developing user interfaces for them.
⚠️ IMPORTANT SECURITY NOTE
Exposing command-line tools through a web interface requires careful consideration. This plugin provides the tools to do so securely. For more information see Security Considerations.
Installation and Setup ​
1. Install the Plugin ​
Add the CLI Connector plugin to your Raclette project:
yarn add @raclettejs/cli-connector-plugin
2. Configure the Plugin ​
Add the plugin to your raclette.config.js
file in the plugins
section:
// raclette.config.js
export default {
// ... other configuration options
plugins: [
"@raclettejs/cli-connector-plugin",
// ... other plugins
],
// ... rest of configuration
}
3. Initialize and Register Scripts ​
Create an initialization function to register your CLI commands, typically in your application's startup code or inside your self-written plugins index.ts file.
Adding this to the server part is crucial!
Below the example within a self-written plugin context:
import type { PluginFastifyInstance } from "@raclettejs/raclette-core"
import {
registerScriptByConfig,
setupFastify,
} from "@raclettejs/cli-connector-plugin"
export const initializeCliCommands = (fastify: PluginFastifyInstance) => {
// Initialize the plugin with the Fastify instance
setupFastify(fastify)
// Register your CLI commands here
try {
registerScriptByConfig("system-info", {
exe: "/usr/bin/uname",
args: ["-a"],
})
fastify.log.info("CLI commands registered successfully")
} catch (error) {
fastify.log.error("Failed to register CLI commands:", error)
throw error
}
}
4. Call Initialization in Your App ​
Ensure your initialization function is called when your Raclette application starts:
// In your main application file or plugin hook
export default async function (fastify: PluginFastifyInstance) {
// ... other initialization code
// Initialize CLI commands
await initializeCliCommands(fastify)
// ... rest of your application setup
}
Once installed and configured, the plugin will be available at the /plugin/raclette/cli-connector-plugin/
endpoint prefix. See API Endpoints for more details.
Script Registration ​
Scripts are registered programmatically using import functions. This approach provides flexibility and allows for dynamic configuration with proper type safety.
Registration Functions ​
registerScriptByConfig(cmd: string, config: ScriptConfigInput)
​
Register a script using a configuration object directly in your code.
import { registerScriptByConfig } from "@raclettejs/cli-connector-plugin"
registerScriptByConfig("list-files", {
exe: "/bin/ls",
args: ["-la", { path: "/app/data" }],
opts: {
cwd: "/app/data",
},
})
registerScriptByPath(cmd: string, path: string, hooksPath?: string)
​
Register a script using an external YAML or JSON configuration file. Make sure to provide an absolute path to the file(s).
import { registerScriptByPath } from "@raclettejs/cli-connector-plugin"
import path from "path"
await registerScriptByPath(
"git-status",
path.join(import.meta.dirname, "./configs/git.yaml"),
path.join(import.meta.dirname, "./hooks/git.js")
)
Note that the command-alias (cmd) does not get prefixed. So if you use this across several plugins, make sure they are unique. This might get updated in the future.
Configuration Structure ​
type ScriptConfigInput = {
exe: string // Path to executable, preferrably an absolute path
args?: ArgvInputTemplate[] // Command line arguments as template
opts?: ScriptOptions // Process spawn options
hooks?: IOHooks // I/O processing hooks (recommended for jobs)
}
Argument Templates ​
Arguments can be static strings, runtime placeholders, or combinations with validation:
args: [
"-v", // Static argument
{}, // Runtime placeholder (= variable)
["--user=", {}], // Concatenated argument (your variable will be added after the '=')
{ allow: "^[a-zA-Z0-9]+$" }, // Validated placeholder
{ path: "/app/uploads" }, // Path-restricted placeholder
]
Validation Options ​
allow
: RegExp pattern that values must matchdeny
: RegExp pattern that values must not matchpath
: Base directory to restrict file operations
Hooks for I/O Processing ​
Hooks transform input and output data. For long-running processes (jobs), it is recommended to include output hooks to convert Buffer objects to strings (unless explicitly wanted otherwise):
{
exe: "/usr/bin/command",
hooks: {
stdout: (chunk: Buffer | string): string => {
return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk
},
stderr: (chunk: Buffer | string): string => {
return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk
}
}
}
API Endpoints ​
Single-Run Processes: /script/:cmd
​
Endpoint: POST /script/:cmd
Request Body: { args?: string[], input?: string }
Response: { stdout: string, stderr: string, status: number|string }
For short-running programs (default timeout: 2 seconds). Executes synchronously and returns complete results.
POST /plugin/raclette/cli-connector-plugin/script/list-files
{
"args": ["/home/user"]
}
Long-Running Processes: /run/:cmd
​
Endpoint: POST /run/:cmd
Request Body: { args?: string[] }
Response: 123456
(job ID)
Starts a process and returns a job ID for interaction. Use this for commands that take longer than a few seconds.
POST /plugin/raclette/cli-connector-plugin/run/backup-database
{
"args": ["full", "production"]
}
Status Codes:
201
: Job created successfully404
: Script not found409
: Too many jobs running500
: Server error
Job Management: /job/:id
​
Endpoint: POST /job/:id
Request Body: { input?: string, signal?: string }
Response: { done?: number|string, stdin: string[], stdout: string[], stderr: string[] }
Interact with running processes and retrieve output incrementally. stdout
and stderr
can be transformed via hooks.
Status Codes:
200
: Process completed202
: Process still running400
: Invalid signal404
: Job not found500
: Server error
Job Listing: /jobs
​
Endpoint: GET /jobs
Response: [{ id: string, cmd: string, argv: string[], done?: number|string }, ...]
List all known processes. Restricted to admin users.
Script Listing: /scripts
​
Endpoint: GET /scripts
Response: ["script1", "script2", ...]
List all registered script names.
Configuration Examples ​
Example 1: File Listing ​
Configuration:
registerScriptByConfig("safe-ls", {
exe: "/bin/ls",
args: ["-la", { path: "/app/public" }],
opts: {
cwd: "/app/public",
},
})
Generated Command:
# When called with args: ["documents"]
/bin/ls -la /app/public/documents
API Call:
POST /script/safe-ls
{
"args": ["documents"]
}
Example 2: Git Status Check ​
Configuration:
registerScriptByConfig("git-status", {
exe: "/usr/bin/git",
args: ["status", "--porcelain"],
opts: {
cwd: "/app/repository",
env: {
GIT_PAGER: "",
},
},
})
Generated Command:
# Always runs the same command
/usr/bin/git status --porcelain
API Call:
POST /script/git-status
{}
Example 3: Service Control ​
Configuration:
registerScriptByConfig("service-control", {
exe: "/usr/bin/systemctl",
args: [
{ allow: "^(start|stop|restart|status)$" }, // Only allow these actions
{ allow: "^[a-zA-Z0-9_-]+$" }, // Service name validation
],
})
Generated Commands:
# When called with args: ["restart", "nginx"]
/usr/bin/systemctl restart nginx
# When called with args: ["status", "mysql"]
/usr/bin/systemctl status mysql
API Call:
POST /script/service-control
{
"args": ["restart", "nginx"]
}
Example 4: Database Backup (Long-Running) ​
Configuration:
registerScriptByConfig("db-backup", {
exe: "/usr/bin/mysqldump",
args: [
"--single-transaction",
"myapp",
{ allow: "^[a-zA-Z0-9_]+$" }, // Table name
],
hooks: {
stdout: (chunk: Buffer): string => chunk.toString("utf8"),
stderr: (chunk: Buffer): string => chunk.toString("utf8"),
},
})
Generated Command:
# When called with args: ["users"]
/usr/bin/mysqldump --single-transaction myapp users
API Call:
POST /run/db-backup
{
"args": ["users"]
}
Example 5: File Operations with Concatenation ​
Configuration:
registerScriptByConfig("chmod-files", {
exe: "/bin/chmod",
args: [
["u+", { allow: "^[rwx]+$" }], // Concatenated permission
{ path: "/app/uploads" }, // Path-restricted file
],
opts: {
cwd: "/app/uploads",
},
})
Generated Command:
# When called with args: ["rw", "document.txt"]
/bin/chmod u+rw /app/uploads/document.txt
API Call:
POST /script/chmod-files
{
"args": ["rw", "document.txt"]
}
Security Considerations ​
⚠️ IMPORTANT SECURITY NOTE
Exposing command-line tools through a web interface requires careful consideration. This plugin provides the tools to do so securely, but administrators must:
- Thoroughly understand both the system and the programs being exposed
- Strictly limit use cases and parameters
- Properly sanitize input and output
The CLI Connector addresses three primary security challenges:
Command Shell Vulnerabilities: Uses
argv
-based process execution instead of command shells to prevent injection attacksCommand Tool Power: Provides configuration templates to restrict command lines to safe, use-case specific subsets
I/O Sanitization: Offers a hooks API for custom input/output processing and validation
Best Security Practices ​
- Use Principle of Least Privilege: Only expose minimum required functionality
- Validate All Inputs: Use
allow
/deny
patterns to restrict user inputs - Restrict File Access: Use
path
validation to limit file system access - Sanitize Outputs: Remove sensitive information from command outputs
- Create Specific Commands: Avoid general-purpose commands that could be misused
User Roles ​
Role | Description |
---|---|
User | Uses the Raclette GUI without necessarily knowing they're accessing command-line tools |
User Admin | Uses the web interface with elevated privileges to monitor and manage processes |
Server Admin | Responsible for configuring which tools are exposed and how they're restricted |
JavaScript Programmer | Customizes input forms and implements I/O sanitization hooks when needed |
Technical Implementation Details ​
This section provides technical details about how the CLI Connector plugin works internally. Most users can skip this section.
Process Execution Architecture ​
The plugin uses Node.js child process APIs to execute commands safely:
- Single-run processes (
/script/:cmd
): Useschild_process.spawnSync
with configurable timeouts - Long-running processes (
/run/:cmd
): Useschild_process.spawn
with streaming I/O
Script Template Processing ​
Script configurations undergo several processing steps during registration:
- Argument Template Parsing: Static strings, placeholders, and validation rules are identified and processed
- RegExp Compilation: String patterns in
allow
anddeny
validators are compiled to RegExp objects - Argument Counting: The system tracks how many runtime arguments each template expects
- Hook Loading: External or inline hooks are attached to the processed configuration
Configuration Object Structure ​
interface ScriptConfig {
exe: string
args: ProcessedArgument[]
argCount: number // Calculated during processing
opts?: SpawnOptions
hooks?: {
stdin?: (input: string) => string
stdout?: (output: string | Buffer) => string
stderr?: (error: string | Buffer) => string
}
}
type ProcessedArgument =
| string // Static argument
| Validator // Single placeholder with validation
| (string | Validator)[] // Concatenated argument parts
interface Validator {
allow?: RegExp // Compiled from string pattern
deny?: RegExp // Compiled from string pattern
path?: string // Base directory restriction
}
Argument Resolution Process ​
When a request is made to execute a script:
- Template Retrieval: The script configuration is looked up by command name
- Argument Validation: Each user-provided argument is validated against its template rules
- Path Resolution: File paths are resolved and restricted to allowed directories
- Command Assembly: Static strings and validated placeholders are combined into the final command
- Process Execution: The command is executed with the specified spawn options
Hook Execution Context ​
For long-running processes, hooks have access to a context object:
interface HookContext {
stdout: string[] // Array of all previous stdout chunks
stderr: string[] // Array of all previous stderr chunks
stdin: string[] // Array of all previous stdin chunks
}
Hooks can modify this context, and changes are permanent for the duration of the process.
Job Management System ​
Long-running processes are tracked in an internal job registry:
- Job IDs: Generated using timestamp + random suffix
- Process Tracking: Jobs store process handles, I/O streams, and metadata
- Cleanup: Completed jobs are cleaned up after a configurable timeout
- Admin Access: Job listing endpoint provides visibility into running processes
Error Handling and Status Codes ​
The plugin uses specific HTTP status codes to communicate process states:
200
: Successful completion (single-run) or successful job interaction202
: Process still running (job management)400
: Invalid request or argument validation failure404
: Script or job not found500
: Process execution error or system failure
Buffer vs String Handling ​
By default, process output is handled as Buffer objects. For web interfaces, string conversion is usually required:
- Automatic Conversion: Only occurs when hooks are present and return strings
- Manual Conversion: Hooks must explicitly convert Buffer to string using
toString()
- Encoding: Default encoding is 'utf8', but can be configured in spawn options
This technical implementation ensures secure, efficient, and flexible command execution while maintaining clear separation between user-facing functionality and internal mechanics.