Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534/** * Helper utilities for SMB2 Server * Common utility functions used throughout the server */
import { randomBytes, createHash } from 'crypto';import { resolve, normalize, join } from 'path';import type { SMB2Header, SMB2FileId, SMB2Guid,} from '../protocol/types.js';import { SMB2_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_COMMANDS, SMB2_STATUS,} from '../protocol/constants.js';
/** * Generate a random SMB2 GUID */export function generateGuid(): SMB2Guid { const bytes = randomBytes(16); const hex = bytes.toString('hex').toUpperCase();
// Format as standard GUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX return [ hex.substring(0, 8), hex.substring(8, 12), hex.substring(12, 16), hex.substring(16, 20), hex.substring(20, 32), ].join('-');}
/** * Convert GUID string to bytes */export function guidToBytes(guid: SMB2Guid): Buffer { const hex = guid.replace(/-/g, ''); if (hex.length !== 32) { throw new Error('Invalid GUID format'); } return Buffer.from(hex, 'hex');}
/** * Convert bytes to GUID string */export function bytesToGuid(bytes: Buffer): SMB2Guid { if (bytes.length !== 16) { throw new Error('GUID must be exactly 16 bytes'); }
const hex = bytes.toString('hex').toUpperCase(); return [ hex.substring(0, 8), hex.substring(8, 12), hex.substring(12, 16), hex.substring(16, 20), hex.substring(20, 32), ].join('-');}
/** * Generate a random SMB2 File ID */export function generateFileId(): SMB2FileId { return { persistent: BigInt(`0x${randomBytes(8).toString('hex')}`), volatile: BigInt(`0x${randomBytes(8).toString('hex')}`), };}
/** * Convert File ID to string for use as map key */export function fileIdToString(fileId: SMB2FileId): string { return `${fileId.persistent.toString(16)}-${fileId.volatile.toString(16)}`;}
/** * Parse File ID from string */export function stringToFileId(str: string): SMB2FileId { const parts = str.split('-'); if (parts.length !== 2) { throw new Error('Invalid File ID string format'); }
return { persistent: BigInt(`0x${parts[0]}`), volatile: BigInt(`0x${parts[1]}`), };}
/** * Validate SMB2 header structure */export function validateSMB2Header(header: SMB2Header): boolean { // Check protocol ID if (header.protocolId !== SMB2_PROTOCOL_ID) { return false; }
// Check structure size if (header.structureSize !== SMB2_HEADER_SIZE) { return false; }
// Check if command is valid if (!Object.values(SMB2_COMMANDS).includes(header.command)) { return false; }
// Check signature length if (header.signature.length !== 16) { return false; }
return true;}
/** * Convert Windows FILETIME to JavaScript Date */export function fileTimeToDate(filetime: bigint): Date { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = Number(filetime / 10000n) - Number(FILETIME_EPOCH_DIFF); return new Date(timestamp);}
/** * Convert JavaScript Date to Windows FILETIME */export function dateToFileTime(date: Date): bigint { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = BigInt(date.getTime()) + FILETIME_EPOCH_DIFF; return timestamp * 10000n; // Convert to 100-nanosecond intervals}
/** * Get current time as FILETIME */export function getCurrentFileTime(): bigint { return dateToFileTime(new Date());}
/** * Pad buffer to specified alignment */export function padBuffer(buffer: Buffer, alignment: number): Buffer { const padding = (alignment - (buffer.length % alignment)) % alignment; if (padding === 0) { return buffer; }
const paddingBuffer = Buffer.alloc(padding); return Buffer.concat([buffer, paddingBuffer]);}
/** * Calculate padding needed for alignment */export function calculatePadding(length: number, alignment: number): number { return (alignment - (length % alignment)) % alignment;}
/** * Convert UTF-16LE string to UTF-8 */export function utf16leToUtf8(buffer: Buffer): string { return buffer.toString('utf16le');}
/** * Convert UTF-8 string to UTF-16LE buffer */export function utf8ToUtf16le(str: string): Buffer { return Buffer.from(str, 'utf16le');}
/** * Normalize SMB path (convert backslashes to forward slashes) */export function normalizeSmbPath(path: string): string { return path.replace(/\\/g, '/').replace(/\/+/g, '/');}
/** * Convert SMB path to local file system path */export function smbPathToLocal(smbPath: string, shareRoot: string): string { const normalized = normalizeSmbPath(smbPath); const relativePath = normalized.startsWith('/') ? normalized.substring(1) : normalized; return resolve(shareRoot, relativePath);}
/** * Convert local file system path to SMB path */export function localPathToSmb(localPath: string, shareRoot: string): string { const relativePath = localPath.replace(shareRoot, '').replace(/\\/g, '/'); return relativePath.startsWith('/') ? relativePath : `/${relativePath}`;}
/** * Validate SMB path for security (prevent directory traversal) */export function validateSmbPath(path: string): boolean { const normalized = normalize(path);
// Check for directory traversal attempts if (normalized.includes('..') || normalized.includes('//')) { return false; }
// Check for invalid characters (basic validation) const invalidChars = /[<>:"|?*\x00-\x1f]/; if (invalidChars.test(normalized)) { return false; }
return true;}
/** * Extract filename from SMB path */export function extractFilename(path: string): string { const normalized = normalizeSmbPath(path); const parts = normalized.split('/'); return parts[parts.length - 1] || '';}
/** * Extract directory from SMB path */export function extractDirectory(path: string): string { const normalized = normalizeSmbPath(path); const parts = normalized.split('/'); parts.pop(); // Remove filename return parts.join('/') || '/';}
/** * Join SMB paths */export function joinSmbPaths(...paths: string[]): string { return normalizeSmbPath(paths.join('/'));}
/** * Check if buffer contains only zeros */export function isZeroBuffer(buffer: Buffer): boolean { return buffer.every(byte => byte === 0);}
/** * Generate random bytes of specified length */export function generateRandomBytes(length: number): Buffer { return randomBytes(length);}
/** * Calculate MD5 hash of data */export function calculateMd5(data: Buffer | string): string { return createHash('md5').update(data).digest('hex');}
/** * Calculate SHA256 hash of data */export function calculateSha256(data: Buffer | string): string { return createHash('sha256').update(data).digest('hex');}
/** * Compare two buffers in constant time (to prevent timing attacks) */export function constantTimeCompare(a: Buffer, b: Buffer): boolean { if (!a || !b || a.length !== b.length || a.length === 0 || b.length === 0) { return false; }
let result = 0; for (let i = 0; i < a.length; i++) { result |= a[i] ^ b[i]; }
return result === 0;}
/** * Format bytes as human readable string */export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B';
const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;}
/** * Format duration in milliseconds as human readable string */export function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; return `${(ms / 3600000).toFixed(1)}h`;}
/** * Sleep for specified milliseconds */export function sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms));}
/** * Create a timeout promise that rejects after specified time */export function timeout<T>(promise: Promise<T>, ms: number): Promise<T> { const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error('Timeout')), ms); });
return Promise.race([promise, timeoutPromise]);}
/** * Retry a function with exponential backoff */export async function retry<T>( fn: () => Promise<T>, maxAttempts: number = 3, baseDelay: number = 1000): Promise<T> { let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error;
if (attempt === maxAttempts) { throw lastError; }
const delay = baseDelay * Math.pow(2, attempt - 1); await sleep(delay); } }
throw lastError!;}
/** * Clamp a number between min and max values */export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max);}
/** * Check if a value is null or undefined */export function isNullOrUndefined(value: any): value is null | undefined { return value === null || value === undefined;}
/** * Deep clone an object (simple implementation) */export function deepClone<T>(obj: T): T { if (obj === null || typeof obj !== 'object') { return obj; }
if (obj instanceof Date) { return new Date(obj.getTime()) as any; }
if (obj instanceof Buffer) { return Buffer.from(obj) as any; }
if (Array.isArray(obj)) { return obj.map(item => deepClone(item)) as any; }
const cloned = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } }
return cloned;}
/** * Safe JSON parse that returns null on error */export function safeJsonParse<T = any>(str: string): T | null { try { return JSON.parse(str); } catch { return null; }}
/** * Safe JSON stringify that returns empty string on error */export function safeJsonStringify(obj: any): string { try { return JSON.stringify(obj); } catch { return ''; }}
/** * Get environment variable with default value */export function getEnvVar(name: string, defaultValue: string = ''): string { return process.env[name] ?? defaultValue;}
/** * Get environment variable as number with default value */export function getEnvNumber(name: string, defaultValue: number = 0): number { const value = process.env[name]; if (!value) return defaultValue;
const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed;}
/** * Get environment variable as boolean with default value */export function getEnvBoolean(name: string, defaultValue: boolean = false): boolean { const value = process.env[name]; if (!value) return defaultValue;
return value.toLowerCase() === 'true' || value === '1';}
/** * Debounce function calls */export function debounce<T extends (...args: any[]) => any>( func: T, delay: number): (...args: Parameters<T>) => void { let timeoutId: NodeJS.Timeout;
return (...args: Parameters<T>) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); };}
/** * Throttle function calls */export function throttle<T extends (...args: any[]) => any>( func: T, limit: number): (...args: Parameters<T>) => void { let inThrottle: boolean;
return (...args: Parameters<T>) => { if (!inThrottle) { func.apply(null, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } };}
/** * Create a simple event emitter */export class SimpleEventEmitter<T extends Record<string, any[]>> { private listeners: Map<keyof T, Array<(...args: any[]) => void>> = new Map();
on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(listener); }
off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void { const eventListeners = this.listeners.get(event); if (eventListeners) { const index = eventListeners.indexOf(listener); if (index > -1) { eventListeners.splice(index, 1); } } }
emit<K extends keyof T>(event: K, ...args: T[K]): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.forEach(listener => listener(...args)); } }
removeAllListeners<K extends keyof T>(event?: K): void { if (event) { this.listeners.delete(event); } else { this.listeners.clear(); } }}