Skip to content

Commit

Permalink
v1 - sandbox node (#61)
Browse files Browse the repository at this point in the history
Co-authored-by: yatchiya <yarab@gmail.com>
  • Loading branch information
YatchiYa and yatchiya committed Sep 11, 2024
1 parent e78473d commit 2496923
Show file tree
Hide file tree
Showing 26 changed files with 865 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/sandbox-docker/example/nodejs-project/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: '3'
services:
app:
build: .
environment:
- INPUT_VALUE=${INPUT_VALUE}
volumes:
- ./output:/app/output
9 changes: 9 additions & 0 deletions packages/sandbox-docker/example/nodejs-project/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const fs = require('fs');

const inputValue = process.env.INPUT_VALUE || 'Default Value';
console.log(`Received input: ${inputValue}`);

const output = `Processed: ${inputValue.toUpperCase()}`;
fs.writeFileSync('/app/output/output.txt', output);

console.log('Processing complete');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Processed: HELLO, FROM DOCKER!
5 changes: 5 additions & 0 deletions packages/sandbox-docker/example/nodejs-project/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "your-project-name",
"version": "1.0.0",
"dependencies": {}
}
7 changes: 7 additions & 0 deletions packages/sandbox-docker/example/python-flask/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8081
CMD ["python", "app.py"]
12 changes: 12 additions & 0 deletions packages/sandbox-docker/example/python-flask/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
input_value = os.environ.get('INPUT_VALUE', 'Default Value')
return f"Hello docker file from Flask! Input: {input_value}"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8081)
10 changes: 10 additions & 0 deletions packages/sandbox-docker/example/python-flask/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '3'
services:
web:
build: .
ports:
- "8081:8081"
environment:
- INPUT_VALUE=${INPUT_VALUE}
volumes:
- ./output:/app/output
2 changes: 2 additions & 0 deletions packages/sandbox-docker/example/python-flask/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask==2.0.1
Werkzeug==2.0.1
9 changes: 9 additions & 0 deletions packages/sandbox-docker/example/python-project/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/output && chown -R 1000:1000 /app/output
RUN useradd -m -u 1000 appuser
USER appuser
CMD ["python", "app.py"]
10 changes: 10 additions & 0 deletions packages/sandbox-docker/example/python-project/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import os

input_value = os.environ.get('INPUT_VALUE', 'default')
result = f"Processed input: {input_value.upper()}"

os.makedirs('/app/output', exist_ok=True)
with open('/app/output/output.txt', 'w') as f:
f.write(result)

print(result)
10 changes: 10 additions & 0 deletions packages/sandbox-docker/example/python-project/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '3'
services:
web:
build: .
ports:
- "5001:5000"
environment:
- INPUT_VALUE=${INPUT_VALUE}
volumes:
- ./output:/app/output
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Processed input: HELLO DUDE !
Empty file.
55 changes: 55 additions & 0 deletions packages/sandbox-docker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "secure-nodejs-code-enclave",
"version": "1.0.0",
"description": "A secure execution environment for untrusted JavaScript/TypeScript code",
"main": "dist/test.js",
"scripts": {
"build": "tsc",
"start": "node dist/test.js",
"start_example": "node dist/test.js",
"test": "jest --detectOpenHandles",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"start:server": "node dist/test.js --server"
},
"dependencies": {
"@types/js-yaml": "^4.0.9",
"@vitalets/google-translate-api": "^9.2.0",
"axios": "^1.7.5",
"body-parser": "^1.20.2",
"dotenv": "^16.4.5",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
"express": "^4.19.2",
"js-yaml": "^4.1.0",
"natural": "^8.0.1",
"openai": "^4.56.1",
"qllm-lib": "^3.2.1",
"uuid": "^8.3.2",
"vm2": "^3.9.11",
"yaml": "^2.5.0",
"yargs": "^17.5.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/body-parser": "^1.19.5",
"@types/escodegen": "^0.0.10",
"@types/esprima": "^4.0.6",
"@types/express": "^4.17.21",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.106",
"@types/uuid": "^8.3.4",
"@types/yargs": "^17.0.33",
"jest": "^27.5.1",
"ts-jest": "^27.1.5",
"typescript": "^4.9.5"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": [
"**/__tests__/**/*.ts",
"**/?(*.)+(spec|test).ts"
]
}
}
110 changes: 110 additions & 0 deletions packages/sandbox-docker/src/core/enclave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { v4 as uuidv4 } from 'uuid';
import { PackageManager } from './package-manager';
import { FileManager } from './file-manager';
import { VirtualFileSystem } from './virtual-fs';
import { ResourceLimiter } from '../security/resource-limiter';
import { Logger } from '../utils/logger';
import { ErrorHandler } from '../utils/error-handler';
import { EnclaveConfig, FileInput, EnclaveStatus } from '../types';
import { DockerSandbox } from '../security/docker-sandbox';

/**
* Enclave class represents a secure execution environment for Node.js projects.
*/
export class Enclave {
private readonly id: string;
private readonly tempDir: string;
private readonly packageManager: PackageManager;
private readonly fileManager: FileManager;
private readonly virtualFs: VirtualFileSystem;
private readonly resourceLimiter: ResourceLimiter;
private readonly logger: Logger;
private readonly errorHandler: ErrorHandler;
private readonly dockerSandbox: DockerSandbox;
private status: EnclaveStatus;

/**
* Creates a new Enclave instance.
* @param config - Configuration options for the enclave.
*/
constructor(config: EnclaveConfig) {
this.id = uuidv4();
this.tempDir = `/tmp/secure-nodejs-enclave-${this.id}`;
this.packageManager = new PackageManager(this.tempDir, config.cacheDir);
this.fileManager = new FileManager(this.tempDir);
this.virtualFs = new VirtualFileSystem(this.tempDir);
this.resourceLimiter = new ResourceLimiter(config.resourceLimits);
this.logger = new Logger(config.loggerConfig);
this.errorHandler = new ErrorHandler();
this.dockerSandbox = new DockerSandbox(config.dockerConfig);
this.status = 'initialized';
}

/**
* Executes a Docker container with the specified project and parameters.
* @param projectPath - Path to the project to be executed.
* @param params - Key-value pairs of parameters for the execution.
* @returns An object containing the output and logs from the execution.
* @throws Error if the execution fails.
*/
async executeDocker(projectPath: string, params: Record<string, string>): Promise<{ output: string; logs: string }> {
this.status = 'executing';
this.logger.info(`Starting Docker execution for project: ${projectPath}`);
this.logger.debug(`Params: ${JSON.stringify(params)}`);

try {
const result = await this.dockerSandbox.run(projectPath, params);
this.logger.info('Docker execution completed successfully');
this.status = 'completed';
return result;
} catch (error) {
this.status = 'error';
this.logger.error('Docker execution failed', error as Error);
throw this.errorHandler.handleError(error as Error);
}
}

/**
* Prepares the enclave by writing files and installing packages.
* @param files - Array of file inputs to be written.
* @param packages - Array of package names to be installed.
* @throws Error if preparation fails.
*/
async prepare(files: FileInput[], packages: string[]): Promise<void> {
this.status = 'preparing';
try {
await Promise.all([
this.fileManager.writeFiles(files)
]);
this.status = 'prepared';
} catch (error) {
this.status = 'error';
throw this.errorHandler.handleError(error as Error);
}
}

/**
* Cleans up resources used by the enclave.
*/
async cleanup(): Promise<void> {
this.status = 'cleaning';
try {
await Promise.all([
this.fileManager.cleanup(),
this.packageManager.cleanup()
]);
this.status = 'cleaned';
} catch (error) {
this.status = 'error';
this.logger.error('Cleanup failed', error as Error);
}
}

/**
* Gets the current status of the enclave.
* @returns The current status of the enclave.
*/
getStatus(): EnclaveStatus {
return this.status;
}
}
38 changes: 38 additions & 0 deletions packages/sandbox-docker/src/core/file-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { FileInput } from '../types';

export class FileManager {
constructor(private tempDir: string) {}

async writeFiles(files: FileInput[]): Promise<void> {
for (const file of files) {
if (!this.validateFileName(file.name)) {
throw new Error(`Invalid file name: ${file.name}`);
}
const filePath = path.join(this.tempDir, file.name);
await fs.writeFile(filePath, file.content);
}
}

async readFile(fileName: string): Promise<string> {
if (!this.validateFileName(fileName)) {
throw new Error(`Invalid file name: ${fileName}`);
}
const filePath = path.join(this.tempDir, fileName);
return fs.readFile(filePath, 'utf-8');
}

validateFileName(fileName: string): boolean {
const normalizedPath = path.normalize(fileName);
return !normalizedPath.startsWith('..') && !path.isAbsolute(normalizedPath);
}

async listFiles(): Promise<string[]> {
return fs.readdir(this.tempDir);
}

async cleanup(): Promise<void> {
await fs.rm(this.tempDir, { recursive: true, force: true });
}
}
Loading

0 comments on commit 2496923

Please sign in to comment.