Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457/** * SMB2 Query Directory Command Handler * Handles SMB2_QUERY_DIRECTORY requests and responses for directory enumeration */
import type { SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2QueryDirectoryRequest, SMB2QueryDirectoryResponse, SMB2CommandHandler, FileDirectoryInformation,} from '../protocol/types.js';import { SMB2_STATUS, SMB2_HEADER_SIZE, SMB2_COMMANDS, FILE_INFORMATION_CLASS, ACCESS_MASK, 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, dateToFileTime } from '../utils/helpers.js';
export class SMB2QueryDirectoryHandler { 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('SMB2QueryDirectoryHandler'); }
public async handle( requestData: Buffer, connection: SMB2Connection, session?: SMB2Session, treeConnect?: SMB2TreeConnect, ): Promise<Buffer> { try { // Validate session and tree connect if (!session) { this.logger.warn('Query directory attempt without valid session'); throw new Error('No valid session'); }
if (!treeConnect) { this.logger.warn('Query directory attempt without valid tree connect'); throw new Error('No valid tree connect'); }
// Parse the query directory request const parseResult = this.parser.parseQueryDirectoryRequest(requestData); const request = parseResult.result;
const fileIdStr = fileIdToString(request.fileId); this.logger.debug(`Query directory request from ${connection.clientAddress}:`); this.logger.debug(` File ID: ${fileIdStr}`); this.logger.debug(` File information class: ${request.fileInformationClass}`); this.logger.debug(` Flags: 0x${request.flags.toString(16)}`); this.logger.debug(` File index: ${request.fileIndex}`); this.logger.debug(` Output buffer length: ${request.outputBufferLength}`); this.logger.debug(` File name: ${request.fileName}`);
// Validate request this.validateQueryDirectoryRequest(request, connection);
// Find the open directory const openFile = connection.openFiles.get(fileIdStr); if (!openFile) { this.logger.warn(`Attempted to query non-existent directory: ${fileIdStr}`); throw new Error('Directory not open'); }
// Verify the file belongs to the correct session and tree if (openFile.sessionId !== session.sessionId) { this.logger.warn(`Directory ${fileIdStr} does not belong to session ${session.sessionId}`); throw new Error('Invalid session for directory'); }
if (openFile.treeConnectId !== treeConnect.treeConnectId) { this.logger.warn(`Directory ${fileIdStr} does not belong to tree ${treeConnect.treeConnectId}`); throw new Error('Invalid tree connect for directory'); }
// Check read access if (!(openFile.desiredAccess & ACCESS_MASK.FILE_READ_DATA)) { this.logger.warn(`Read access denied for directory ${fileIdStr}`); throw new Error('Read access denied'); }
// Validate directory access this.validateDirectoryAccess(request, openFile);
try { // Get directory entries from filesystem provider const pattern = request.fileName || '*'; const entries = await this.fileSystemProvider.queryDirectory( request.fileId, pattern );
// Format entries according to information class const outputBuffer = await this.formatDirectoryEntries( entries, request.fileInformationClass, request.flags, request.fileIndex, request.outputBufferLength );
// Create response const response: SMB2QueryDirectoryResponse = { structureSize: 9, outputBufferOffset: outputBuffer.length > 0 ? SMB2_HEADER_SIZE + 9 : 0, outputBufferLength: outputBuffer.length, outputBuffer, };
// Serialize the response const responseBuffer = this.serializer.serializeQueryDirectoryResponse(response);
this.logger.debug(`Query directory successful: ${entries.length} entries returned for ${openFile.fileName}`); return responseBuffer;
} catch (error) { this.logger.error(`Error querying directory ${fileIdStr}:`, error); throw error; }
} catch (error) { this.logger.error('Error handling query directory request:', error); throw error; } }
/** * Validate query directory request parameters */ private validateQueryDirectoryRequest( request: SMB2QueryDirectoryRequest, connection: SMB2Connection ): void { if (request.structureSize !== 33) { 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 information class if (!this.isValidInformationClass(request.fileInformationClass)) { throw new Error(`Unsupported file information class: ${request.fileInformationClass}`); }
// Validate output buffer length if (request.outputBufferLength === 0) { throw new Error('Output buffer length cannot be zero'); }
const maxBufferSize = connection.maxTransactSize || 65536; if (request.outputBufferLength > maxBufferSize) { throw new Error('Output buffer length exceeds maximum allowed size'); }
// Validate flags const validFlags = 0x01 | 0x02 | 0x04 | 0x08; // Known flags if ((request.flags & ~validFlags) !== 0) { this.logger.warn(`Unknown query directory flags: 0x${request.flags.toString(16)}`); } }
/** * Validate directory access */ private validateDirectoryAccess(request: SMB2QueryDirectoryRequest, openFile: any): void { // Verify this is actually a directory if (!openFile.isDirectory) { throw new Error('Not a directory'); }
// Additional validation can be added here this.logger.debug(`Querying directory: ${openFile.fileName}`); }
/** * Format directory entries according to information class */ private async formatDirectoryEntries( entries: any[], informationClass: FILE_INFORMATION_CLASS, flags: number, fileIndex: number, maxBufferLength: number ): Promise<Buffer> { const buffers: Buffer[] = []; let totalLength = 0; let entryIndex = 0;
// Check if this is a restart scan or continuation const restartScan = !!(flags & 0x01); // SMB2_REOPEN const returnSingleEntry = !!(flags & 0x02); // SMB2_RETURN_SINGLE_ENTRY const indexSpecified = !!(flags & 0x04); // SMB2_INDEX_SPECIFIED
let startIndex = 0; if (indexSpecified) { startIndex = fileIndex; } else if (!restartScan) { // Continue from where we left off (would be tracked per handle) startIndex = 0; }
// Process entries starting from the specified index for (let i = startIndex; i < entries.length; i++) { const entry = entries[i]; let entryBuffer: Buffer;
try { // Format entry according to information class entryBuffer = await this.formatDirectoryEntry( entry, informationClass, entryIndex ); } catch (error) { this.logger.warn(`Error formatting directory entry ${entry.name}:`, error); continue; // Skip this entry }
// Check if adding this entry would exceed buffer limit if (totalLength + entryBuffer.length > maxBufferLength) { if (buffers.length === 0) { // First entry is too large - this shouldn't happen with reasonable buffer sizes this.logger.warn(`Single directory entry too large: ${entryBuffer.length} bytes`); break; } // Stop adding entries break; }
buffers.push(entryBuffer); totalLength += entryBuffer.length; entryIndex++;
// If single entry requested, stop after first entry if (returnSingleEntry) { break; } }
// Concatenate all entry buffers return Buffer.concat(buffers); }
/** * Format a single directory entry */ private async formatDirectoryEntry( entry: any, informationClass: FILE_INFORMATION_CLASS, entryIndex: number ): Promise<Buffer> { switch (informationClass) { case FILE_INFORMATION_CLASS.FileDirectoryInformation: return this.formatFileDirectoryInformation(entry, entryIndex); case FILE_INFORMATION_CLASS.FileFullDirectoryInformation: return this.formatFileFullDirectoryInformation(entry, entryIndex); case FILE_INFORMATION_CLASS.FileBothDirectoryInformation: return this.formatFileBothDirectoryInformation(entry, entryIndex); case FILE_INFORMATION_CLASS.FileIdBothDirectoryInformation: return this.formatFileIdBothDirectoryInformation(entry, entryIndex); case FILE_INFORMATION_CLASS.FileIdFullDirectoryInformation: return this.formatFileIdFullDirectoryInformation(entry, entryIndex); default: throw new Error(`Unsupported information class: ${informationClass}`); } }
/** * Format FileDirectoryInformation entry */ private formatFileDirectoryInformation(entry: any, entryIndex: number): Buffer { const fileNameBytes = Buffer.from(entry.name, 'utf16le'); const baseSize = 64; const totalSize = baseSize + fileNameBytes.length; const buffer = Buffer.alloc(totalSize);
let offset = 0; buffer.writeUInt32LE(0, offset); offset += 4; // NextEntryOffset (filled later) buffer.writeUInt32LE(entryIndex, offset); offset += 4; // FileIndex buffer.writeBigUInt64LE(dateToFileTime(entry.creationTime), offset); offset += 8; // CreationTime buffer.writeBigUInt64LE(dateToFileTime(entry.lastAccessTime), offset); offset += 8; // LastAccessTime buffer.writeBigUInt64LE(dateToFileTime(entry.lastWriteTime), offset); offset += 8; // LastWriteTime buffer.writeBigUInt64LE(dateToFileTime(entry.changeTime), offset); offset += 8; // ChangeTime buffer.writeBigUInt64LE(entry.fileSize, offset); offset += 8; // EndOfFile buffer.writeBigUInt64LE(entry.fileSize, offset); offset += 8; // AllocationSize (simplified) buffer.writeUInt32LE(entry.fileAttributes, offset); offset += 4; // FileAttributes buffer.writeUInt32LE(fileNameBytes.length, offset); offset += 4; // FileNameLength
// Copy filename fileNameBytes.copy(buffer, offset);
return buffer; }
/** * Format FileFullDirectoryInformation entry (stub) */ private formatFileFullDirectoryInformation(entry: any, entryIndex: number): Buffer { // Similar to FileDirectoryInformation but with EA size field const directoryInfo = this.formatFileDirectoryInformation(entry, entryIndex); const buffer = Buffer.alloc(directoryInfo.length + 4); directoryInfo.copy(buffer, 0); buffer.writeUInt32LE(0, directoryInfo.length); // EaSize return buffer; }
/** * Format FileBothDirectoryInformation entry (stub) */ private formatFileBothDirectoryInformation(entry: any, entryIndex: number): Buffer { // Similar to FileFullDirectoryInformation but with short name const fullInfo = this.formatFileFullDirectoryInformation(entry, entryIndex); const buffer = Buffer.alloc(fullInfo.length + 30); // 24 bytes for short name + length fields fullInfo.copy(buffer, 0); // Add short name fields (stubbed as empty) buffer.writeUInt8(0, fullInfo.length); // ShortNameLength buffer.writeUInt8(0, fullInfo.length + 1); // Reserved // ShortName field (24 bytes) - left as zeros return buffer; }
/** * Format FileIdBothDirectoryInformation entry (stub) */ private formatFileIdBothDirectoryInformation(entry: any, entryIndex: number): Buffer { // Similar to FileBothDirectoryInformation but with file ID const bothInfo = this.formatFileBothDirectoryInformation(entry, entryIndex); const buffer = Buffer.alloc(bothInfo.length + 16); // 8 bytes reserved + 8 bytes file ID bothInfo.copy(buffer, 0); buffer.writeBigUInt64LE(0n, bothInfo.length); // Reserved buffer.writeBigUInt64LE(BigInt(entryIndex), bothInfo.length + 8); // FileId (simplified) return buffer; }
/** * Format FileIdFullDirectoryInformation entry (stub) */ private formatFileIdFullDirectoryInformation(entry: any, entryIndex: number): Buffer { // Similar to FileFullDirectoryInformation but with file ID const fullInfo = this.formatFileFullDirectoryInformation(entry, entryIndex); const buffer = Buffer.alloc(fullInfo.length + 16); fullInfo.copy(buffer, 0); buffer.writeBigUInt64LE(0n, fullInfo.length); // Reserved buffer.writeBigUInt64LE(BigInt(entryIndex), fullInfo.length + 8); // FileId return buffer; }
/** * Check if information class is supported */ private isValidInformationClass(infoClass: number): boolean { const supportedClasses = [ FILE_INFORMATION_CLASS.FileDirectoryInformation, FILE_INFORMATION_CLASS.FileFullDirectoryInformation, FILE_INFORMATION_CLASS.FileBothDirectoryInformation, FILE_INFORMATION_CLASS.FileIdBothDirectoryInformation, FILE_INFORMATION_CLASS.FileIdFullDirectoryInformation, ]; return supportedClasses.includes(infoClass); }
/** * Get query directory statistics for monitoring */ public getQueryDirectoryStats(): { totalQueries: number; totalEntriesReturned: number; averageEntriesPerQuery: number; } { // This would be implemented to track query statistics // For now, return default values return { totalQueries: 0, totalEntriesReturned: 0, averageEntriesPerQuery: 0, }; }
/** * Handle wildcard pattern matching */ private matchesPattern(filename: string, pattern: string): boolean { if (pattern === '*') { return true; }
// Convert SMB wildcard pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.') .replace(/<"/g, '*') // SMB specific: <" means "zero or more characters, but not dot" .replace(/>/g, '?'); // SMB specific: > means "exactly one character, but not dot"
const regex = new RegExp(`^${regexPattern}$`, 'i'); return regex.test(filename); }
/** * Handle directory enumeration continuation */ private handleContinuation( request: SMB2QueryDirectoryRequest, openFile: any ): { startIndex: number; restartScan: boolean; } { const restartScan = !!(request.flags & 0x01); const indexSpecified = !!(request.flags & 0x04);
let startIndex = 0; if (indexSpecified) { startIndex = request.fileIndex; } else if (!restartScan) { // In a full implementation, this would track the continuation point per handle startIndex = 0; }
return { startIndex, restartScan }; }}