/** * SMB2/3 Server Implementation * Main server class that handles connections, sessions, and message routing */ import { EventEmitter } from 'events'; import { createServer, Server as NetServer, Socket } from 'net'; import type { SMB2ServerConfig, SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2Message, SMB2Header, SMB2Event, SMB2EventHandler, SMB2CommandHandler, SMB2EventType, SMB2FileId, SMB2Guid, } from './protocol/types.js'; import { SMB2_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_COMMANDS, SMB2_STATUS, SMB2_FLAGS, SMB2_DIALECTS, DEFAULT_MAX_TRANSACT_SIZE, DEFAULT_MAX_READ_SIZE, DEFAULT_MAX_WRITE_SIZE, NETWORK_TIMEOUT, SESSION_TIMEOUT, } from './protocol/constants.js'; import { SMB2MessageParser } from './protocol/parser.js'; import { SMB2MessageSerializer } from './protocol/serializer.js'; import { SMB2NegotiateHandler } from './commands/negotiate.js'; import { SMB2SessionSetupHandler } from './commands/session-setup.js'; import { SMB2TreeConnectHandler } from './commands/tree-connect.js'; import { SMB2CreateHandler } from './commands/create'; import { SMB2CloseHandler } from './commands/close.js'; import { SMB2ReadHandler } from './commands/read.js'; import { SMB2WriteHandler } from './commands/write.js'; import { SMB2QueryDirectoryHandler } from './commands/query-directory.js'; import { SMB2QueryInfoHandler } from './commands/query-info.js'; import { SMB2SetInfoHandler } from './commands/set-info.js'; import { SMB2EchoHandler } from './commands/echo.js'; import { SMB2LogoffHandler } from './commands/logoff.js'; import { SMB2TreeDisconnectHandler } from './commands/tree-disconnect.js'; import { SMB2AuthenticationManager } from './auth/authentication.js'; import { SMB2FileSystemProvider } from './filesystem/provider.js'; import { Logger } from './utils/logger.js'; import { generateGuid, validateSMB2Header } from './utils/helpers.js'; export class SMB2Server extends EventEmitter { private readonly config: SMB2ServerConfig; private readonly logger: Logger; private readonly parser: SMB2MessageParser; private readonly serializer: SMB2MessageSerializer; private readonly authManager: SMB2AuthenticationManager; private readonly fileSystemProvider: SMB2FileSystemProvider; private readonly commandHandlers: Map; private readonly connections: Map; private readonly sessions: Map; private readonly treeConnects: Map; private readonly asyncContexts: Map; private server: NetServer | null = null; private isRunning = false; private connectionIdCounter = 0; private sessionIdCounter = 1n; private treeIdCounter = 1; private messageIdCounter = 1n; private asyncIdCounter = 1n; constructor(config: SMB2ServerConfig) { super(); this.config = { ...this.getDefaultConfig(), ...config, }; this.logger = new Logger('SMB2Server'); this.parser = new SMB2MessageParser(); this.serializer = new SMB2MessageSerializer(); this.authManager = new SMB2AuthenticationManager(this.config); this.fileSystemProvider = new SMB2FileSystemProvider(this.config); this.commandHandlers = new Map(); this.connections = new Map(); this.sessions = new Map(); this.treeConnects = new Map(); this.asyncContexts = new Map(); this.setupCommandHandlers(); this.setupEventHandlers(); } private getDefaultConfig(): Partial { return { serverGuid: generateGuid(), serverName: 'SMB2-Server', supportedDialects: [ SMB2_DIALECTS.SMB2_0_2, SMB2_DIALECTS.SMB2_1_0, SMB2_DIALECTS.SMB3_0_0, SMB2_DIALECTS.SMB3_0_2, SMB2_DIALECTS.SMB3_1_1, ], requireMessageSigning: false, enableEncryption: false, maxConnections: 100, maxSessionsPerConnection: 16, sessionTimeout: SESSION_TIMEOUT, maxTransactSize: DEFAULT_MAX_TRANSACT_SIZE, maxReadSize: DEFAULT_MAX_READ_SIZE, maxWriteSize: DEFAULT_MAX_WRITE_SIZE, shares: [], users: [], }; } private setupCommandHandlers(): void { this.commandHandlers.set(SMB2_COMMANDS.NEGOTIATE, new SMB2NegotiateHandler(this.config)); this.commandHandlers.set(SMB2_COMMANDS.SESSION_SETUP, new SMB2SessionSetupHandler(this.authManager)); this.commandHandlers.set(SMB2_COMMANDS.TREE_CONNECT, new SMB2TreeConnectHandler(this.config)); this.commandHandlers.set(SMB2_COMMANDS.CREATE, new SMB2CreateHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.CLOSE, new SMB2CloseHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.READ, new SMB2ReadHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.WRITE, new SMB2WriteHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.QUERY_DIRECTORY, new SMB2QueryDirectoryHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.QUERY_INFO, new SMB2QueryInfoHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.SET_INFO, new SMB2SetInfoHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.ECHO, new SMB2EchoHandler()); this.commandHandlers.set(SMB2_COMMANDS.LOGOFF, new SMB2LogoffHandler()); this.commandHandlers.set(SMB2_COMMANDS.TREE_DISCONNECT, new SMB2TreeDisconnectHandler()); } private setupEventHandlers(): void { // Handle cleanup on process termination process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); process.on('uncaughtException', (error) => { this.logger.error('Uncaught exception:', error); this.emitEvent('error', { error }); }); process.on('unhandledRejection', (reason) => { this.logger.error('Unhandled rejection:', reason); this.emitEvent('error', { error: reason }); }); } public async start(port: number = 445, hostname?: string): Promise { if (this.isRunning) { throw new Error('Server is already running'); } return new Promise((resolve, reject) => { this.server = createServer((socket) => { this.handleConnection(socket); }); this.server.on('error', (error) => { this.logger.error('Server error:', error); this.emitEvent('error', { error }); reject(error); }); this.server.on('listening', () => { this.isRunning = true; const address = this.server?.address(); this.logger.info(`SMB2 Server listening on ${hostname ?? '0.0.0.0'}:${port}`); this.logger.info(`Server GUID: ${this.config.serverGuid}`); this.logger.info(`Supported dialects: ${this.config.supportedDialects.map(d => `0x${d.toString(16).toUpperCase().padStart(4, '0')}`).join(', ')}`); resolve(); }); this.server.listen(port, hostname); }); } public async shutdown(): Promise { if (!this.isRunning || !this.server) { return; } this.logger.info('Shutting down SMB2 Server...'); this.isRunning = false; // Close all connections gracefully const closePromises = Array.from(this.connections.values()).map(connection => this.closeConnection(connection.connectionId, 'Server shutdown') ); await Promise.all(closePromises); // Close the server return new Promise((resolve) => { this.server?.close(() => { this.logger.info('SMB2 Server shut down'); resolve(); }); }); } private handleConnection(socket: Socket): void { const connectionId = `conn_${++this.connectionIdCounter}`; const clientAddress = `${socket.remoteAddress}:${socket.remotePort}`; const connection: SMB2Connection = { connectionId, clientAddress, serverAddress: `${socket.localAddress}:${socket.localPort}`, dialect: SMB2_DIALECTS.SMB2_0_2, // Will be negotiated supportsMultiCredit: false, maxTransactSize: this.config.maxTransactSize, maxReadSize: this.config.maxReadSize, maxWriteSize: this.config.maxWriteSize, requireMessageSigning: this.config.requireMessageSigning, clientGuid: '', creationTime: new Date(), lastActivityTime: new Date(), sessions: new Map(), treeConnects: new Map(), openFiles: new Map(), }; this.connections.set(connectionId, connection); this.logger.info(`New connection established: ${connectionId} from ${clientAddress}`); this.emitEvent('connection:established', { connection }); // Set up socket event handlers socket.setTimeout(NETWORK_TIMEOUT); socket.setKeepAlive(true, 30000); socket.setNoDelay(true); let buffer = Buffer.alloc(0); socket.on('data', async (data) => { try { buffer = Buffer.concat([buffer, data]); buffer = await this.processMessages(buffer, connection); connection.lastActivityTime = new Date(); } catch (error) { this.logger.error(`Error processing data from ${connectionId}:`, error); this.emitEvent('error', { connectionId, error }); this.closeConnection(connectionId, 'Protocol error'); } }); socket.on('error', (error) => { this.logger.error(`Socket error for ${connectionId}:`, error); this.emitEvent('error', { connectionId, error }); }); socket.on('timeout', () => { this.logger.warn(`Socket timeout for ${connectionId}`); this.closeConnection(connectionId, 'Timeout'); }); socket.on('close', () => { this.logger.info(`Connection closed: ${connectionId}`); this.closeConnection(connectionId, 'Client disconnected'); }); // Store the socket for later use (connection as any).socket = socket; } private async processMessages(buffer: Buffer, connection: SMB2Connection): Promise { let offset = 0; while (offset + SMB2_HEADER_SIZE <= buffer.length) { try { // Parse the SMB2 header first to get message length const headerResult = this.parser.parseHeader(buffer, offset); const header = headerResult.result; // Validate the header if (!validateSMB2Header(header)) { throw new Error('Invalid SMB2 header'); } // Calculate total message size const messageSize = this.calculateMessageSize(header, buffer, offset); // Check if we have the complete message if (offset + messageSize > buffer.length) { break; // Wait for more data } // Extract the complete message const messageBuffer = buffer.subarray(offset, offset + messageSize); const message: SMB2Message = { header, data: messageBuffer.subarray(SMB2_HEADER_SIZE), }; // Process the message await this.processMessage(message, connection); offset += messageSize; // Handle compound messages if (header.nextCommand > 0) { offset = Math.max(offset, header.nextCommand); } } catch (error) { this.logger.error('Error parsing message:', JSON.stringify(error)); // Send error response if possible await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER); break; } } return buffer.subarray(offset); } private calculateMessageSize(header: SMB2Header, buffer: Buffer, offset: number): number { // For most commands, we need to read the structure size from the payload // For now, we'll use a simple heuristic based on the command type const baseSize = SMB2_HEADER_SIZE; if (header.command === SMB2_COMMANDS.NEGOTIATE) { return baseSize + this.calculateNegotiateSize(buffer, offset + baseSize); } // For other commands, use next command offset or default size if (header.nextCommand > 0) { return header.nextCommand; } // Default minimum message size return Math.min(baseSize + 512, buffer.length - offset); } private calculateNegotiateSize(buffer: Buffer, offset: number): number { if (offset + 36 > buffer.length) return 0; const structureSize = buffer.readUInt16LE(offset); const dialectCount = buffer.readUInt16LE(offset + 2); return structureSize + (dialectCount * 2); } private async processMessage(message: SMB2Message, connection: SMB2Connection): Promise { const { header } = message; try { // Find the command handler const handler = this.commandHandlers.get(header.command); if (!handler) { this.logger.warn(`Unknown command: 0x${header.command.toString(16).toUpperCase()}`); await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER, header); return; } // Get session and tree connect if applicable const session = header.sessionId !== 0n ? this.sessions.get(header.sessionId) : undefined; const treeConnect = header.treeId !== 0 ? this.treeConnects.get(header.treeId) : undefined; // Call the handler const responseData = await handler(message.data, connection, session, treeConnect); // Send the response await this.sendResponse(connection, header, SMB2_STATUS.SUCCESS, responseData); } catch (error) { this.logger.error(`Error processing ${SMB2_COMMANDS[header.command]} command:`, error); await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER, header); } } private async sendResponse( connection: SMB2Connection, requestHeader: SMB2Header, status: SMB2_STATUS, data: Buffer ): Promise { const responseHeader: SMB2Header = { protocolId: SMB2_PROTOCOL_ID, structureSize: SMB2_HEADER_SIZE, creditCharge: requestHeader.creditCharge, status, command: requestHeader.command, creditRequestResponse: 1, // Grant 1 credit flags: requestHeader.flags | SMB2_FLAGS.SERVER_TO_REDIR, nextCommand: 0, messageId: requestHeader.messageId, reserved: 0, treeId: requestHeader.treeId, sessionId: requestHeader.sessionId, signature: Buffer.alloc(16), // TODO: Implement signing }; const headerBuffer = this.serializer.serializeHeader(responseHeader); const responseBuffer = Buffer.concat([headerBuffer, data]); await this.sendToConnection(connection, responseBuffer); } private async sendErrorResponse( connection: SMB2Connection, status: SMB2_STATUS, requestHeader?: SMB2Header ): Promise { const errorData = Buffer.alloc(8); // Empty error data errorData.writeUInt16LE(8, 0); // Structure size errorData.writeUInt16LE(0, 2); // Reserved errorData.writeUInt32LE(0, 4); // Byte count const responseHeader: SMB2Header = { protocolId: SMB2_PROTOCOL_ID, structureSize: SMB2_HEADER_SIZE, creditCharge: requestHeader?.creditCharge ?? 1, status, command: requestHeader?.command ?? SMB2_COMMANDS.NEGOTIATE, creditRequestResponse: 1, flags: (requestHeader?.flags ?? 0) | SMB2_FLAGS.SERVER_TO_REDIR, nextCommand: 0, messageId: requestHeader?.messageId ?? this.messageIdCounter++, reserved: 0, treeId: requestHeader?.treeId ?? 0, sessionId: requestHeader?.sessionId ?? 0n, signature: Buffer.alloc(16), }; const headerBuffer = this.serializer.serializeHeader(responseHeader); const responseBuffer = Buffer.concat([headerBuffer, errorData]); await this.sendToConnection(connection, responseBuffer); } private async sendToConnection(connection: SMB2Connection, data: Buffer): Promise { const socket = (connection as any).socket as Socket; if (socket && !socket.destroyed) { return new Promise((resolve, reject) => { socket.write(data, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } } private async closeConnection(connectionId: string, reason: string): Promise { const connection = this.connections.get(connectionId); if (!connection) return; this.logger.info(`Closing connection ${connectionId}: ${reason}`); // Close all sessions for this connection for (const session of connection.sessions.values()) { await this.closeSession(session.sessionId, reason); } // Close all tree connects for this connection for (const treeConnect of connection.treeConnects.values()) { await this.closeTreeConnect(treeConnect.treeConnectId, reason); } // Close the socket const socket = (connection as any).socket as Socket; if (socket && !socket.destroyed) { socket.destroy(); } // Remove the connection this.connections.delete(connectionId); this.emitEvent('connection:closed', { connectionId, reason }); } private async closeSession(sessionId: bigint, reason: string): Promise { const session = this.sessions.get(sessionId); if (!session) return; this.logger.debug(`Closing session ${sessionId}: ${reason}`); this.sessions.delete(sessionId); this.emitEvent('session:closed', { sessionId, reason }); } private async closeTreeConnect(treeConnectId: number, reason: string): Promise { const treeConnect = this.treeConnects.get(treeConnectId); if (!treeConnect) return; this.logger.debug(`Closing tree connect ${treeConnectId}: ${reason}`); this.treeConnects.delete(treeConnectId); this.emitEvent('tree:disconnected', { treeConnectId, reason }); } private emitEvent(type: SMB2EventType, data: T): void { const event: SMB2Event = { type, timestamp: new Date(), data, }; this.emit(type, event); this.emit('event', event); } // Public API methods public onEvent(type: SMB2EventType, handler: SMB2EventHandler): void { this.on(type, handler); } public offEvent(type: SMB2EventType, handler: SMB2EventHandler): void { this.off(type, handler); } public getConnections(): SMB2Connection[] { return Array.from(this.connections.values()); } public getConnection(connectionId: string): SMB2Connection | undefined { return this.connections.get(connectionId); } public getSessions(): SMB2Session[] { return Array.from(this.sessions.values()); } public getSession(sessionId: bigint): SMB2Session | undefined { return this.sessions.get(sessionId); } public getTreeConnects(): SMB2TreeConnect[] { return Array.from(this.treeConnects.values()); } public getTreeConnect(treeConnectId: number): SMB2TreeConnect | undefined { return this.treeConnects.get(treeConnectId); } public getStats(): { connections: number; sessions: number; treeConnects: number; uptime: number; } { return { connections: this.connections.size, sessions: this.sessions.size, treeConnects: this.treeConnects.size, uptime: this.isRunning ? Date.now() - this.getServerStartTime() : 0, }; } private getServerStartTime(): number { // This would be set when the server starts return Date.now(); // Placeholder } public isServerRunning(): boolean { return this.isRunning; } public getConfig(): SMB2ServerConfig { return { ...this.config }; } // Session management methods public createSession(connectionId: string, userId: string): SMB2Session { const sessionId = this.sessionIdCounter++; const connection = this.connections.get(connectionId); if (!connection) { throw new Error(`Connection ${connectionId} not found`); } const session: SMB2Session = { sessionId, userId, isGuest: false, isAnonymous: false, creationTime: new Date(), lastActivityTime: new Date(), signingRequired: this.config.requireMessageSigning, encryptionRequired: this.config.enableEncryption, preauthIntegrityHashValue: Buffer.alloc(64), sessionKey: Buffer.alloc(32), signKey: Buffer.alloc(16), }; this.sessions.set(sessionId, session); connection.sessions.set(sessionId, session); this.emitEvent('session:established', { sessionId, connectionId, userId }); return session; } public createTreeConnect(sessionId: bigint, shareName: string): SMB2TreeConnect { const treeConnectId = this.treeIdCounter++; const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const shareConfig = this.config.shares.find(s => s.name === shareName); if (!shareConfig) { throw new Error(`Share ${shareName} not found`); } const treeConnect: SMB2TreeConnect = { treeConnectId, sessionId, shareName, shareType: shareConfig.shareType, shareFlags: shareConfig.shareFlags, maximalAccess: shareConfig.maximalAccess, encryptData: shareConfig.encryptData, creationTime: new Date(), lastActivityTime: new Date(), }; this.treeConnects.set(treeConnectId, treeConnect); this.emitEvent('tree:connected', { treeConnectId, sessionId, shareName }); return treeConnect; } public generateFileId(): SMB2FileId { return { persistent: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)), volatile: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)), }; } }