Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454/** * 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<Buffer> { 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<SMB2CreateContext[]> { 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<SMB2CreateContext | null> { 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; }}