/** * SMB2 Write Command Handler * Handles SMB2_WRITE requests and responses for writing data to files */ import type { SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2WriteRequest, SMB2WriteResponse, SMB2CommandHandler, SMB2FileId, } from '../protocol/types.js'; import { SMB2_STATUS, SMB2_HEADER_SIZE, SMB2_COMMANDS, ACCESS_MASK, } from '../protocol/constants.js'; import { SMB2MessageParser } from '../protocol/parser.js'; import { SMB2MessageSerializer } from '../protocol/serializer.js'; import { SMB2FileSystemProvider } from '../filesystem/provider.js'; import { Logger } from '../utils/logger.js'; import { fileIdToString, clamp } from '../utils/helpers.js'; export class SMB2WriteHandler { private readonly fileSystemProvider: SMB2FileSystemProvider; private readonly parser: SMB2MessageParser; private readonly serializer: SMB2MessageSerializer; private readonly logger: Logger; constructor(fileSystemProvider: SMB2FileSystemProvider) { this.fileSystemProvider = fileSystemProvider; this.parser = new SMB2MessageParser(); this.serializer = new SMB2MessageSerializer(); this.logger = new Logger('SMB2WriteHandler'); } public async handle( requestData: Buffer, connection: SMB2Connection, session?: SMB2Session, treeConnect?: SMB2TreeConnect, ): Promise { try { // Validate session and tree connect if (!session) { this.logger.warn('Write attempt without valid session'); throw new Error('No valid session'); } if (!treeConnect) { this.logger.warn('Write attempt without valid tree connect'); throw new Error('No valid tree connect'); } // Parse the write request const parseResult = this.parser.parseWriteRequest(requestData); const request = parseResult.result; const fileIdStr = fileIdToString(request.fileId); this.logger.debug(`Write request from ${connection.clientAddress}:`); this.logger.debug(` File ID: ${fileIdStr}`); this.logger.debug(` Length: ${request.length}`); this.logger.debug(` Offset: ${request.offset}`); this.logger.debug(` Flags: 0x${request.flags.toString(16)}`); this.logger.debug(` Channel: ${request.channel}`); // Validate request this.validateWriteRequest(request, connection); // Find the open file const openFile = connection.openFiles.get(fileIdStr); if (!openFile) { this.logger.warn(`Attempted to write to non-existent file: ${fileIdStr}`); throw new Error('File not open'); } // Verify the file belongs to the correct session and tree if (openFile.sessionId !== session.sessionId) { this.logger.warn(`File ${fileIdStr} does not belong to session ${session.sessionId}`); throw new Error('Invalid session for file'); } if (openFile.treeConnectId !== treeConnect.treeConnectId) { this.logger.warn(`File ${fileIdStr} does not belong to tree ${treeConnect.treeConnectId}`); throw new Error('Invalid tree connect for file'); } // Check write access if (!(openFile.desiredAccess & (ACCESS_MASK.FILE_WRITE_DATA | ACCESS_MASK.FILE_APPEND_DATA))) { this.logger.warn(`Write access denied for file ${fileIdStr}`); throw new Error('Write access denied'); } // Validate write parameters this.validateWriteParameters(request, openFile); // Calculate actual write length const maxWriteSize = connection.maxWriteSize || 65536; const actualLength = Math.min(request.length, maxWriteSize); // Ensure we don't write more data than provided const dataToWrite = request.data.subarray(0, actualLength); try { // Check for write-through flag const writeThrough = !!(request.flags & 0x00000001); // SMB2_WRITEFLAG_WRITE_THROUGH if (writeThrough) { this.logger.debug('Write-through requested'); } // Write data to the file const bytesWritten = await this.fileSystemProvider.writeFile( request.fileId, request.offset, dataToWrite ); // Calculate remaining bytes for continuation const remainingBytes = Math.max(0, request.length - bytesWritten); // Create response const response: SMB2WriteResponse = { structureSize: 17, reserved: 0, count: bytesWritten, remaining: remainingBytes, writeChannelInfoOffset: 0, writeChannelInfoLength: 0, writeChannelInfo: Buffer.alloc(0), }; // Serialize the response const responseBuffer = this.serializer.serializeWriteResponse(response); this.logger.debug(`Write successful: ${bytesWritten} bytes to ${openFile.fileName} at offset ${request.offset}`); // Log performance metrics for large writes if (bytesWritten > 1024 * 1024) { // 1MB this.logger.debug(`Large write completed: ${(bytesWritten / 1024 / 1024).toFixed(2)}MB`); } return responseBuffer; } catch (error) { this.logger.error(`Error writing to file ${fileIdStr}:`, error); throw error; } } catch (error) { this.logger.error('Error handling write request:', error); throw error; } } /** * Validate write request parameters */ private validateWriteRequest(request: SMB2WriteRequest, connection: SMB2Connection): void { if (request.structureSize !== 49) { throw new Error(`Invalid structure size: ${request.structureSize}`); } // Validate file ID is not null if (request.fileId.persistent === 0n && request.fileId.volatile === 0n) { throw new Error('Invalid file ID (null file ID)'); } // Validate write length if (request.length === 0) { throw new Error('Write length cannot be zero'); } if (request.length > (connection.maxWriteSize || 16 * 1024 * 1024)) { throw new Error('Write length exceeds maximum allowed size'); } // Validate that we have data to write if (!request.data || request.data.length === 0) { throw new Error('No data provided for write'); } if (request.data.length < request.length) { throw new Error('Provided data is shorter than specified length'); } // Validate offset (check for reasonable bounds) if (request.offset < 0n) { throw new Error('Write offset cannot be negative'); } // Check for potential overflow const maxOffset = BigInt(Number.MAX_SAFE_INTEGER); if (request.offset > maxOffset) { throw new Error('Write offset too large'); } // Validate flags const validFlags = 0x00000001 | 0x00000002; // WRITE_THROUGH | WRITE_UNBUFFERED if ((request.flags & ~validFlags) !== 0) { this.logger.warn(`Unknown write flags: 0x${request.flags.toString(16)}`); } } /** * Validate write parameters against file properties */ private validateWriteParameters(request: SMB2WriteRequest, openFile: any): void { // Additional validation can be added here based on file properties // For example, checking file permissions, disk space, etc. // Check if this is an append operation if (openFile.desiredAccess & ACCESS_MASK.FILE_APPEND_DATA) { this.logger.debug(`Append operation to file: ${openFile.fileName}`); } // Log the write operation this.logger.debug(`Writing to file: ${openFile.fileName}`); } /** * Handle write with channel info (for multichannel scenarios) */ private async handleChannelWrite( request: SMB2WriteRequest, connection: SMB2Connection ): Promise { // Stub implementation for multichannel writes // In a full implementation, this would handle: // - Channel selection for optimal performance // - Load balancing across channels // - Failover scenarios // - Coordinated writes across multiple channels this.logger.debug('Multichannel write requested (not implemented)'); // Fall back to regular write return this.fileSystemProvider.writeFile( request.fileId, request.offset, request.data ); } /** * Get write statistics for monitoring */ public getWriteStats(): { totalWrites: number; totalBytesWritten: bigint; averageWriteSize: number; largeWrites: number; } { // This would be implemented to track write statistics // For now, return default values return { totalWrites: 0, totalBytesWritten: 0n, averageWriteSize: 0, largeWrites: 0, }; } /** * Check if write operation would benefit from async processing */ private shouldUseAsyncWrite(request: SMB2WriteRequest, openFile: any): boolean { // Determine if this write should be handled asynchronously // Factors to consider: // - Write size // - File location (network vs local) // - Current server load // - Client capabilities // - Write-through requirements const ASYNC_THRESHOLD = 1024 * 1024; // 1MB const hasWriteThrough = !!(request.flags & 0x00000001); // Don't use async for write-through operations return request.length > ASYNC_THRESHOLD && !hasWriteThrough; } /** * Perform async write operation */ private async performAsyncWrite( request: SMB2WriteRequest, connection: SMB2Connection, session: SMB2Session ): Promise { // Stub implementation for async writes // In a full implementation, this would: // 1. Send interim response // 2. Perform write operation in background // 3. Send final response when complete this.logger.debug('Async write operation requested (not fully implemented)'); // For now, just perform synchronous write return this.fileSystemProvider.writeFile( request.fileId, request.offset, request.data ); } /** * Handle write caching and optimization */ private async handleWriteCaching( request: SMB2WriteRequest, openFile: any ): Promise { // Implement write caching logic for performance optimization // This would buffer small writes and flush them periodically this.logger.debug('Write caching optimization triggered (not implemented)'); } /** * Validate write bounds and handle file extension */ private async validateWriteBounds( request: SMB2WriteRequest, fileId: SMB2FileId ): Promise { try { const fileInfo = await this.fileSystemProvider.getFileInfo(fileId); const currentSize = fileInfo.standard.endOfFile; const writeEnd = request.offset + BigInt(request.length); // Check if write will extend the file if (writeEnd > currentSize) { this.logger.debug(`Write will extend file: current ${currentSize}, new ${writeEnd}`); // Check available disk space (would be implemented) // await this.checkDiskSpace(writeEnd - currentSize); } } catch (error) { // If we can't get file info, continue with write anyway this.logger.warn('Could not validate write bounds:', error); } } /** * Handle compressed file writes */ private async handleCompressedWrite( request: SMB2WriteRequest, openFile: any ): Promise { // Stub implementation for compressed file handling // In a full implementation, this would: // - Detect if file is compressed // - Compress data before writing // - Handle partial writes to compressed files this.logger.debug('Compressed file write (not implemented)'); // Fall back to regular write return this.fileSystemProvider.writeFile( request.fileId, request.offset, request.data ); } /** * Handle write conflicts and locking */ private async handleWriteConflicts( request: SMB2WriteRequest, openFile: any ): Promise { // Implement write conflict detection and resolution // This would handle: // - Range locks // - Exclusive access // - Opportunistic locks // - Share mode conflicts this.logger.debug('Write conflict checking (not implemented)'); } /** * Perform write-through operation */ private async performWriteThrough( request: SMB2WriteRequest, connection: SMB2Connection ): Promise { // Implement write-through operation // This ensures data is written directly to storage without caching this.logger.debug('Write-through operation'); const bytesWritten = await this.fileSystemProvider.writeFile( request.fileId, request.offset, request.data ); // Force flush to storage (would be implemented at filesystem level) // await this.fileSystemProvider.flushFile(request.fileId); return bytesWritten; } /** * Handle unbuffered writes */ private async performUnbufferedWrite( request: SMB2WriteRequest, connection: SMB2Connection ): Promise { // Implement unbuffered write operation // This bypasses system caches for direct I/O this.logger.debug('Unbuffered write operation'); // For now, just perform regular write return this.fileSystemProvider.writeFile( request.fileId, request.offset, request.data ); } }