Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629/** * SMB2/3 Server Implementation * Main server class that handles connections, sessions, and message routing */
import { EventEmitter } from 'events';import { createServer, Server as NetServer, Socket } from 'net';import type { SMB2ServerConfig, SMB2Connection, SMB2Session, SMB2TreeConnect, SMB2Message, SMB2Header, SMB2Event, SMB2EventHandler, SMB2CommandHandler, SMB2EventType, SMB2FileId, SMB2Guid,} from './protocol/types.js';import { SMB2_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_COMMANDS, SMB2_STATUS, SMB2_FLAGS, SMB2_DIALECTS, DEFAULT_MAX_TRANSACT_SIZE, DEFAULT_MAX_READ_SIZE, DEFAULT_MAX_WRITE_SIZE, NETWORK_TIMEOUT, SESSION_TIMEOUT,} from './protocol/constants.js';import { SMB2MessageParser } from './protocol/parser.js';import { SMB2MessageSerializer } from './protocol/serializer.js';import { SMB2NegotiateHandler } from './commands/negotiate.js';import { SMB2SessionSetupHandler } from './commands/session-setup.js';import { SMB2TreeConnectHandler } from './commands/tree-connect.js';import { SMB2CreateHandler } from './commands/create';import { SMB2CloseHandler } from './commands/close.js';import { SMB2ReadHandler } from './commands/read.js';import { SMB2WriteHandler } from './commands/write.js';import { SMB2QueryDirectoryHandler } from './commands/query-directory.js';import { SMB2QueryInfoHandler } from './commands/query-info.js';import { SMB2SetInfoHandler } from './commands/set-info.js';import { SMB2EchoHandler } from './commands/echo.js';import { SMB2LogoffHandler } from './commands/logoff.js';import { SMB2TreeDisconnectHandler } from './commands/tree-disconnect.js';import { SMB2AuthenticationManager } from './auth/authentication.js';import { SMB2FileSystemProvider } from './filesystem/provider.js';import { Logger } from './utils/logger.js';import { generateGuid, validateSMB2Header } from './utils/helpers.js';
export class SMB2Server extends EventEmitter { private readonly config: SMB2ServerConfig; private readonly logger: Logger; private readonly parser: SMB2MessageParser; private readonly serializer: SMB2MessageSerializer; private readonly authManager: SMB2AuthenticationManager; private readonly fileSystemProvider: SMB2FileSystemProvider; private readonly commandHandlers: Map<SMB2_COMMANDS, SMB2CommandHandler>; private readonly connections: Map<string, SMB2Connection>; private readonly sessions: Map<bigint, SMB2Session>; private readonly treeConnects: Map<number, SMB2TreeConnect>; private readonly asyncContexts: Map<bigint, any>;
private server: NetServer | null = null; private isRunning = false; private connectionIdCounter = 0; private sessionIdCounter = 1n; private treeIdCounter = 1; private messageIdCounter = 1n; private asyncIdCounter = 1n;
constructor(config: SMB2ServerConfig) { super(); this.config = { ...this.getDefaultConfig(), ...config, }; this.logger = new Logger('SMB2Server'); this.parser = new SMB2MessageParser(); this.serializer = new SMB2MessageSerializer(); this.authManager = new SMB2AuthenticationManager(this.config); this.fileSystemProvider = new SMB2FileSystemProvider(this.config); this.commandHandlers = new Map(); this.connections = new Map(); this.sessions = new Map(); this.treeConnects = new Map(); this.asyncContexts = new Map();
this.setupCommandHandlers(); this.setupEventHandlers(); }
private getDefaultConfig(): Partial<SMB2ServerConfig> { return { serverGuid: generateGuid(), serverName: 'SMB2-Server', supportedDialects: [ SMB2_DIALECTS.SMB2_0_2, SMB2_DIALECTS.SMB2_1_0, SMB2_DIALECTS.SMB3_0_0, SMB2_DIALECTS.SMB3_0_2, SMB2_DIALECTS.SMB3_1_1, ], requireMessageSigning: false, enableEncryption: false, maxConnections: 100, maxSessionsPerConnection: 16, sessionTimeout: SESSION_TIMEOUT, maxTransactSize: DEFAULT_MAX_TRANSACT_SIZE, maxReadSize: DEFAULT_MAX_READ_SIZE, maxWriteSize: DEFAULT_MAX_WRITE_SIZE, shares: [], users: [], }; }
private setupCommandHandlers(): void { this.commandHandlers.set(SMB2_COMMANDS.NEGOTIATE, new SMB2NegotiateHandler(this.config)); this.commandHandlers.set(SMB2_COMMANDS.SESSION_SETUP, new SMB2SessionSetupHandler(this.authManager)); this.commandHandlers.set(SMB2_COMMANDS.TREE_CONNECT, new SMB2TreeConnectHandler(this.config)); this.commandHandlers.set(SMB2_COMMANDS.CREATE, new SMB2CreateHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.CLOSE, new SMB2CloseHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.READ, new SMB2ReadHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.WRITE, new SMB2WriteHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.QUERY_DIRECTORY, new SMB2QueryDirectoryHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.QUERY_INFO, new SMB2QueryInfoHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.SET_INFO, new SMB2SetInfoHandler(this.fileSystemProvider)); this.commandHandlers.set(SMB2_COMMANDS.ECHO, new SMB2EchoHandler()); this.commandHandlers.set(SMB2_COMMANDS.LOGOFF, new SMB2LogoffHandler()); this.commandHandlers.set(SMB2_COMMANDS.TREE_DISCONNECT, new SMB2TreeDisconnectHandler()); }
private setupEventHandlers(): void { // Handle cleanup on process termination process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); process.on('uncaughtException', (error) => { this.logger.error('Uncaught exception:', error); this.emitEvent('error', { error }); }); process.on('unhandledRejection', (reason) => { this.logger.error('Unhandled rejection:', reason); this.emitEvent('error', { error: reason }); }); }
public async start(port: number = 445, hostname?: string): Promise<void> { if (this.isRunning) { throw new Error('Server is already running'); }
return new Promise((resolve, reject) => { this.server = createServer((socket) => { this.handleConnection(socket); });
this.server.on('error', (error) => { this.logger.error('Server error:', error); this.emitEvent('error', { error }); reject(error); });
this.server.on('listening', () => { this.isRunning = true; const address = this.server?.address(); this.logger.info(`SMB2 Server listening on ${hostname ?? '0.0.0.0'}:${port}`); this.logger.info(`Server GUID: ${this.config.serverGuid}`); this.logger.info(`Supported dialects: ${this.config.supportedDialects.map(d => `0x${d.toString(16).toUpperCase().padStart(4, '0')}`).join(', ')}`); resolve(); });
this.server.listen(port, hostname); }); }
public async shutdown(): Promise<void> { if (!this.isRunning || !this.server) { return; }
this.logger.info('Shutting down SMB2 Server...'); this.isRunning = false;
// Close all connections gracefully const closePromises = Array.from(this.connections.values()).map(connection => this.closeConnection(connection.connectionId, 'Server shutdown') );
await Promise.all(closePromises);
// Close the server return new Promise((resolve) => { this.server?.close(() => { this.logger.info('SMB2 Server shut down'); resolve(); }); }); }
private handleConnection(socket: Socket): void { const connectionId = `conn_${++this.connectionIdCounter}`; const clientAddress = `${socket.remoteAddress}:${socket.remotePort}`;
const connection: SMB2Connection = { connectionId, clientAddress, serverAddress: `${socket.localAddress}:${socket.localPort}`, dialect: SMB2_DIALECTS.SMB2_0_2, // Will be negotiated supportsMultiCredit: false, maxTransactSize: this.config.maxTransactSize, maxReadSize: this.config.maxReadSize, maxWriteSize: this.config.maxWriteSize, requireMessageSigning: this.config.requireMessageSigning, clientGuid: '', creationTime: new Date(), lastActivityTime: new Date(), sessions: new Map(), treeConnects: new Map(), openFiles: new Map(), };
this.connections.set(connectionId, connection); this.logger.info(`New connection established: ${connectionId} from ${clientAddress}`); this.emitEvent('connection:established', { connection });
// Set up socket event handlers socket.setTimeout(NETWORK_TIMEOUT); socket.setKeepAlive(true, 30000); socket.setNoDelay(true);
let buffer = Buffer.alloc(0);
socket.on('data', async (data) => { try { buffer = Buffer.concat([buffer, data]); buffer = await this.processMessages(buffer, connection); connection.lastActivityTime = new Date(); } catch (error) { this.logger.error(`Error processing data from ${connectionId}:`, error); this.emitEvent('error', { connectionId, error }); this.closeConnection(connectionId, 'Protocol error'); } });
socket.on('error', (error) => { this.logger.error(`Socket error for ${connectionId}:`, error); this.emitEvent('error', { connectionId, error }); });
socket.on('timeout', () => { this.logger.warn(`Socket timeout for ${connectionId}`); this.closeConnection(connectionId, 'Timeout'); });
socket.on('close', () => { this.logger.info(`Connection closed: ${connectionId}`); this.closeConnection(connectionId, 'Client disconnected'); });
// Store the socket for later use (connection as any).socket = socket; }
private async processMessages(buffer: Buffer, connection: SMB2Connection): Promise<Buffer> { let offset = 0;
while (offset + SMB2_HEADER_SIZE <= buffer.length) { try { // Parse the SMB2 header first to get message length const headerResult = this.parser.parseHeader(buffer, offset); const header = headerResult.result;
// Validate the header if (!validateSMB2Header(header)) { throw new Error('Invalid SMB2 header'); }
// Calculate total message size const messageSize = this.calculateMessageSize(header, buffer, offset);
// Check if we have the complete message if (offset + messageSize > buffer.length) { break; // Wait for more data }
// Extract the complete message const messageBuffer = buffer.subarray(offset, offset + messageSize); const message: SMB2Message = { header, data: messageBuffer.subarray(SMB2_HEADER_SIZE), };
// Process the message await this.processMessage(message, connection);
offset += messageSize;
// Handle compound messages if (header.nextCommand > 0) { offset = Math.max(offset, header.nextCommand); } } catch (error) { this.logger.error('Error parsing message:', JSON.stringify(error)); // Send error response if possible await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER); break; } }
return buffer.subarray(offset); }
private calculateMessageSize(header: SMB2Header, buffer: Buffer, offset: number): number { // For most commands, we need to read the structure size from the payload // For now, we'll use a simple heuristic based on the command type const baseSize = SMB2_HEADER_SIZE;
if (header.command === SMB2_COMMANDS.NEGOTIATE) { return baseSize + this.calculateNegotiateSize(buffer, offset + baseSize); }
// For other commands, use next command offset or default size if (header.nextCommand > 0) { return header.nextCommand; }
// Default minimum message size return Math.min(baseSize + 512, buffer.length - offset); }
private calculateNegotiateSize(buffer: Buffer, offset: number): number { if (offset + 36 > buffer.length) return 0;
const structureSize = buffer.readUInt16LE(offset); const dialectCount = buffer.readUInt16LE(offset + 2);
return structureSize + (dialectCount * 2); }
private async processMessage(message: SMB2Message, connection: SMB2Connection): Promise<void> { const { header } = message;
try { // Find the command handler const handler = this.commandHandlers.get(header.command); if (!handler) { this.logger.warn(`Unknown command: 0x${header.command.toString(16).toUpperCase()}`); await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER, header); return; }
// Get session and tree connect if applicable const session = header.sessionId !== 0n ? this.sessions.get(header.sessionId) : undefined; const treeConnect = header.treeId !== 0 ? this.treeConnects.get(header.treeId) : undefined;
// Call the handler const responseData = await handler(message.data, connection, session, treeConnect);
// Send the response await this.sendResponse(connection, header, SMB2_STATUS.SUCCESS, responseData);
} catch (error) { this.logger.error(`Error processing ${SMB2_COMMANDS[header.command]} command:`, error); await this.sendErrorResponse(connection, SMB2_STATUS.INVALID_PARAMETER, header); } }
private async sendResponse( connection: SMB2Connection, requestHeader: SMB2Header, status: SMB2_STATUS, data: Buffer ): Promise<void> { const responseHeader: SMB2Header = { protocolId: SMB2_PROTOCOL_ID, structureSize: SMB2_HEADER_SIZE, creditCharge: requestHeader.creditCharge, status, command: requestHeader.command, creditRequestResponse: 1, // Grant 1 credit flags: requestHeader.flags | SMB2_FLAGS.SERVER_TO_REDIR, nextCommand: 0, messageId: requestHeader.messageId, reserved: 0, treeId: requestHeader.treeId, sessionId: requestHeader.sessionId, signature: Buffer.alloc(16), // TODO: Implement signing };
const headerBuffer = this.serializer.serializeHeader(responseHeader); const responseBuffer = Buffer.concat([headerBuffer, data]);
await this.sendToConnection(connection, responseBuffer); }
private async sendErrorResponse( connection: SMB2Connection, status: SMB2_STATUS, requestHeader?: SMB2Header ): Promise<void> { const errorData = Buffer.alloc(8); // Empty error data errorData.writeUInt16LE(8, 0); // Structure size errorData.writeUInt16LE(0, 2); // Reserved errorData.writeUInt32LE(0, 4); // Byte count
const responseHeader: SMB2Header = { protocolId: SMB2_PROTOCOL_ID, structureSize: SMB2_HEADER_SIZE, creditCharge: requestHeader?.creditCharge ?? 1, status, command: requestHeader?.command ?? SMB2_COMMANDS.NEGOTIATE, creditRequestResponse: 1, flags: (requestHeader?.flags ?? 0) | SMB2_FLAGS.SERVER_TO_REDIR, nextCommand: 0, messageId: requestHeader?.messageId ?? this.messageIdCounter++, reserved: 0, treeId: requestHeader?.treeId ?? 0, sessionId: requestHeader?.sessionId ?? 0n, signature: Buffer.alloc(16), };
const headerBuffer = this.serializer.serializeHeader(responseHeader); const responseBuffer = Buffer.concat([headerBuffer, errorData]);
await this.sendToConnection(connection, responseBuffer); }
private async sendToConnection(connection: SMB2Connection, data: Buffer): Promise<void> { const socket = (connection as any).socket as Socket; if (socket && !socket.destroyed) { return new Promise((resolve, reject) => { socket.write(data, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } }
private async closeConnection(connectionId: string, reason: string): Promise<void> { const connection = this.connections.get(connectionId); if (!connection) return;
this.logger.info(`Closing connection ${connectionId}: ${reason}`);
// Close all sessions for this connection for (const session of connection.sessions.values()) { await this.closeSession(session.sessionId, reason); }
// Close all tree connects for this connection for (const treeConnect of connection.treeConnects.values()) { await this.closeTreeConnect(treeConnect.treeConnectId, reason); }
// Close the socket const socket = (connection as any).socket as Socket; if (socket && !socket.destroyed) { socket.destroy(); }
// Remove the connection this.connections.delete(connectionId); this.emitEvent('connection:closed', { connectionId, reason }); }
private async closeSession(sessionId: bigint, reason: string): Promise<void> { const session = this.sessions.get(sessionId); if (!session) return;
this.logger.debug(`Closing session ${sessionId}: ${reason}`); this.sessions.delete(sessionId); this.emitEvent('session:closed', { sessionId, reason }); }
private async closeTreeConnect(treeConnectId: number, reason: string): Promise<void> { const treeConnect = this.treeConnects.get(treeConnectId); if (!treeConnect) return;
this.logger.debug(`Closing tree connect ${treeConnectId}: ${reason}`); this.treeConnects.delete(treeConnectId); this.emitEvent('tree:disconnected', { treeConnectId, reason }); }
private emitEvent<T = unknown>(type: SMB2EventType, data: T): void { const event: SMB2Event<T> = { type, timestamp: new Date(), data, };
this.emit(type, event); this.emit('event', event); }
// Public API methods public onEvent<T = unknown>(type: SMB2EventType, handler: SMB2EventHandler<T>): void { this.on(type, handler); }
public offEvent<T = unknown>(type: SMB2EventType, handler: SMB2EventHandler<T>): void { this.off(type, handler); }
public getConnections(): SMB2Connection[] { return Array.from(this.connections.values()); }
public getConnection(connectionId: string): SMB2Connection | undefined { return this.connections.get(connectionId); }
public getSessions(): SMB2Session[] { return Array.from(this.sessions.values()); }
public getSession(sessionId: bigint): SMB2Session | undefined { return this.sessions.get(sessionId); }
public getTreeConnects(): SMB2TreeConnect[] { return Array.from(this.treeConnects.values()); }
public getTreeConnect(treeConnectId: number): SMB2TreeConnect | undefined { return this.treeConnects.get(treeConnectId); }
public getStats(): { connections: number; sessions: number; treeConnects: number; uptime: number; } { return { connections: this.connections.size, sessions: this.sessions.size, treeConnects: this.treeConnects.size, uptime: this.isRunning ? Date.now() - this.getServerStartTime() : 0, }; }
private getServerStartTime(): number { // This would be set when the server starts return Date.now(); // Placeholder }
public isServerRunning(): boolean { return this.isRunning; }
public getConfig(): SMB2ServerConfig { return { ...this.config }; }
// Session management methods public createSession(connectionId: string, userId: string): SMB2Session { const sessionId = this.sessionIdCounter++; const connection = this.connections.get(connectionId); if (!connection) { throw new Error(`Connection ${connectionId} not found`); }
const session: SMB2Session = { sessionId, userId, isGuest: false, isAnonymous: false, creationTime: new Date(), lastActivityTime: new Date(), signingRequired: this.config.requireMessageSigning, encryptionRequired: this.config.enableEncryption, preauthIntegrityHashValue: Buffer.alloc(64), sessionKey: Buffer.alloc(32), signKey: Buffer.alloc(16), };
this.sessions.set(sessionId, session); connection.sessions.set(sessionId, session); this.emitEvent('session:established', { sessionId, connectionId, userId });
return session; }
public createTreeConnect(sessionId: bigint, shareName: string): SMB2TreeConnect { const treeConnectId = this.treeIdCounter++; const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); }
const shareConfig = this.config.shares.find(s => s.name === shareName); if (!shareConfig) { throw new Error(`Share ${shareName} not found`); }
const treeConnect: SMB2TreeConnect = { treeConnectId, sessionId, shareName, shareType: shareConfig.shareType, shareFlags: shareConfig.shareFlags, maximalAccess: shareConfig.maximalAccess, encryptData: shareConfig.encryptData, creationTime: new Date(), lastActivityTime: new Date(), };
this.treeConnects.set(treeConnectId, treeConnect); this.emitEvent('tree:connected', { treeConnectId, sessionId, shareName });
return treeConnect; }
public generateFileId(): SMB2FileId { return { persistent: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)), volatile: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)), }; }}