/** * SMB2 Close Command Handler * Handles SMB2_CLOSE requests and responses for closing files and directories */ import type { SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2CloseRequest, SMB2CloseResponse, SMB2CommandHandler, SMB2FileId, SMB2OpenFile, } from '../protocol/types.js'; import { SMB2_STATUS, SMB2_HEADER_SIZE, SMB2_COMMANDS, FILE_ATTRIBUTES, } 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, getCurrentFileTime } from '../utils/helpers.js'; export class SMB2CloseHandler { 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('SMB2CloseHandler'); } public async handle( requestData: Buffer, connection: SMB2Connection, session?: SMB2Session, treeConnect?: SMB2TreeConnect, ): Promise { try { // Validate session and tree connect if (!session) { this.logger.warn('Close attempt without valid session'); throw new Error('No valid session'); } if (!treeConnect) { this.logger.warn('Close attempt without valid tree connect'); throw new Error('No valid tree connect'); } // Parse the close request const parseResult = this.parser.parseCloseRequest(requestData); const request = parseResult.result; const fileIdStr = fileIdToString(request.fileId); this.logger.debug(`Close request from ${connection.clientAddress}:`); this.logger.debug(` File ID: ${fileIdStr}`); this.logger.debug(` Flags: 0x${request.flags.toString(16)}`); // Validate request this.validateCloseRequest(request); // Find the open file const openFile = connection.openFiles.get(fileIdStr); if (!openFile) { this.logger.warn(`Attempted to close 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'); } // Get file information before closing (if requested by flags) let fileInfo = null; if (request.flags & 0x0001) { // SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB try { fileInfo = await this.fileSystemProvider.getFileInfo(request.fileId); } catch (error) { this.logger.warn(`Failed to get file info before close: ${error}`); // Continue with close even if we can't get file info } } try { // Close the file through filesystem provider await this.fileSystemProvider.closeFile(request.fileId); } catch (error) { this.logger.error(`Error closing file ${fileIdStr}:`, error); // Continue to remove from tracking even if close failed } // Remove from connection's open files tracking connection.openFiles.delete(fileIdStr); // Create response with file information if requested and available const currentTime = getCurrentFileTime(); const response: SMB2CloseResponse = { structureSize: 60, flags: request.flags, reserved: 0, creationTime: fileInfo?.basic.creationTime || currentTime, lastAccessTime: fileInfo?.basic.lastAccessTime || currentTime, lastWriteTime: fileInfo?.basic.lastWriteTime || currentTime, changeTime: fileInfo?.basic.changeTime || currentTime, allocationSize: fileInfo?.standard.allocationSize || 0n, endOfFile: fileInfo?.standard.endOfFile || 0n, fileAttributes: fileInfo?.basic.fileAttributes || FILE_ATTRIBUTES.NORMAL, }; // Serialize the response const responseBuffer = this.serializer.serializeCloseResponse(response); this.logger.info(`File closed successfully: ${openFile.fileName} (FileID: ${fileIdStr})`); return responseBuffer; } catch (error) { this.logger.error('Error handling close request:', error); throw error; } } /** * Validate close request parameters */ private validateCloseRequest(request: SMB2CloseRequest): void { if (request.structureSize !== 24) { 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 flags (only known flags are allowed) const validFlags = 0x0001; // SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB if ((request.flags & ~validFlags) !== 0) { this.logger.warn(`Unknown close flags: 0x${request.flags.toString(16)}`); } } /** * Force close all files for a session */ public async forceCloseSessionFiles( connection: SMB2Connection, sessionId: bigint ): Promise { const filesToClose: string[] = []; // Find all files belonging to the session for (const [fileIdStr, openFile] of connection.openFiles.entries()) { if (openFile.sessionId === sessionId) { filesToClose.push(fileIdStr); } } // Close each file for (const fileIdStr of filesToClose) { try { const openFile = connection.openFiles.get(fileIdStr); if (openFile) { await this.fileSystemProvider.closeFile(openFile.fileId); connection.openFiles.delete(fileIdStr); this.logger.debug(`Force closed file: ${openFile.fileName} (${fileIdStr})`); } } catch (error) { this.logger.warn(`Error force closing file ${fileIdStr}:`, error); } } if (filesToClose.length > 0) { this.logger.info(`Force closed ${filesToClose.length} files for session ${sessionId}`); } } /** * Force close all files for a tree connect */ public async forceCloseTreeFiles( connection: SMB2Connection, treeConnectId: number ): Promise { const filesToClose: string[] = []; // Find all files belonging to the tree connect for (const [fileIdStr, openFile] of connection.openFiles.entries()) { if (openFile.treeConnectId === treeConnectId) { filesToClose.push(fileIdStr); } } // Close each file for (const fileIdStr of filesToClose) { try { const openFile = connection.openFiles.get(fileIdStr); if (openFile) { await this.fileSystemProvider.closeFile(openFile.fileId); connection.openFiles.delete(fileIdStr); this.logger.debug(`Force closed file: ${openFile.fileName} (${fileIdStr})`); } } catch (error) { this.logger.warn(`Error force closing file ${fileIdStr}:`, error); } } if (filesToClose.length > 0) { this.logger.info(`Force closed ${filesToClose.length} files for tree ${treeConnectId}`); } } /** * Force close all files for a connection */ public async forceCloseConnectionFiles(connection: SMB2Connection): Promise { const filesToClose = Array.from(connection.openFiles.keys()); // Close each file for (const fileIdStr of filesToClose) { try { const openFile = connection.openFiles.get(fileIdStr); if (openFile) { await this.fileSystemProvider.closeFile(openFile.fileId); connection.openFiles.delete(fileIdStr); this.logger.debug(`Force closed file: ${openFile.fileName} (${fileIdStr})`); } } catch (error) { this.logger.warn(`Error force closing file ${fileIdStr}:`, error); } } if (filesToClose.length > 0) { this.logger.info(`Force closed ${filesToClose.length} files for connection ${connection.connectionId}`); } } /** * Get information about open files */ public getOpenFilesInfo(connection: SMB2Connection): Array<{ fileId: string; fileName: string; sessionId: bigint; treeConnectId: number; openTime: Date; }> { const openFiles: Array<{ fileId: string; fileName: string; sessionId: bigint; treeConnectId: number; openTime: Date; }> = []; for (const [fileIdStr, openFile] of connection.openFiles.entries()) { openFiles.push({ fileId: fileIdStr, fileName: openFile.fileName, sessionId: openFile.sessionId, treeConnectId: openFile.treeConnectId, openTime: openFile.creationTime, }); } return openFiles; } /** * Check if a file is open */ public isFileOpen(connection: SMB2Connection, fileId: SMB2FileId): boolean { const fileIdStr = fileIdToString(fileId); return connection.openFiles.has(fileIdStr); } /** * Get open file information */ public getOpenFile(connection: SMB2Connection, fileId: SMB2FileId): SMB2OpenFile | undefined { const fileIdStr = fileIdToString(fileId); return connection.openFiles.get(fileIdStr); } }