Skip to main content
Glama
FileController.ts11.8 kB
import { Request, Response } from 'express'; import { FileContent } from '../models'; import fs from 'fs'; import path from 'path'; import { PolicyEvaluationContext } from '../models/Policy'; import { PolicyEngine } from '../services/PolicyEngine'; import { ValidationError, NotFoundError, ForbiddenError, InternalError } from '../utils/AppError'; import { Logger } from '../utils/Logger'; export class FileController { private workspacePath: string; private policyEngine: PolicyEngine; private logger: Logger; constructor(workspacePath: string = './workspace') { this.workspacePath = path.resolve(workspacePath); // Ensure workspace directory exists if (!fs.existsSync(this.workspacePath)) { fs.mkdirSync(this.workspacePath, { recursive: true }); } // Initialize policy engine this.policyEngine = new PolicyEngine(); // Add default policy for development this.policyEngine.addPolicy(PolicyEngine.createDefaultPolicy()); // Initialize logger this.logger = new Logger('./logs/file-controller.log', 'DEBUG', 'FileController'); } /** * Get file content by path * @param req Request with file path * @param res Response with file content */ public getFile(req: Request, res: Response): void { const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2); this.logger.info('getFile request received', { filePath: req.params.path }, requestId); try { const filePath = req.params.path; // Validate file path if (!filePath) { this.logger.warn('getFile validation failed: File path is required', { filePath }, requestId); throw new ValidationError('File path is required'); } // Sanitize file path to prevent directory traversal if (!this.isValidPath(filePath)) { this.logger.warn('getFile validation failed: Invalid file path', { filePath }, requestId); throw new ValidationError('Invalid file path'); } const fullPath = path.resolve(path.join(this.workspacePath, filePath)); // Create policy evaluation context const context: PolicyEvaluationContext = { sessionId: req.headers['x-session-id'] as string || 'unknown', ipAddress: req.ip || 'unknown', resource: filePath, action: 'fileAccess', timestamp: new Date() }; // Check policy if (!this.policyEngine.isActionAllowed(context)) { this.logger.warn('getFile policy check failed: Access denied by policy', { filePath, context }, requestId); throw new ForbiddenError('Access denied by policy'); } // Security check: ensure file is within workspace if (!fullPath.startsWith(this.workspacePath)) { this.logger.warn('getFile security check failed: Access denied', { filePath, fullPath, workspacePath: this.workspacePath }, requestId); throw new ForbiddenError('Access denied'); } if (!fs.existsSync(fullPath)) { this.logger.warn('getFile not found: File not found', { filePath, fullPath }, requestId); throw new NotFoundError('File not found'); } // Check if it's a directory const stats = fs.statSync(fullPath); if (stats.isDirectory()) { this.logger.warn('getFile validation failed: Path is a directory, not a file', { filePath, fullPath }, requestId); throw new ValidationError('Path is a directory, not a file'); } const content = fs.readFileSync(fullPath, 'utf8'); const fileContent: FileContent = { path: filePath, content }; this.logger.info('getFile successful', { filePath, fullPath }, requestId); res.status(200).json(fileContent); } catch (error: any) { if (error instanceof ValidationError) { this.logger.warn('getFile validation error', { error: error.message }, requestId); res.status(400).json({ error: error.message }); } else if (error instanceof NotFoundError) { this.logger.warn('getFile not found error', { error: error.message }, requestId); res.status(404).json({ error: error.message }); } else if (error instanceof ForbiddenError) { this.logger.warn('getFile forbidden error', { error: error.message }, requestId); res.status(403).json({ error: error.message }); } else { this.logger.error('getFile unexpected error', { error: error.message, stack: error.stack }, requestId); res.status(500).json({ error: 'Failed to read file' }); } } } /** * Write file content * @param req Request with file content * @param res Response with status */ public writeFile(req: Request, res: Response): void { const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2); this.logger.info('writeFile request received', { fileContent: req.body }, requestId); try { const fileContent: FileContent = req.body; // Validate input if (!fileContent.path) { this.logger.warn('writeFile validation failed: File path is required', { fileContent }, requestId); throw new ValidationError('File path is required'); } if (fileContent.content === undefined) { this.logger.warn('writeFile validation failed: File content is required', { fileContent }, requestId); throw new ValidationError('File content is required'); } // Sanitize file path to prevent directory traversal if (!this.isValidPath(fileContent.path)) { this.logger.warn('writeFile validation failed: Invalid file path', { fileContent }, requestId); throw new ValidationError('Invalid file path'); } // Create policy evaluation context const context: PolicyEvaluationContext = { sessionId: req.headers['x-session-id'] as string || 'unknown', ipAddress: req.ip || 'unknown', resource: fileContent.path, action: 'fileAccess', timestamp: new Date() }; // Check policy if (!this.policyEngine.isActionAllowed(context)) { this.logger.warn('writeFile policy check failed: Access denied by policy', { fileContent, context }, requestId); throw new ForbiddenError('Access denied by policy'); } const fullPath = path.resolve(path.join(this.workspacePath, fileContent.path)); // Security check: ensure file is within workspace if (!fullPath.startsWith(this.workspacePath)) { this.logger.warn('writeFile security check failed: Access denied', { fileContent, fullPath, workspacePath: this.workspacePath }, requestId); throw new ForbiddenError('Access denied'); } // Ensure directory exists const dirPath = path.dirname(fullPath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync(fullPath, fileContent.content, 'utf8'); this.logger.info('writeFile successful', { fileContent, fullPath }, requestId); res.status(200).json({ success: true, message: 'File written successfully' }); } catch (error: any) { if (error instanceof ValidationError) { this.logger.warn('writeFile validation error', { error: error.message }, requestId); res.status(400).json({ error: error.message }); } else if (error instanceof ForbiddenError) { this.logger.warn('writeFile forbidden error', { error: error.message }, requestId); res.status(403).json({ error: error.message }); } else { this.logger.error('writeFile unexpected error', { error: error.message, stack: error.stack }, requestId); res.status(500).json({ error: 'Failed to write file' }); } } } /** * List files in a directory * @param req Request with directory path * @param res Response with file list */ public listFiles(req: Request, res: Response): void { const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2); this.logger.info('listFiles request received', { dirPath: req.params.path }, requestId); try { // Handle root directory case const dirPath = req.params.path || ''; // Validate directory path if (dirPath && !this.isValidPath(dirPath)) { this.logger.warn('listFiles validation failed: Invalid directory path', { dirPath }, requestId); throw new ValidationError('Invalid directory path'); } // Create policy evaluation context const context: PolicyEvaluationContext = { sessionId: req.headers['x-session-id'] as string || 'unknown', ipAddress: req.ip || 'unknown', resource: dirPath, action: 'fileAccess', timestamp: new Date() }; // Check policy if (!this.policyEngine.isActionAllowed(context)) { this.logger.warn('listFiles policy check failed: Access denied by policy', { dirPath, context }, requestId); throw new ForbiddenError('Access denied by policy'); } const fullPath = dirPath ? path.resolve(path.join(this.workspacePath, dirPath)) : this.workspacePath; // Security check: ensure path is within workspace if (!fullPath.startsWith(this.workspacePath)) { this.logger.warn('listFiles security check failed: Access denied', { dirPath, fullPath, workspacePath: this.workspacePath }, requestId); throw new ForbiddenError('Access denied'); } if (!fs.existsSync(fullPath)) { this.logger.warn('listFiles not found: Directory not found', { dirPath, fullPath }, requestId); throw new NotFoundError('Directory not found'); } // Check if it's a file const stats = fs.statSync(fullPath); if (stats.isFile()) { this.logger.warn('listFiles validation failed: Path is a file, not a directory', { dirPath, fullPath }, requestId); throw new ValidationError('Path is a file, not a directory'); } const files = fs.readdirSync(fullPath); this.logger.info('listFiles successful', { dirPath, fullPath, fileCount: files.length }, requestId); res.status(200).json({ path: dirPath, files }); } catch (error: any) { if (error instanceof ValidationError) { this.logger.warn('listFiles validation error', { error: error.message }, requestId); res.status(400).json({ error: error.message }); } else if (error instanceof NotFoundError) { this.logger.warn('listFiles not found error', { error: error.message }, requestId); res.status(404).json({ error: error.message }); } else if (error instanceof ForbiddenError) { this.logger.warn('listFiles forbidden error', { error: error.message }, requestId); res.status(403).json({ error: error.message }); } else { this.logger.error('listFiles unexpected error', { error: error.message, stack: error.stack }, requestId); res.status(500).json({ error: 'Failed to list files' }); } } } /** * Validate file path to prevent directory traversal attacks * @param filePath File path to validate * @returns True if valid, false otherwise */ private isValidPath(filePath: string): boolean { // Check for directory traversal attempts if (filePath.includes('../') || filePath.includes('..\\')) { return false; } // Check for absolute paths if (path.isAbsolute(filePath)) { return false; } // Check for null bytes if (filePath.includes('\0')) { return false; } return true; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Nom-nom-hub/fullstack-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server