@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
583 lines (582 loc) • 24.3 kB
TypeScript
/**
* 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;
}