UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

583 lines (582 loc) 24.3 kB
/** * ARCHITECTURE DOCUMENTATION: HttpServer - Web Server and Real-Time Notification Hub * =================================================================================== * * HttpServer provides the HTTP/HTTPS server for Minecraft Creator Tools and serves as * a central hub for real-time client notifications via WebSocket connections. It exposes * REST APIs for server management and proxies events from DedicatedServer instances. * * ## Core Responsibilities * * 1. **HTTP Server**: Serves static files, API endpoints, and the web application * 2. **WebSocket Notifications**: Broadcasts real-time events to connected clients * 3. **Storage Watching**: Monitors NodeStorage for file changes and broadcasts updates * 4. **Authentication**: Manages session tokens and permission levels * 5. **MCP Server Integration**: Hosts Model Context Protocol server * 6. **Server Management API**: REST endpoints for controlling DedicatedServer instances * * ## API Architecture * * The HTTP server exposes REST endpoints for remote server management: * * ``` * /api/auth - Authentication with passcode * /api/<slot>/start - Start a DedicatedServer on the specified slot * /api/<slot>/stop - Stop the server on the specified slot * /api/<slot>/status - Get server status and recent log messages * /api/<slot>/command - Send a slash command to the server * /api/<slot>/deploy - Deploy add-on content to the server * /api/content/<path> - Access files when in view/edit mode * /api/validate - Validate Minecraft content * /api/shutdown - Graceful shutdown (view mode only) * /api/acceptEula - Accept Minecraft EULA (admin only, enables BDS features) * /api/eulaStatus - Check if EULA has been accepted * /api/commands - List available ToolCommands * /api/commands/<cmd> - Execute a ToolCommand (POST with args/flags JSON body) * ``` * * ## Real-Time Sync Architecture * * HttpServer sits between server-side file changes and client-side updates: * * ``` * NodeStorage (fs.watch) ──→ HttpServer ──→ WebSocket ──→ HttpStorage (client) * │ │ │ * └── IStorageChangeEvent ────┘ │ * │ │ * IServerNotification ─────────────────→ onFileUpdated event * │ * MCWorld ─→ WorldView * ``` * * ## WebSocket Event Types * * The WebSocket connection broadcasts various event types to clients: * * | Event Name | Description | * |--------------------|-------------------------------------------------| * | statusUpdate | Server status changed (starting, started, etc.)| * | playerJoined | Player connected to the Minecraft server | * | playerLeft | Player disconnected from the server | * | playerMoved | Player position changed (from position polling) | * | storage/change | File in watched storage was modified | * | debugConnected | Script debugger connection established | * | debugStats | Profiling statistics from script debugger | * | gameEvent | Generic game event from server | * | serverShutdown | MCT server is shutting down (sent before close) | * * ## Storage Watcher Integration * * The server maintains watchers for NodeStorage instances and converts storage events * to WebSocket notifications: * * - **startWatchingStorage()**: Registers a storage for watching with a unique slot ID * - **stopWatchingStorage()**: Stops watching a specific storage * - **stopAllStorageWatchers()**: Cleanup when server stops * - **_handleStorageChange()**: Converts IStorageChangeEvent to IServerNotification * * ## Authentication & Permission Levels * * Four permission levels control access to different features: * * | Level | Access | * |--------------------|-------------------------------------------------| * | displayReadOnly | View server status and logs | * | fullReadOnly | Above + file browsing | * | updateState | Above + start/stop servers, deploy content | * | admin | Full access including shutdown | * * Passcodes are set via command line and validated using encrypted tokens. * * ## SSL/TLS Support (Experimental) * * HttpServer supports experimental HTTPS via command-line configuration: * - Certificate and key files specified at startup * - Not persisted to disk for security * - Optional HTTPS-only mode * * ## Integration with ServerManager * * HttpServer receives events from ServerManager and converts them to notifications: * * ``` * DedicatedServer ──► ServerManager ──► HttpServer ──► WebSocket Clients * │ │ │ * │ │ bubbleServerStarted * │ │ │ * │ │ └─► notify({ eventName: 'statusUpdate', ... }) * │ │ * │ │ bubblePlayerConnected * │ │ │ * │ │ └─► notify({ eventName: 'playerJoined', ... }) * ``` * * ## Related Files * * - ServerManager.ts: Creates HttpServer and forwards server events * - IServerNotification.ts: Notification message types * - NodeStorage.ts: Server-side file watching with fs.watch() * - HttpStorage.ts: Client-side notification receiver * - IStorageWatcher.ts: Interface definitions for watcher system * - MinecraftMcpServer.ts: MCP server for AI tool integration * * ## Key Methods * * - init(): Initialize HTTP/HTTPS server and WebSocket * - stop(): Cleanup resources including storage watchers * - notify(): Broadcast notification to all connected WebSocket clients * - notifyStatusUpdate(): Send server status change to a specific slot * - sendNotificationToSlot(): Send notification to clients subscribed to a slot * - startWatchingStorage(): Begin monitoring a NodeStorage instance * - processRequest(): Main HTTP request handler and router */ import * as http from "http"; import ServerManager from "./ServerManager"; import LocalEnvironment from "./LocalEnvironment"; import NodeStorage from "./NodeStorage"; import { ServerPermissionLevel } from "./IAuthenticationToken"; import CreatorTools from "../app/CreatorTools"; import { ISlotConfig } from "../app/CreatorToolsAuthentication"; import { DedicatedServerStatus, OutputLine } from "./DedicatedServer"; import IStorage from "../storage/IStorage"; import ISslConfig from "./ISslConfig"; import { IServerNotification, IServerNotificationBody } from "./IServerNotification"; export interface CartoServerAuthenticationResponse { token?: string; iv?: string; authTag?: string; permissionLevel: ServerPermissionLevel; serverStatus: CartoServerStatusResponse[]; /** Whether the Minecraft EULA has been accepted (required for BDS features) */ eulaAccepted?: boolean; } export interface CartoServerStatusResponse { id: number; status?: DedicatedServerStatus; time: number; /** Recent log messages from the server */ recentMessages?: OutputLine[]; /** Slot configuration - included in initial auth response */ slotConfig?: ISlotConfig; /** World ID currently associated with this slot */ worldId?: string; } export default class HttpServer { host: string; port: number; creatorTools: CreatorTools | undefined; headers: { "Access-Control-Allow-Origin": string; "Access-Control-Allow-Methods": string; "Access-Control-Max-Age": number; "Access-Control-Allow-Headers": string; "Access-Control-Allow-Credentials": string; }; private _webStorage; private _resStorage; private _dataStorage; private _distStorage; private _schemasStorage; private _formsStorage; private _esbuildWasmStorage; private _serverManager; private _localEnvironment; private _algorithm; private _httpsServer; private _httpServer; private _mcpServer; private _pwdHash; private _isListeningMetaFlag; private _readyResolvers; /** * Returns true if the HTTP server is actually listening and accepting connections. * This checks the underlying server state, not just the flag. */ get isListening(): boolean; /** * Returns a promise that resolves when the server is ready to accept connections. * If the server is already listening, resolves immediately. * @param timeoutMs Optional timeout in milliseconds. If provided and the server * doesn't become ready in time, the promise rejects with a timeout error. */ waitForReady(timeoutMs?: number): Promise<void>; /** * Called when the server starts listening to mark it as ready and resolve any pending waiters. */ private _markAsListening; private _sslConfig?; get sslConfig(): ISslConfig | undefined; set sslConfig(config: ISslConfig | undefined); private _tempContent; private _contentStorage; private _contentPath; private _isViewMode; private _isEditMode; private _mcpRequireAuth; private _mcpServerInitPromise; private _wsServer; private _wsClients; private _storageWatchers; private _slotStorages; private static readonly BLOCKED_FILE_NAMES; private static readonly SAFE_EXTENSIONS; /** Whether local res/ storage includes the vanilla serve folder with PNG textures */ private _hasLocalVanillaServe; constructor(localEnv: LocalEnvironment, serverManager: ServerManager); init(): void; /** * Initialize HTTPS server with experimental SSL configuration. * SSL config is passed via command line arguments - nothing is persisted. */ private initHttps; /** * Build HTTPS server options from experimental SSL configuration. */ private buildHttpsOptions; /** * Initialize WebSocket server for pushing notifications to clients. * The WebSocket server shares the HTTP server and handles upgrade requests to /ws/notifications. */ private initWebSocketServer; /** * Authenticate a WebSocket upgrade request. * Returns the permission level if authenticated, undefined otherwise. * * Requires a valid auth token via query string (?token=...) or mctauth cookie. * Localhost connections are NOT exempted — all WebSocket clients must authenticate. */ private authenticateWebSocketRequest; /** * Handle a new WebSocket connection. */ private handleWebSocketConnection; /** * Handle incoming WebSocket message (subscription requests). */ private handleWebSocketMessage; /** * Validate an encrypted auth token and return the permission level. */ private validateToken; /** * Broadcast a notification to all subscribed WebSocket clients. * @param notification The notification to broadcast */ broadcastNotification(notification: IServerNotification): void; /** * Create and broadcast a notification. * Helper method for common notification patterns. */ notify(body: IServerNotificationBody): void; /** * Notify clients of a file change in world content. */ notifyFileChange(eventName: "fileChanged" | "fileAdded" | "fileRemoved", slot: number, category: "behavior_packs" | "resource_packs" | "world", path: string): void; /** * Notify clients of a server state change. */ notifyServerState(slot: number, state: "starting" | "started" | "stopping" | "stopped" | "error", message?: string): void; /** * Notify clients of a player movement. */ notifyPlayerMoved(slot: number, playerName: string, position: { x: number; y: number; z: number; }, rotation?: { yaw: number; pitch: number; }, dimension?: string): void; /** * Forward a game event from Minecraft to WebSocket clients. */ notifyGameEvent(slot: number, minecraftEventName: string, data: object): void; /** * Notify clients of a full status update for a server slot. * This replaces the need for polling /api/{slot}/status/ * * @param slot The server slot number * @param status The current DedicatedServerStatus value * @param recentMessages Recent messages from the server * @param title Optional server title */ notifyStatusUpdate(slot: number, status: number, recentMessages?: Array<{ message: string; received: number; type?: number; }>, title?: string): void; /** * Start watching a NodeStorage for file changes and broadcast them via WebSocket. * This enables real-time synchronization of file changes to connected clients. * * @param storage The NodeStorage to watch * @param slot The server slot number this storage is associated with * @param category The category of content (behavior_packs, resource_packs, world) */ startWatchingStorage(storage: NodeStorage, slot: number, category: "behavior_packs" | "resource_packs" | "world"): void; /** * Stop watching a specific storage. */ stopWatchingStorage(storage: NodeStorage): void; /** * Stop watching all storages. */ stopAllStorageWatchers(): void; /** * Handle a storage change event and broadcast it to WebSocket clients. */ private _handleStorageChange; stop(reason?: string): Promise<void>; private _salt; getPasswordHash(): Buffer<ArrayBufferLike>; getSalt(): Buffer<ArrayBufferLike>; /** * Generates a generic fingerprint hash for token binding. * Uses partial IP and browser family to balance security with usability. * * Note: This is intentionally "soft" binding - we only use partial IP * to avoid breaking sessions for mobile users while still providing * some protection against token theft across different networks. */ generateFingerprint(userAgent?: string, ipAddress?: string): string; /** * Validates that a token fingerprint matches current request. * Uses timing-safe comparison to prevent timing attacks. */ validateFingerprint(storedFingerprint: string, currentFingerprint: string): boolean; /** * Gets the expected session ID for a given permission level. * Used to validate that tokens were issued for the current server session. * This prevents replay attacks with tokens from previous server restarts. */ getExpectedSessionIdForPermission(permissionLevel: ServerPermissionLevel): string | undefined; getAllowedCorsOrigins(): string[]; /** * Check if an origin is allowed for CORS. * Allows any localhost/127.0.0.1 port for development convenience. */ isOriginAllowed(origin: string | undefined): boolean; getCorsHeaders(req: http.IncomingMessage): { [key: string]: string; }; /** * Register temporary content to be served at a specific path. * Useful for serving project-specific content for headless rendering. * @param urlPath The URL path to serve (e.g., "/temp/geometry.json") * @param content The content to serve (string for JSON/text, Uint8Array for binary) * @param contentType The content type (e.g., "application/json", "image/png") */ registerTempContent(urlPath: string, content: string | Uint8Array, contentType: string): void; /** * Unregister temporary content. * @param urlPath The URL path to remove */ unregisterTempContent(urlPath: string): void; /** * Clear all temporary content. */ clearTempContent(): void; /** * Set the content path for serving local files via /api/content. * Used by the 'view' command to serve Minecraft content for browsing. * @param contentPath Absolute path to the local folder to serve */ setContentPath(contentPath: string): void; /** * Get the content path if set. */ getContentPath(): string | undefined; /** * Set whether the server is running in "view" mode. * When true, enables the /api/shutdown endpoint for graceful shutdown. */ setViewMode(isViewMode: boolean): void; /** * Check if the server is running in view mode. */ isViewMode(): boolean; /** * Set whether the server is running in "edit" mode. * When true, enables write operations (PUT/DELETE) on /api/content endpoints. * Edit mode also enables view mode features like /api/shutdown. */ setEditMode(isEditMode: boolean): void; /** * Check if the server is running in edit mode. */ isEditMode(): boolean; /** * Set whether MCP requires authentication even from localhost. * When false (default), localhost requests to /mcp bypass authentication. * When true, all /mcp requests must authenticate via passcode or session token. */ setMcpRequireAuth(requireAuth: boolean): void; /** * Handles an MCP request by lazily initializing the MCP server and delegating. * Sets CORS headers before handing off to the MCP transport, which manages * its own response headers (the SDK's StreamableHTTPServerTransport). */ private _handleMcpRequest; /** * Check if a file name is safe to serve (not in blocked list and has safe extension). */ private isFileSafeToServe; /** * Check if a folder name is safe to traverse (not hidden or blocked). */ private isFolderSafeToServe; getRootPath(): string; /** * Get the path to the res/ folder, checking both the default location * and the public/ folder for development environments. * * The serve vanilla folder (with PNG-converted textures) is often only * available under public/res/ — not under toolbuild/jsn/res/. This method * checks multiple candidate paths to find the one that actually contains * vanilla resource data. */ getResRootPath(): string; /** * Check if a URL path is for vanilla resources (under /res/latest/van/) */ isVanillaResourcePath(urlPath: string): boolean; parseCookies(req: http.IncomingMessage): { [name: string]: string; }; processRequest(req: http.IncomingMessage, res: http.ServerResponse): void; getStatus(portOrSlot: number, includeConfig?: boolean): CartoServerStatusResponse; /** * Handle requests to /api/content/* for serving local Minecraft content. * Generates synthetic index.json files for folders and serves safe file types. * In edit mode, also handles PUT (create/update), DELETE, and POST (mkdir) operations. */ private handleContentRequest; /** * Generate and serve a synthetic index.json for a folder. */ private serveContentFolderListing; /** * Serve a file from the content storage. */ private serveContentFile; /** * Handle requests to /api/worldContent/{slot}/* for serving dedicated server world content. * This endpoint exposes a virtualized view of the server's content folders: * - /api/worldContent/{slot}/behavior_packs/ -> development_behavior_packs * - /api/worldContent/{slot}/resource_packs/ -> development_resource_packs * - /api/worldContent/{slot}/world/ -> worlds/defaultWorld * * The slot parameter is the port number of the dedicated server. */ private handleWorldContentRequest; /** * Serve the root listing of world content categories. */ private serveWorldContentRootListing; /** * Serve a folder listing for world content. */ private serveWorldContentFolderListing; /** * Serve a file from world content. */ private serveWorldContentFile; /** * Handle PUT requests to /api/content/* for creating or updating files. * Requires edit mode and updateState permission. */ private handleContentPut; /** * Handle DELETE requests to /api/content/* for deleting files. * Requires edit mode and updateState permission. */ private handleContentDelete; /** * Handle POST requests to /api/content/* for creating directories or other actions. * Requires edit mode and updateState permission. */ private handleContentPost; /** * Read the full request body as a Buffer. */ private readRequestBody; sendErrorRequest(statusCode: number, message: string, req: http.IncomingMessage, res: http.ServerResponse): void; hasPermissionLevel(currentPermLevel: ServerPermissionLevel, requiredPermissionLevel: ServerPermissionLevel, req: http.IncomingMessage, res: http.ServerResponse): boolean; serveContent(baseSegment: string, relativeUrl: string, storage: IStorage, res: http.ServerResponse): Promise<void>; encrypt(text: string): { iv: string; content: string; authTag: string; }; decrypt(iv: string, content: string, authTag?: string): string; getMimeTypeForFile(extension: string): "application/json" | "application/zip" | "image/jpeg" | "image/png" | "image/svg+xml" | "image/x-icon" | "text/css" | "text/javascript" | "font/woff" | "font/woff2" | "font/ttf"; /** * Escapes a string for safe inclusion in HTML/JavaScript. * Prevents XSS attacks from user-controlled content. */ private escapeForHtml; /** * Escapes a string for safe inclusion in a JavaScript string literal. */ private escapeForJs; /** * Handle upload requests with lazy server initialization. * This allows edit/view modes to deploy content to BDS without requiring * the server to be pre-configured on startup. * * The server will be lazily initialized when the first deploy is attempted. */ private handleUploadWithLazyInit; /** * Handle POST /api/{slot}/stop - Stop the server on a specific slot. */ private handleSlotStop; /** * Handle POST /api/{slot}/restart - Restart the server on a specific slot. */ private handleSlotRestart; /** * Handle requests to /api/worlds/* endpoints. * Provides CRUD operations for managed worlds and their backups. * * Endpoints: * GET /api/worlds - List all managed worlds * POST /api/worlds - Create a new managed world * GET /api/worlds/{id} - Get world info * DELETE /api/worlds/{id} - Delete a world (requires admin) * GET /api/worlds/{id}/backups - List backups for a world * POST /api/worlds/{id}/backups - Create a new backup (with source slot) * GET /api/worlds/{id}/backups/{timestamp} - Get backup info * DELETE /api/worlds/{id}/backups/{timestamp} - Delete a backup * POST /api/worlds/{id}/backups/{timestamp}/restore - Restore backup to slot * GET /api/worlds/{id}/backups/{timestamp}/export - Export as .mcworld */ private handleWorldsRequest; /** * Read request body as JSON with size limit and error handling. */ private readRequestBodyJson; /** * Handle /api/commands/* endpoints for invoking ToolCommands via REST API. * * Endpoints: * GET /api/commands - List available commands * POST /api/commands/{commandName} - Execute a command * * Request body for POST (JSON): * { * "args": ["arg1", "arg2"], * "flags": { "session": "mySession", "traits": ["tameable"] } * } * * Response (JSON): * { * "success": true/false, * "message": "...", * "data": { ... }, * "error": { "code": "...", "message": "..." } * } */ private handleCommandsRequest; getMainContent(req: http.IncomingMessage): string; }