/** * SMB2 Create Command Handler * Handles SMB2_CREATE requests and responses for opening/creating files and directories */ import type { SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2CreateRequest, SMB2CreateResponse, SMB2CreateContext, SMB2CommandHandler, SMB2FileId, } from '../protocol/types.js'; import { SMB2_STATUS, SMB2_HEADER_SIZE, SMB2_COMMANDS, ACCESS_MASK, SHARE_ACCESS, CREATE_DISPOSITION, CREATE_OPTIONS, FILE_ATTRIBUTES, OPLOCK_LEVEL, } 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 { getCurrentFileTime } from '../utils/helpers.js'; export class SMB2CreateHandler { 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('SMB2CreateHandler'); } public async handle( requestData: Buffer, connection: SMB2Connection, session?: SMB2Session, treeConnect?: SMB2TreeConnect, ): Promise { try { // Validate session and tree connect if (!session) { this.logger.warn('Create attempt without valid session'); throw new Error('No valid session'); } if (!treeConnect) { this.logger.warn('Create attempt without valid tree connect'); throw new Error('No valid tree connect'); } // Parse the create request const parseResult = this.parser.parseCreateRequest(requestData); const request = parseResult.result; this.logger.debug(`Create request from ${connection.clientAddress}:`); this.logger.debug(` Name: ${request.name}`); this.logger.debug(` Desired access: 0x${request.desiredAccess.toString(16)}`); this.logger.debug(` File attributes: 0x${request.fileAttributes.toString(16)}`); this.logger.debug(` Share access: 0x${request.shareAccess.toString(16)}`); this.logger.debug(` Create disposition: ${request.createDisposition}`); this.logger.debug(` Create options: 0x${request.createOptions.toString(16)}`); this.logger.debug(` Requested oplock level: ${request.requestedOplockLevel}`); // Validate request this.validateCreateRequest(request); // Determine create action let createAction = this.determineCreateAction(request.createDisposition); try { // Open or create the file/directory const fileHandle = await this.fileSystemProvider.openFile( treeConnect.shareName, request.name, request.desiredAccess, request.shareAccess, request.createDisposition, request.createOptions, request.fileAttributes, session, treeConnect ); // Update create action based on what actually happened createAction = this.getActualCreateAction(request.createDisposition, true); // Process create contexts (if any) const responseContexts = await this.processCreateContexts( request.createContexts, fileHandle, request ); // Determine granted oplock level const grantedOplockLevel = this.determineOplockLevel( request.requestedOplockLevel, request.createOptions, fileHandle ); // Get file times const currentTime = getCurrentFileTime(); // Create response const response: SMB2CreateResponse = { structureSize: 89, oplockLevel: grantedOplockLevel, flags: 0, // No special flags for now createAction, creationTime: fileHandle.creationTime ? BigInt(fileHandle.creationTime.getTime()) * 10000n + 116444736000000000n : currentTime, lastAccessTime: fileHandle.lastAccessTime ? BigInt(fileHandle.lastAccessTime.getTime()) * 10000n + 116444736000000000n : currentTime, lastWriteTime: fileHandle.lastWriteTime ? BigInt(fileHandle.lastWriteTime.getTime()) * 10000n + 116444736000000000n : currentTime, changeTime: fileHandle.changeTime ? BigInt(fileHandle.changeTime.getTime()) * 10000n + 116444736000000000n : currentTime, allocationSize: fileHandle.allocationSize, endOfFile: fileHandle.fileSize, fileAttributes: fileHandle.fileAttributes, reserved2: 0, fileId: fileHandle.fileId, createContextsOffset: responseContexts.length > 0 ? SMB2_HEADER_SIZE + 89 : 0, createContextsLength: this.calculateContextsLength(responseContexts), createContexts: responseContexts, }; // Add file handle to connection's open files const fileIdStr = `${fileHandle.fileId.persistent.toString(16)}-${fileHandle.fileId.volatile.toString(16)}`; const openFile = { fileId: fileHandle.fileId, treeConnectId: treeConnect.treeConnectId, sessionId: session.sessionId, fileName: request.name, desiredAccess: request.desiredAccess, shareAccess: request.shareAccess, createOptions: request.createOptions, fileAttributes: fileHandle.fileAttributes, oplockLevel: grantedOplockLevel, creationTime: new Date(), lastAccessTime: new Date(), isDurable: false, isResilient: false, }; connection.openFiles.set(fileIdStr, openFile); // Serialize the response const responseBuffer = this.serializer.serializeCreateResponse(response); this.logger.info(`File created/opened successfully: ${request.name} (FileID: ${fileIdStr})`); return responseBuffer; } catch (error) { this.logger.error(`Error creating/opening file ${request.name}:`, error); throw error; } } catch (error) { this.logger.error('Error handling create request:', error); throw error; } } /** * Validate create request parameters */ private validateCreateRequest(request: SMB2CreateRequest): void { if (request.structureSize !== 57) { throw new Error(`Invalid structure size: ${request.structureSize}`); } if (request.nameLength === 0) { throw new Error('Empty filename provided'); } if (request.name.length === 0) { throw new Error('No filename provided'); } if (request.nameLength > 32767) { throw new Error('Filename too long'); } // Check for invalid create disposition if (request.createDisposition < 0 || request.createDisposition > 5) { throw new Error(`Invalid create disposition: ${request.createDisposition}`); } // Validate create options compatibility if ((request.createOptions & CREATE_OPTIONS.DIRECTORY_FILE) && (request.createOptions & CREATE_OPTIONS.NON_DIRECTORY_FILE)) { throw new Error('Cannot specify both DIRECTORY_FILE and NON_DIRECTORY_FILE'); } } /** * Determine initial create action from disposition */ private determineCreateAction(disposition: CREATE_DISPOSITION): number { switch (disposition) { case CREATE_DISPOSITION.SUPERSEDE: return 0; // FILE_SUPERSEDED case CREATE_DISPOSITION.OPEN: return 1; // FILE_OPENED case CREATE_DISPOSITION.CREATE: return 2; // FILE_CREATED case CREATE_DISPOSITION.OPEN_IF: return 1; // FILE_OPENED (will be updated if created) case CREATE_DISPOSITION.OVERWRITE: return 3; // FILE_OVERWRITTEN case CREATE_DISPOSITION.OVERWRITE_IF: return 3; // FILE_OVERWRITTEN (will be updated if created) default: return 1; // FILE_OPENED (default) } } /** * Get actual create action based on what happened */ private getActualCreateAction(disposition: CREATE_DISPOSITION, existed: boolean): number { switch (disposition) { case CREATE_DISPOSITION.SUPERSEDE: return 0; // FILE_SUPERSEDED case CREATE_DISPOSITION.OPEN: return 1; // FILE_OPENED case CREATE_DISPOSITION.CREATE: return 2; // FILE_CREATED case CREATE_DISPOSITION.OPEN_IF: return existed ? 1 : 2; // FILE_OPENED or FILE_CREATED case CREATE_DISPOSITION.OVERWRITE: return 3; // FILE_OVERWRITTEN case CREATE_DISPOSITION.OVERWRITE_IF: return existed ? 3 : 2; // FILE_OVERWRITTEN or FILE_CREATED default: return 1; // FILE_OPENED } } /** * Determine granted oplock level */ private determineOplockLevel( requestedLevel: OPLOCK_LEVEL, createOptions: CREATE_OPTIONS, fileHandle: any ): OPLOCK_LEVEL { // For directories, no oplocks if (fileHandle.isDirectory) { return OPLOCK_LEVEL.NONE; } // For now, don't grant any oplocks (stub implementation) // In a full implementation, this would consider: // - Other opens on the same file // - Client capabilities // - Share access modes // - Server policies return OPLOCK_LEVEL.NONE; } /** * Process create contexts */ private async processCreateContexts( contexts: SMB2CreateContext[], fileHandle: any, request: SMB2CreateRequest ): Promise { const responseContexts: SMB2CreateContext[] = []; // Process each create context for (const context of contexts) { try { const responseContext = await this.processCreateContext(context, fileHandle, request); if (responseContext) { responseContexts.push(responseContext); } } catch (error) { this.logger.warn(`Error processing create context ${context.name}:`, error); // Continue with other contexts } } return responseContexts; } /** * Process individual create context */ private async processCreateContext( context: SMB2CreateContext, fileHandle: any, request: SMB2CreateRequest ): Promise { switch (context.name) { case 'MxAc': // Maximal Access return this.createMaximalAccessContext(fileHandle, request); case 'QFid': // Query File ID return this.createQueryFileIdContext(fileHandle); case 'RqLs': // Request Lease (stub) return this.createRequestLeaseContext(context, fileHandle); case 'DH2Q': // Durable Handle v2 Query (stub) return this.createDurableHandleContext(context, fileHandle); case 'DHnQ': // Durable Handle Query (stub) return this.createDurableHandleContext(context, fileHandle); default: this.logger.debug(`Unsupported create context: ${context.name}`); return null; } } /** * Create Maximal Access response context */ private createMaximalAccessContext(fileHandle: any, request: SMB2CreateRequest): SMB2CreateContext { const data = Buffer.alloc(8); data.writeUInt32LE(0, 0); // Query status (success) data.writeUInt32LE(request.desiredAccess, 4); // Maximal access return { next: 0, nameOffset: 16, nameLength: 8, // "MxAc".length * 2 for UTF-16 reserved: 0, dataOffset: 32, dataLength: 8, name: 'MxAc', data, }; } /** * Create Query File ID response context */ private createQueryFileIdContext(fileHandle: any): SMB2CreateContext { const data = Buffer.alloc(32); // File ID (16 bytes) data.writeBigUInt64LE(fileHandle.fileId.persistent, 0); data.writeBigUInt64LE(fileHandle.fileId.volatile, 8); // Volume ID (16 bytes) - stub implementation data.fill(0, 16, 32); return { next: 0, nameOffset: 16, nameLength: 8, // "QFid".length * 2 for UTF-16 reserved: 0, dataOffset: 32, dataLength: 32, name: 'QFid', data, }; } /** * Create Request Lease response context (stub) */ private createRequestLeaseContext(context: SMB2CreateContext, fileHandle: any): SMB2CreateContext { // Stub implementation - deny lease request const data = Buffer.alloc(32); data.fill(0); // All zeros = no lease granted return { next: 0, nameOffset: 16, nameLength: 8, // "RqLs".length * 2 for UTF-16 reserved: 0, dataOffset: 32, dataLength: 32, name: 'RqLs', data, }; } /** * Create Durable Handle response context (stub) */ private createDurableHandleContext(context: SMB2CreateContext, fileHandle: any): SMB2CreateContext { // Stub implementation - deny durable handle request const data = Buffer.alloc(16); data.fill(0); // All zeros = no durable handle granted return { next: 0, nameOffset: 16, nameLength: context.name.length * 2, // UTF-16 length reserved: 0, dataOffset: 32, dataLength: 16, name: context.name, data, }; } /** * Calculate total length of create contexts */ private calculateContextsLength(contexts: SMB2CreateContext[]): number { let totalLength = 0; for (const context of contexts) { totalLength += 16; // Context header totalLength += context.name.length * 2; // UTF-16 name totalLength += context.dataLength; // Context data // Add padding to align to 8-byte boundary const padding = (8 - (totalLength % 8)) % 8; totalLength += padding; } return totalLength; } /** * Check if filename contains wildcards */ private containsWildcards(filename: string): boolean { return filename.includes('*') || filename.includes('?'); } /** * Check if create options are compatible */ private areCreateOptionsCompatible(options: CREATE_OPTIONS): boolean { // Check for incompatible combinations if ((options & CREATE_OPTIONS.DIRECTORY_FILE) && (options & CREATE_OPTIONS.NON_DIRECTORY_FILE)) { return false; } if ((options & CREATE_OPTIONS.SEQUENTIAL_ONLY) && (options & CREATE_OPTIONS.RANDOM_ACCESS)) { return false; } return true; } }