/** * SMB2 Read Command Handler * Handles SMB2_READ requests and responses for reading data from files */ import type { SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2ReadRequest, SMB2ReadResponse, 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 SMB2ReadHandler { 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('SMB2ReadHandler'); } public async handle( requestData: Buffer, connection: SMB2Connection, session?: SMB2Session, treeConnect?: SMB2TreeConnect, ): Promise { try { // Validate session and tree connect if (!session) { this.logger.warn('Read attempt without valid session'); throw new Error('No valid session'); } if (!treeConnect) { this.logger.warn('Read attempt without valid tree connect'); throw new Error('No valid tree connect'); } // Parse the read request const parseResult = this.parser.parseReadRequest(requestData); const request = parseResult.result; const fileIdStr = fileIdToString(request.fileId); this.logger.debug(`Read 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(` Minimum count: ${request.minimumCount}`); this.logger.debug(` Flags: 0x${request.flags.toString(16)}`); // Validate request this.validateReadRequest(request, connection); // Find the open file const openFile = connection.openFiles.get(fileIdStr); if (!openFile) { this.logger.warn(`Attempted to read from 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 read access if (!(openFile.desiredAccess & ACCESS_MASK.FILE_READ_DATA)) { this.logger.warn(`Read access denied for file ${fileIdStr}`); throw new Error('Read access denied'); } // Validate read parameters this.validateReadParameters(request, openFile); // Calculate actual read length const maxReadSize = connection.maxReadSize || 65536; const actualLength = Math.min(request.length, maxReadSize); try { // Read data from the file const data = await this.fileSystemProvider.readFile( request.fileId, request.offset, actualLength ); // Check if we read less than the minimum required if (data.length < request.minimumCount && data.length < actualLength) { this.logger.warn(`Read returned ${data.length} bytes, less than minimum ${request.minimumCount}`); // Continue anyway - some clients are flexible about this } // Calculate remaining bytes (for sequential reads) const remainingBytes = Math.max(0, actualLength - data.length); // Create response const response: SMB2ReadResponse = { structureSize: 17, dataOffset: SMB2_HEADER_SIZE + 17, dataLength: data.length, dataRemaining: remainingBytes, reserved: 0, data, }; // Serialize the response const responseBuffer = this.serializer.serializeReadResponse(response); this.logger.debug(`Read successful: ${data.length} bytes from ${openFile.fileName} at offset ${request.offset}`); // Log performance metrics for large reads if (data.length > 1024 * 1024) { // 1MB this.logger.debug(`Large read completed: ${(data.length / 1024 / 1024).toFixed(2)}MB`); } return responseBuffer; } catch (error) { this.logger.error(`Error reading from file ${fileIdStr}:`, error); throw error; } } catch (error) { this.logger.error('Error handling read request:', error); throw error; } } /** * Validate read request parameters */ private validateReadRequest(request: SMB2ReadRequest, 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 read length if (request.length === 0) { throw new Error('Read length cannot be zero'); } if (request.length > (connection.maxReadSize || 16 * 1024 * 1024)) { throw new Error('Read length exceeds maximum allowed size'); } // Validate minimum count if (request.minimumCount > request.length) { throw new Error('Minimum count cannot exceed read length'); } // Validate offset (check for reasonable bounds) if (request.offset < 0n) { throw new Error('Read offset cannot be negative'); } // Check for potential overflow const maxOffset = BigInt(Number.MAX_SAFE_INTEGER); if (request.offset > maxOffset) { throw new Error('Read offset too large'); } } /** * Validate read parameters against file properties */ private validateReadParameters(request: SMB2ReadRequest, openFile: any): void { // Additional validation can be added here based on file properties // For example, checking file size bounds, etc. // For now, just log the read operation this.logger.debug(`Reading from file: ${openFile.fileName}`); } /** * Handle read with channel info (for multichannel scenarios) */ private async handleChannelRead( request: SMB2ReadRequest, connection: SMB2Connection ): Promise { // Stub implementation for multichannel reads // In a full implementation, this would handle: // - Channel selection // - Load balancing across channels // - Failover scenarios this.logger.debug('Multichannel read requested (not implemented)'); // Fall back to regular read return this.fileSystemProvider.readFile( request.fileId, request.offset, request.length ); } /** * Get read statistics for monitoring */ public getReadStats(): { totalReads: number; totalBytesRead: bigint; averageReadSize: number; largeReads: number; } { // This would be implemented to track read statistics // For now, return default values return { totalReads: 0, totalBytesRead: 0n, averageReadSize: 0, largeReads: 0, }; } /** * Check if read operation would benefit from async processing */ private shouldUseAsyncRead(request: SMB2ReadRequest, openFile: any): boolean { // Determine if this read should be handled asynchronously // Factors to consider: // - Read size // - File location (network vs local) // - Current server load // - Client capabilities const ASYNC_THRESHOLD = 1024 * 1024; // 1MB return request.length > ASYNC_THRESHOLD; } /** * Perform async read operation */ private async performAsyncRead( request: SMB2ReadRequest, connection: SMB2Connection, session: SMB2Session ): Promise { // Stub implementation for async reads // In a full implementation, this would: // 1. Send interim response // 2. Perform read operation in background // 3. Send final response when complete this.logger.debug('Async read operation requested (not fully implemented)'); // For now, just perform synchronous read return this.fileSystemProvider.readFile( request.fileId, request.offset, request.length ); } /** * Handle read-ahead optimization */ private async handleReadAhead( request: SMB2ReadRequest, openFile: any ): Promise { // Implement read-ahead logic for sequential access patterns // This would cache subsequent blocks to improve performance this.logger.debug('Read-ahead optimization triggered (not implemented)'); } /** * Validate read bounds against file size */ private async validateReadBounds( request: SMB2ReadRequest, fileId: SMB2FileId ): Promise { try { const fileInfo = await this.fileSystemProvider.getFileInfo(fileId); const fileSize = fileInfo.standard.endOfFile; // Check if read starts beyond end of file if (request.offset >= fileSize) { this.logger.debug(`Read starts beyond EOF: offset ${request.offset}, size ${fileSize}`); // This is not necessarily an error - Windows allows it } // Log if read extends beyond file size if (request.offset + BigInt(request.length) > fileSize) { this.logger.debug(`Read extends beyond EOF: will be truncated`); } } catch (error) { // If we can't get file info, continue with read anyway this.logger.warn('Could not validate read bounds:', error); } } /** * Handle compressed file reads */ private async handleCompressedRead( request: SMB2ReadRequest, openFile: any ): Promise { // Stub implementation for compressed file handling // In a full implementation, this would: // - Detect if file is compressed // - Decompress data on-the-fly // - Handle partial decompression for range reads this.logger.debug('Compressed file read (not implemented)'); // Fall back to regular read return this.fileSystemProvider.readFile( request.fileId, request.offset, request.length ); } }