2025-08-18 21:45:50 -04:00
class EnhancedSecureFileTransfer {
constructor ( webrtcManager , onProgress , onComplete , onError , onFileReceived ) {
this . webrtcManager = webrtcManager ;
this . onProgress = onProgress ;
this . onComplete = onComplete ;
this . onError = onError ;
this . onFileReceived = onFileReceived ;
// Validate webrtcManager
if ( ! webrtcManager ) {
throw new Error ( 'webrtcManager is required for EnhancedSecureFileTransfer' ) ;
}
2025-08-20 18:19:42 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Устанавливаем глобальный флаг
window . FILE _TRANSFER _ACTIVE = true ;
window . fileTransferSystem = this ;
2025-08-21 17:40:17 -04:00
2025-08-18 21:45:50 -04:00
// Transfer settings
2025-08-21 04:07:16 -04:00
// Размер чанка по умолчанию (баланс нагрузки и стабильности очереди)
this . CHUNK _SIZE = 64 * 1024 ; // 64 KB
2025-08-18 21:45:50 -04:00
this . MAX _FILE _SIZE = 100 * 1024 * 1024 ; // 100 MB limit
this . MAX _CONCURRENT _TRANSFERS = 3 ;
this . CHUNK _TIMEOUT = 30000 ; // 30 seconds per chunk
this . RETRY _ATTEMPTS = 3 ;
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Система ограничений по типам файлов
this . FILE _TYPE _RESTRICTIONS = {
// Документы
documents : {
extensions : [ '.pdf' , '.doc' , '.docx' , '.txt' , '.md' , '.rtf' , '.odt' ] ,
mimeTypes : [
'application/pdf' ,
'application/msword' ,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
'text/plain' ,
'text/markdown' ,
'application/rtf' ,
'application/vnd.oasis.opendocument.text'
] ,
maxSize : 50 * 1024 * 1024 , // 50 MB
category : 'Documents' ,
description : 'PDF, DOC, TXT, MD, RTF, ODT'
} ,
// Изображения
images : {
extensions : [ '.jpg' , '.jpeg' , '.png' , '.gif' , '.webp' , '.bmp' , '.svg' , '.ico' ] ,
mimeTypes : [
'image/jpeg' ,
'image/png' ,
'image/gif' ,
'image/webp' ,
'image/bmp' ,
'image/svg+xml' ,
'image/x-icon'
] ,
maxSize : 25 * 1024 * 1024 , // 25 MB
category : 'Images' ,
description : 'JPG, PNG, GIF, WEBP, BMP, SVG, ICO'
} ,
// Архивы
archives : {
extensions : [ '.zip' , '.rar' , '.7z' , '.tar' , '.gz' , '.bz2' , '.xz' ] ,
mimeTypes : [
'application/zip' ,
'application/x-rar-compressed' ,
'application/x-7z-compressed' ,
'application/x-tar' ,
'application/gzip' ,
'application/x-bzip2' ,
'application/x-xz'
] ,
maxSize : 100 * 1024 * 1024 , // 100 MB
category : 'Archives' ,
description : 'ZIP, RAR, 7Z, TAR, GZ, BZ2, XZ'
} ,
// Медиа файлы
media : {
extensions : [ '.mp3' , '.mp4' , '.avi' , '.mkv' , '.mov' , '.wmv' , '.flv' , '.webm' , '.ogg' , '.wav' ] ,
mimeTypes : [
'audio/mpeg' ,
'video/mp4' ,
'video/x-msvideo' ,
'video/x-matroska' ,
'video/quicktime' ,
'video/x-ms-wmv' ,
'video/x-flv' ,
'video/webm' ,
'audio/ogg' ,
'audio/wav'
] ,
maxSize : 100 * 1024 * 1024 , // 100 MB
category : 'Media' ,
description : 'MP3, MP4, AVI, MKV, MOV, WMV, FLV, WEBM, OGG, WAV'
} ,
// Общие файлы (любые другие типы)
general : {
extensions : [ ] , // Пустой массив означает "все остальные"
mimeTypes : [ ] , // Пустой массив означает "все остальные"
maxSize : 50 * 1024 * 1024 , // 50 MB
category : 'General' ,
description : 'Any file type up to size limits'
}
} ;
2025-08-18 21:45:50 -04:00
// Active transfers tracking
this . activeTransfers = new Map ( ) ; // fileId -> transfer state
this . receivingTransfers = new Map ( ) ; // fileId -> receiving state
this . transferQueue = [ ] ; // Queue for pending transfers
this . pendingChunks = new Map ( ) ;
2025-08-18 23:56:10 -04:00
// Session key derivation
2025-08-18 21:45:50 -04:00
this . sessionKeys = new Map ( ) ; // fileId -> derived session key
// Security
this . processedChunks = new Set ( ) ; // Prevent replay attacks
this . transferNonces = new Map ( ) ; // fileId -> current nonce counter
2025-08-21 04:07:16 -04:00
this . receivedFileBuffers = new Map ( ) ; // fileId -> { buffer:ArrayBuffer, type:string, name:string, size:number }
2025-08-18 21:45:50 -04:00
2025-08-20 18:19:42 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Регистрируем обработчик сообщений
2025-08-18 21:45:50 -04:00
this . setupFileMessageHandlers ( ) ;
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Устанавливаем ссылку в WebRTC менеджере
if ( this . webrtcManager ) {
this . webrtcManager . fileTransferSystem = this ;
}
}
// ============================================
// FILE TYPE VALIDATION SYSTEM
// ============================================
// Определяем тип файла по расширению и MIME типу
getFileType ( file ) {
const fileName = file . name . toLowerCase ( ) ;
const fileExtension = fileName . substring ( fileName . lastIndexOf ( '.' ) ) ;
const mimeType = file . type . toLowerCase ( ) ;
// Проверяем каждый тип файла
for ( const [ typeKey , typeConfig ] of Object . entries ( this . FILE _TYPE _RESTRICTIONS ) ) {
if ( typeKey === 'general' ) continue ; // Пропускаем общий тип
// Проверяем расширение
if ( typeConfig . extensions . includes ( fileExtension ) ) {
return {
type : typeKey ,
category : typeConfig . category ,
description : typeConfig . description ,
maxSize : typeConfig . maxSize ,
allowed : true
} ;
}
// Проверяем MIME тип
if ( typeConfig . mimeTypes . includes ( mimeType ) ) {
return {
type : typeKey ,
category : typeConfig . category ,
description : typeConfig . description ,
maxSize : typeConfig . maxSize ,
allowed : true
} ;
}
}
// Если не найден в специфических типах, используем общий
const generalConfig = this . FILE _TYPE _RESTRICTIONS . general ;
return {
type : 'general' ,
category : generalConfig . category ,
description : generalConfig . description ,
maxSize : generalConfig . maxSize ,
allowed : true
} ;
}
// Проверяем, разрешен ли файл для передачи
validateFile ( file ) {
const fileType = this . getFileType ( file ) ;
const errors = [ ] ;
// Проверяем размер файла
if ( file . size > fileType . maxSize ) {
errors . push ( ` File size ( ${ this . formatFileSize ( file . size ) } ) exceeds maximum allowed for ${ fileType . category } ( ${ this . formatFileSize ( fileType . maxSize ) } ) ` ) ;
}
// Проверяем, разрешен ли тип файла
if ( ! fileType . allowed ) {
errors . push ( ` File type not allowed. Supported types: ${ fileType . description } ` ) ;
}
// Проверяем общий лимит размера
if ( file . size > this . MAX _FILE _SIZE ) {
errors . push ( ` File size ( ${ this . formatFileSize ( file . size ) } ) exceeds general limit ( ${ this . formatFileSize ( this . MAX _FILE _SIZE ) } ) ` ) ;
}
return {
isValid : errors . length === 0 ,
errors : errors ,
fileType : fileType ,
fileSize : file . size ,
formattedSize : this . formatFileSize ( file . size )
} ;
}
// Форматируем размер файла для отображения
formatFileSize ( bytes ) {
if ( bytes === 0 ) return '0 B' ;
const k = 1024 ;
const sizes = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
}
// Получаем список поддерживаемых типов файлов
getSupportedFileTypes ( ) {
const supportedTypes = { } ;
for ( const [ typeKey , typeConfig ] of Object . entries ( this . FILE _TYPE _RESTRICTIONS ) ) {
if ( typeKey === 'general' ) continue ;
supportedTypes [ typeKey ] = {
category : typeConfig . category ,
description : typeConfig . description ,
extensions : typeConfig . extensions ,
maxSize : this . formatFileSize ( typeConfig . maxSize ) ,
maxSizeBytes : typeConfig . maxSize
} ;
}
return supportedTypes ;
}
// Получаем общую информацию о поддерживаемых типах
getFileTypeInfo ( ) {
return {
supportedTypes : this . getSupportedFileTypes ( ) ,
generalMaxSize : this . formatFileSize ( this . MAX _FILE _SIZE ) ,
generalMaxSizeBytes : this . MAX _FILE _SIZE ,
restrictions : this . FILE _TYPE _RESTRICTIONS
} ;
2025-08-18 21:45:50 -04:00
}
2025-08-21 04:07:16 -04:00
// ============================================
// ENCODING HELPERS (Base64 for efficient transport)
// ============================================
arrayBufferToBase64 ( buffer ) {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array ( buffer ) ;
let binary = '' ;
const len = bytes . byteLength ;
for ( let i = 0 ; i < len ; i ++ ) {
binary += String . fromCharCode ( bytes [ i ] ) ;
}
return btoa ( binary ) ;
}
base64ToUint8Array ( base64 ) {
const binaryString = atob ( base64 ) ;
const len = binaryString . length ;
const bytes = new Uint8Array ( len ) ;
for ( let i = 0 ; i < len ; i ++ ) {
bytes [ i ] = binaryString . charCodeAt ( i ) ;
}
return bytes ;
}
// ============================================
// PUBLIC ACCESSORS FOR RECEIVED FILES
// ============================================
getReceivedFileMeta ( fileId ) {
const entry = this . receivedFileBuffers . get ( fileId ) ;
if ( ! entry ) return null ;
return { fileId , fileName : entry . name , fileSize : entry . size , mimeType : entry . type } ;
}
async getBlob ( fileId ) {
const entry = this . receivedFileBuffers . get ( fileId ) ;
if ( ! entry ) return null ;
return new Blob ( [ entry . buffer ] , { type : entry . type } ) ;
}
async getObjectURL ( fileId ) {
const blob = await this . getBlob ( fileId ) ;
if ( ! blob ) return null ;
return URL . createObjectURL ( blob ) ;
}
revokeObjectURL ( url ) {
try { URL . revokeObjectURL ( url ) ; } catch ( _ ) { }
}
2025-08-20 18:19:42 -04:00
// ============================================
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ - ОБРАБОТКА СООБЩЕНИЙ
// ============================================
setupFileMessageHandlers ( ) {
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Ждем готовности dataChannel
if ( ! this . webrtcManager . dataChannel ) {
// Попытаемся настроить через небольшой интервал
const setupRetry = setInterval ( ( ) => {
if ( this . webrtcManager . dataChannel ) {
clearInterval ( setupRetry ) ;
this . setupMessageInterception ( ) ;
}
} , 100 ) ;
// Timeout для предотвращения бесконечного ожидания
setTimeout ( ( ) => {
clearInterval ( setupRetry ) ;
} , 5000 ) ;
return ;
}
// Если dataChannel уже готов, сразу настраиваем
this . setupMessageInterception ( ) ;
}
// В методе setupMessageInterception(), замените весь метод на:
setupMessageInterception ( ) {
try {
if ( ! this . webrtcManager . dataChannel ) {
return ;
}
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Глобальный флаг для блокировки файловых сообщений
window . FILE _TRANSFER _ACTIVE = true ;
window . fileTransferSystem = this ;
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Устанавливаем ссылку в WebRTC менеджере
if ( this . webrtcManager ) {
this . webrtcManager . fileTransferSystem = this ;
}
2025-08-20 18:19:42 -04:00
// 1. ПЕРЕХВАТ НА УРОВНЕ dataChannel.onmessage
if ( this . webrtcManager . dataChannel . onmessage ) {
this . originalOnMessage = this . webrtcManager . dataChannel . onmessage ;
}
this . webrtcManager . dataChannel . onmessage = async ( event ) => {
try {
// Проверяем файловые сообщения ПЕРВЫМИ
if ( typeof event . data === 'string' ) {
try {
const parsed = JSON . parse ( event . data ) ;
if ( this . isFileTransferMessage ( parsed ) ) {
await this . handleFileMessage ( parsed ) ;
return ; // КРИТИЧЕСКИ ВАЖНО: НЕ передаем дальше
}
} catch ( parseError ) {
// Не JSON - передаем оригинальному обработчику
}
}
// Передаем обычные сообщения оригинальному обработчику
if ( this . originalOnMessage ) {
return this . originalOnMessage . call ( this . webrtcManager . dataChannel , event ) ;
}
} catch ( error ) {
console . error ( '❌ Error in file system message interception:' , error ) ;
if ( this . originalOnMessage ) {
return this . originalOnMessage . call ( this . webrtcManager . dataChannel , event ) ;
}
}
} ;
} catch ( error ) {
console . error ( '❌ Failed to set up message interception:' , error ) ;
}
}
// Проверяем, является ли сообщение файловым
isFileTransferMessage ( message ) {
if ( ! message || typeof message !== 'object' || ! message . type ) {
return false ;
}
const fileMessageTypes = [
'file_transfer_start' ,
'file_transfer_response' ,
'file_chunk' ,
'chunk_confirmation' ,
'file_transfer_complete' ,
'file_transfer_error'
] ;
2025-08-21 17:40:17 -04:00
return fileMessageTypes . includes ( message . type ) ;
2025-08-20 18:19:42 -04:00
}
// Обрабатываем файловые сообщения
async handleFileMessage ( message ) {
try {
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Проверяем готовность файловой системы
if ( ! this . webrtcManager . fileTransferSystem ) {
try {
// Попытка инициализации файловой системы
if ( typeof this . webrtcManager . initializeFileTransfer === 'function' ) {
this . webrtcManager . initializeFileTransfer ( ) ;
// Ждем инициализации
let attempts = 0 ;
const maxAttempts = 50 ; // 5 секунд максимум
while ( ! this . webrtcManager . fileTransferSystem && attempts < maxAttempts ) {
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
attempts ++ ;
}
if ( ! this . webrtcManager . fileTransferSystem ) {
throw new Error ( 'File transfer system initialization timeout' ) ;
}
} else {
throw new Error ( 'initializeFileTransfer method not available' ) ;
}
} catch ( initError ) {
console . error ( '❌ Failed to initialize file transfer system:' , initError ) ;
// Отправляем ошибку отправителю
if ( message . fileId ) {
const errorMessage = {
type : 'file_transfer_error' ,
fileId : message . fileId ,
error : 'File transfer system not available' ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
}
return ;
}
}
2025-08-20 18:19:42 -04:00
switch ( message . type ) {
case 'file_transfer_start' :
await this . handleFileTransferStart ( message ) ;
break ;
case 'file_transfer_response' :
this . handleTransferResponse ( message ) ;
break ;
case 'file_chunk' :
await this . handleFileChunk ( message ) ;
break ;
case 'chunk_confirmation' :
this . handleChunkConfirmation ( message ) ;
break ;
case 'file_transfer_complete' :
this . handleTransferComplete ( message ) ;
break ;
case 'file_transfer_error' :
this . handleTransferError ( message ) ;
break ;
default :
console . warn ( '⚠️ Unknown file message type:' , message . type ) ;
}
} catch ( error ) {
console . error ( '❌ Error handling file message:' , error ) ;
// Отправляем сообщение об ошибке
if ( message . fileId ) {
const errorMessage = {
type : 'file_transfer_error' ,
fileId : message . fileId ,
error : error . message ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
}
}
}
2025-08-18 21:45:50 -04:00
// ============================================
2025-08-18 23:56:10 -04:00
// SIMPLIFIED KEY DERIVATION - USE SHARED DATA
2025-08-18 21:45:50 -04:00
// ============================================
2025-08-18 23:56:10 -04:00
async deriveFileSessionKey ( fileId ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-18 23:56:10 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем keyFingerprint и sessionSalt
// которые уже согласованы между пирами
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'WebRTC session data not available' ) ;
2025-08-18 21:45:50 -04:00
}
2025-08-18 23:56:10 -04:00
// Генерируем соль для этого конкретного файла
const fileSalt = crypto . getRandomValues ( new Uint8Array ( 32 ) ) ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Создаем seed из согласованных данных
const encoder = new TextEncoder ( ) ;
const fingerprintData = encoder . encode ( this . webrtcManager . keyFingerprint ) ;
const fileIdData = encoder . encode ( fileId ) ;
// Объединяем все компоненты для создания уникального seed
const sessionSaltArray = new Uint8Array ( this . webrtcManager . sessionSalt ) ;
const combinedSeed = new Uint8Array (
fingerprintData . length +
sessionSaltArray . length +
fileSalt . length +
fileIdData . length
) ;
2025-08-18 21:45:50 -04:00
let offset = 0 ;
2025-08-18 23:56:10 -04:00
combinedSeed . set ( fingerprintData , offset ) ;
offset += fingerprintData . length ;
combinedSeed . set ( sessionSaltArray , offset ) ;
offset += sessionSaltArray . length ;
combinedSeed . set ( fileSalt , offset ) ;
offset += fileSalt . length ;
combinedSeed . set ( fileIdData , offset ) ;
// Хешируем для получения ключевого материала
const keyMaterial = await crypto . subtle . digest ( 'SHA-256' , combinedSeed ) ;
// Импортируем как AES ключ напрямую
const fileSessionKey = await crypto . subtle . importKey (
2025-08-18 21:45:50 -04:00
'raw' ,
2025-08-18 23:56:10 -04:00
keyMaterial ,
{ name : 'AES-GCM' } ,
2025-08-18 21:45:50 -04:00
false ,
[ 'encrypt' , 'decrypt' ]
) ;
2025-08-18 23:56:10 -04:00
// Сохраняем ключ и соль
2025-08-18 21:45:50 -04:00
this . sessionKeys . set ( fileId , {
key : fileSessionKey ,
2025-08-18 23:56:10 -04:00
salt : Array . from ( fileSalt ) ,
2025-08-18 21:45:50 -04:00
created : Date . now ( )
} ) ;
2025-08-18 23:56:10 -04:00
return { key : fileSessionKey , salt : Array . from ( fileSalt ) } ;
2025-08-18 21:45:50 -04:00
} catch ( error ) {
console . error ( '❌ Failed to derive file session key:' , error ) ;
throw error ;
}
}
2025-08-18 23:56:10 -04:00
async deriveFileSessionKeyFromSalt ( fileId , saltArray ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-18 23:56:10 -04:00
// Проверка соли
if ( ! saltArray || ! Array . isArray ( saltArray ) || saltArray . length !== 32 ) {
throw new Error ( ` Invalid salt: ${ saltArray ? . length || 0 } bytes ` ) ;
2025-08-18 21:45:50 -04:00
}
2025-08-18 23:56:10 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'WebRTC session data not available' ) ;
}
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Используем тот же процесс что и отправитель
const encoder = new TextEncoder ( ) ;
const fingerprintData = encoder . encode ( this . webrtcManager . keyFingerprint ) ;
const fileIdData = encoder . encode ( fileId ) ;
// Используем полученную соль файла
const fileSalt = new Uint8Array ( saltArray ) ;
const sessionSaltArray = new Uint8Array ( this . webrtcManager . sessionSalt ) ;
// Объединяем компоненты в том же порядке
const combinedSeed = new Uint8Array (
fingerprintData . length +
sessionSaltArray . length +
fileSalt . length +
fileIdData . length
2025-08-18 21:45:50 -04:00
) ;
2025-08-18 23:56:10 -04:00
let offset = 0 ;
combinedSeed . set ( fingerprintData , offset ) ;
offset += fingerprintData . length ;
combinedSeed . set ( sessionSaltArray , offset ) ;
offset += sessionSaltArray . length ;
combinedSeed . set ( fileSalt , offset ) ;
offset += fileSalt . length ;
combinedSeed . set ( fileIdData , offset ) ;
// Хешируем для получения того же ключевого материала
const keyMaterial = await crypto . subtle . digest ( 'SHA-256' , combinedSeed ) ;
// Импортируем как AES ключ
const fileSessionKey = await crypto . subtle . importKey (
'raw' ,
keyMaterial ,
{ name : 'AES-GCM' } ,
2025-08-18 21:45:50 -04:00
false ,
[ 'encrypt' , 'decrypt' ]
) ;
this . sessionKeys . set ( fileId , {
key : fileSessionKey ,
salt : saltArray ,
created : Date . now ( )
} ) ;
return fileSessionKey ;
} catch ( error ) {
console . error ( '❌ Failed to derive session key from salt:' , error ) ;
throw error ;
}
}
// ============================================
// FILE TRANSFER IMPLEMENTATION
// ============================================
async sendFile ( file ) {
try {
// Validate webrtcManager
if ( ! this . webrtcManager ) {
throw new Error ( 'WebRTC Manager not initialized' ) ;
}
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Валидация файла с новой системой типов
2025-08-18 21:45:50 -04:00
if ( ! file || ! file . size ) {
throw new Error ( 'Invalid file object' ) ;
}
2025-08-21 17:40:17 -04:00
// Проверяем тип и размер файла
const validation = this . validateFile ( file ) ;
if ( ! validation . isValid ) {
const errorMessage = validation . errors . join ( '. ' ) ;
throw new Error ( errorMessage ) ;
2025-08-18 21:45:50 -04:00
}
if ( this . activeTransfers . size >= this . MAX _CONCURRENT _TRANSFERS ) {
throw new Error ( 'Maximum concurrent transfers reached' ) ;
}
// Generate unique file ID
const fileId = ` file_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
// Calculate file hash for integrity verification
const fileHash = await this . calculateFileHash ( file ) ;
2025-08-18 23:56:10 -04:00
// Derive session key for this file
const keyResult = await this . deriveFileSessionKey ( fileId ) ;
2025-08-18 21:45:50 -04:00
const sessionKey = keyResult . key ;
const salt = keyResult . salt ;
// Create transfer state
const transferState = {
fileId : fileId ,
file : file ,
fileHash : fileHash ,
sessionKey : sessionKey ,
salt : salt , // Сохраняем соль для отправки
totalChunks : Math . ceil ( file . size / this . CHUNK _SIZE ) ,
sentChunks : 0 ,
confirmedChunks : 0 ,
startTime : Date . now ( ) ,
status : 'preparing' ,
retryCount : 0 ,
lastChunkTime : Date . now ( )
} ;
this . activeTransfers . set ( fileId , transferState ) ;
this . transferNonces . set ( fileId , 0 ) ;
// Send file metadata first
await this . sendFileMetadata ( transferState ) ;
// Start chunk transmission
await this . startChunkTransmission ( transferState ) ;
return fileId ;
} catch ( error ) {
console . error ( '❌ File sending failed:' , error ) ;
if ( this . onError ) this . onError ( error . message ) ;
throw error ;
}
}
async sendFileMetadata ( transferState ) {
try {
const metadata = {
type : 'file_transfer_start' ,
fileId : transferState . fileId ,
fileName : transferState . file . name ,
fileSize : transferState . file . size ,
fileType : transferState . file . type || 'application/octet-stream' ,
fileHash : transferState . fileHash ,
totalChunks : transferState . totalChunks ,
chunkSize : this . CHUNK _SIZE ,
salt : transferState . salt , // Отправляем соль получателю
timestamp : Date . now ( ) ,
2025-08-18 23:56:10 -04:00
version : '2.0'
2025-08-18 21:45:50 -04:00
} ;
// Send metadata through secure channel
await this . sendSecureMessage ( metadata ) ;
transferState . status = 'metadata_sent' ;
} catch ( error ) {
console . error ( '❌ Failed to send file metadata:' , error ) ;
transferState . status = 'failed' ;
throw error ;
}
}
async startChunkTransmission ( transferState ) {
try {
transferState . status = 'transmitting' ;
const file = transferState . file ;
const totalChunks = transferState . totalChunks ;
for ( let chunkIndex = 0 ; chunkIndex < totalChunks ; chunkIndex ++ ) {
const start = chunkIndex * this . CHUNK _SIZE ;
const end = Math . min ( start + this . CHUNK _SIZE , file . size ) ;
// Read chunk from file
const chunkData = await this . readFileChunk ( file , start , end ) ;
2025-08-21 04:07:16 -04:00
// Send chunk (с учётом backpressure)
2025-08-18 21:45:50 -04:00
await this . sendFileChunk ( transferState , chunkIndex , chunkData ) ;
// Update progress
transferState . sentChunks ++ ;
const progress = Math . round ( ( transferState . sentChunks / totalChunks ) * 95 ) + 5 ; // 5-100%
2025-08-21 04:07:16 -04:00
// Backpressure: ждём разгрузки очереди перед следующим чанком
await this . waitForBackpressure ( ) ;
2025-08-18 21:45:50 -04:00
}
transferState . status = 'waiting_confirmation' ;
// Timeout for completion confirmation
setTimeout ( ( ) => {
if ( this . activeTransfers . has ( transferState . fileId ) ) {
const state = this . activeTransfers . get ( transferState . fileId ) ;
if ( state . status === 'waiting_confirmation' ) {
this . cleanupTransfer ( transferState . fileId ) ;
}
}
} , 30000 ) ;
} catch ( error ) {
console . error ( '❌ Chunk transmission failed:' , error ) ;
transferState . status = 'failed' ;
throw error ;
}
}
async readFileChunk ( file , start , end ) {
try {
const blob = file . slice ( start , end ) ;
return await blob . arrayBuffer ( ) ;
} catch ( error ) {
console . error ( '❌ Failed to read file chunk:' , error ) ;
throw error ;
}
}
async sendFileChunk ( transferState , chunkIndex , chunkData ) {
try {
const sessionKey = transferState . sessionKey ;
const nonce = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
// Encrypt chunk data
const encryptedChunk = await crypto . subtle . encrypt (
{
name : 'AES-GCM' ,
iv : nonce
} ,
sessionKey ,
chunkData
) ;
2025-08-21 04:07:16 -04:00
// Use Base64 to drastically reduce JSON overhead
const encryptedB64 = this . arrayBufferToBase64 ( new Uint8Array ( encryptedChunk ) ) ;
2025-08-18 21:45:50 -04:00
const chunkMessage = {
type : 'file_chunk' ,
fileId : transferState . fileId ,
chunkIndex : chunkIndex ,
totalChunks : transferState . totalChunks ,
nonce : Array . from ( nonce ) ,
2025-08-21 04:07:16 -04:00
encryptedDataB64 : encryptedB64 ,
2025-08-18 21:45:50 -04:00
chunkSize : chunkData . byteLength ,
timestamp : Date . now ( )
} ;
2025-08-21 04:07:16 -04:00
// Перед отправкой проверяем backpressure (доп. защита)
await this . waitForBackpressure ( ) ;
2025-08-18 21:45:50 -04:00
// Send chunk through secure channel
await this . sendSecureMessage ( chunkMessage ) ;
} catch ( error ) {
console . error ( '❌ Failed to send file chunk:' , error ) ;
throw error ;
}
}
async sendSecureMessage ( message ) {
2025-08-21 04:07:16 -04:00
// ВАЖНО: отправляем напрямую в DataChannel, чтобы file_* и chunk_confirmation
// приходили верхнего уровня и перехватывались файловой системой, без обёртки type: 'message'
const messageString = JSON . stringify ( message ) ;
const dc = this . webrtcManager ? . dataChannel ;
const maxRetries = 10 ;
let attempt = 0 ;
const wait = ( ms ) => new Promise ( r => setTimeout ( r , ms ) ) ;
while ( true ) {
try {
if ( ! dc || dc . readyState !== 'open' ) {
throw new Error ( 'Data channel not ready' ) ;
}
await this . waitForBackpressure ( ) ;
dc . send ( messageString ) ;
return ; // success
} catch ( error ) {
const msg = String ( error ? . message || '' ) ;
const queueFull = msg . includes ( 'send queue is full' ) || msg . includes ( 'bufferedAmount' ) ;
const opErr = error ? . name === 'OperationError' ;
if ( ( queueFull || opErr ) && attempt < maxRetries ) {
attempt ++ ;
await this . waitForBackpressure ( ) ;
await wait ( Math . min ( 50 * attempt , 500 ) ) ;
continue ;
}
console . error ( '❌ Failed to send secure message:' , error ) ;
throw error ;
}
}
}
async waitForBackpressure ( ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-21 04:07:16 -04:00
const dc = this . webrtcManager ? . dataChannel ;
if ( ! dc ) return ;
if ( typeof dc . bufferedAmountLowThreshold === 'number' ) {
// Если буфер превышает порог — ждём события снижения
if ( dc . bufferedAmount > dc . bufferedAmountLowThreshold ) {
await new Promise ( resolve => {
const handler = ( ) => {
dc . removeEventListener ( 'bufferedamountlow' , handler ) ;
resolve ( ) ;
} ;
dc . addEventListener ( 'bufferedamountlow' , handler , { once : true } ) ;
} ) ;
}
return ;
2025-08-18 21:45:50 -04:00
}
2025-08-21 04:07:16 -04:00
// Фолбэк: опрашиваем bufferedAmount и ждём пока не упадёт ниже 4MB
const softLimit = 4 * 1024 * 1024 ;
while ( dc . bufferedAmount > softLimit ) {
await new Promise ( r => setTimeout ( r , 20 ) ) ;
}
} catch ( _ ) {
// ignore
2025-08-18 21:45:50 -04:00
}
}
async calculateFileHash ( file ) {
try {
const arrayBuffer = await file . arrayBuffer ( ) ;
2025-08-18 23:56:10 -04:00
const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , arrayBuffer ) ;
2025-08-18 21:45:50 -04:00
const hashArray = Array . from ( new Uint8Array ( hashBuffer ) ) ;
return hashArray . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
} catch ( error ) {
console . error ( '❌ File hash calculation failed:' , error ) ;
throw error ;
}
}
// ============================================
// MESSAGE HANDLERS
// ============================================
async handleFileTransferStart ( metadata ) {
try {
// Validate metadata
if ( ! metadata . fileId || ! metadata . fileName || ! metadata . fileSize ) {
throw new Error ( 'Invalid file transfer metadata' ) ;
}
// Check if we already have this transfer
if ( this . receivingTransfers . has ( metadata . fileId ) ) {
return ;
}
2025-08-18 23:56:10 -04:00
// Derive session key from salt
2025-08-18 21:45:50 -04:00
const sessionKey = await this . deriveFileSessionKeyFromSalt (
2025-08-18 23:56:10 -04:00
metadata . fileId ,
2025-08-18 21:45:50 -04:00
metadata . salt
) ;
// Create receiving transfer state
const receivingState = {
fileId : metadata . fileId ,
fileName : metadata . fileName ,
fileSize : metadata . fileSize ,
fileType : metadata . fileType || 'application/octet-stream' ,
fileHash : metadata . fileHash ,
totalChunks : metadata . totalChunks ,
chunkSize : metadata . chunkSize || this . CHUNK _SIZE ,
sessionKey : sessionKey ,
2025-08-18 23:56:10 -04:00
salt : metadata . salt ,
2025-08-18 21:45:50 -04:00
receivedChunks : new Map ( ) ,
receivedCount : 0 ,
startTime : Date . now ( ) ,
lastChunkTime : Date . now ( ) ,
status : 'receiving'
} ;
this . receivingTransfers . set ( metadata . fileId , receivingState ) ;
// Send acceptance response
const response = {
type : 'file_transfer_response' ,
fileId : metadata . fileId ,
accepted : true ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( response ) ;
// Process buffered chunks if any
if ( this . pendingChunks . has ( metadata . fileId ) ) {
const bufferedChunks = this . pendingChunks . get ( metadata . fileId ) ;
for ( const [ chunkIndex , chunkMessage ] of bufferedChunks . entries ( ) ) {
await this . handleFileChunk ( chunkMessage ) ;
}
this . pendingChunks . delete ( metadata . fileId ) ;
}
} catch ( error ) {
console . error ( '❌ Failed to handle file transfer start:' , error ) ;
// Send error response
2025-08-18 23:56:10 -04:00
const errorResponse = {
type : 'file_transfer_response' ,
fileId : metadata . fileId ,
accepted : false ,
error : error . message ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorResponse ) ;
2025-08-18 21:45:50 -04:00
}
}
async handleFileChunk ( chunkMessage ) {
try {
let receivingState = this . receivingTransfers . get ( chunkMessage . fileId ) ;
// Buffer early chunks if transfer not yet initialized
if ( ! receivingState ) {
if ( ! this . pendingChunks . has ( chunkMessage . fileId ) ) {
this . pendingChunks . set ( chunkMessage . fileId , new Map ( ) ) ;
}
this . pendingChunks . get ( chunkMessage . fileId ) . set ( chunkMessage . chunkIndex , chunkMessage ) ;
return ;
}
// Update last chunk time
receivingState . lastChunkTime = Date . now ( ) ;
// Check if chunk already received
if ( receivingState . receivedChunks . has ( chunkMessage . chunkIndex ) ) {
return ;
}
// Validate chunk
if ( chunkMessage . chunkIndex < 0 || chunkMessage . chunkIndex >= receivingState . totalChunks ) {
throw new Error ( ` Invalid chunk index: ${ chunkMessage . chunkIndex } ` ) ;
}
2025-08-18 23:56:10 -04:00
// Decrypt chunk
2025-08-18 21:45:50 -04:00
const nonce = new Uint8Array ( chunkMessage . nonce ) ;
2025-08-21 04:07:16 -04:00
// Backward compatible: prefer Base64, fallback to numeric array
let encryptedData ;
if ( chunkMessage . encryptedDataB64 ) {
encryptedData = this . base64ToUint8Array ( chunkMessage . encryptedDataB64 ) ;
} else if ( chunkMessage . encryptedData ) {
encryptedData = new Uint8Array ( chunkMessage . encryptedData ) ;
} else {
throw new Error ( 'Missing encrypted data' ) ;
}
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
const decryptedChunk = await crypto . subtle . decrypt (
{
name : 'AES-GCM' ,
iv : nonce
} ,
receivingState . sessionKey ,
encryptedData
) ;
2025-08-18 21:45:50 -04:00
// Verify chunk size
if ( decryptedChunk . byteLength !== chunkMessage . chunkSize ) {
throw new Error ( ` Chunk size mismatch: expected ${ chunkMessage . chunkSize } , got ${ decryptedChunk . byteLength } ` ) ;
}
// Store chunk
receivingState . receivedChunks . set ( chunkMessage . chunkIndex , decryptedChunk ) ;
receivingState . receivedCount ++ ;
// Send chunk confirmation
const confirmation = {
type : 'chunk_confirmation' ,
fileId : chunkMessage . fileId ,
chunkIndex : chunkMessage . chunkIndex ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( confirmation ) ;
// Check if all chunks received
if ( receivingState . receivedCount === receivingState . totalChunks ) {
await this . assembleFile ( receivingState ) ;
}
} catch ( error ) {
console . error ( '❌ Failed to handle file chunk:' , error ) ;
// Send error notification
2025-08-18 23:56:10 -04:00
const errorMessage = {
type : 'file_transfer_error' ,
fileId : chunkMessage . fileId ,
error : error . message ,
chunkIndex : chunkMessage . chunkIndex ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
// Mark transfer as failed
const receivingState = this . receivingTransfers . get ( chunkMessage . fileId ) ;
if ( receivingState ) {
receivingState . status = 'failed' ;
}
if ( this . onError ) {
this . onError ( ` Chunk processing failed: ${ error . message } ` ) ;
2025-08-18 21:45:50 -04:00
}
}
}
async assembleFile ( receivingState ) {
try {
receivingState . status = 'assembling' ;
// Verify we have all chunks
for ( let i = 0 ; i < receivingState . totalChunks ; i ++ ) {
if ( ! receivingState . receivedChunks . has ( i ) ) {
throw new Error ( ` Missing chunk ${ i } ` ) ;
}
}
// Combine all chunks in order
const chunks = [ ] ;
for ( let i = 0 ; i < receivingState . totalChunks ; i ++ ) {
const chunk = receivingState . receivedChunks . get ( i ) ;
chunks . push ( new Uint8Array ( chunk ) ) ;
}
// Calculate total size
const totalSize = chunks . reduce ( ( sum , chunk ) => sum + chunk . length , 0 ) ;
// Verify total size matches expected
if ( totalSize !== receivingState . fileSize ) {
throw new Error ( ` File size mismatch: expected ${ receivingState . fileSize } , got ${ totalSize } ` ) ;
}
// Combine into single array
const fileData = new Uint8Array ( totalSize ) ;
let offset = 0 ;
for ( const chunk of chunks ) {
fileData . set ( chunk , offset ) ;
offset += chunk . length ;
}
// Verify file integrity
const receivedHash = await this . calculateFileHashFromData ( fileData ) ;
if ( receivedHash !== receivingState . fileHash ) {
throw new Error ( 'File integrity check failed - hash mismatch' ) ;
}
2025-08-21 04:07:16 -04:00
// Lazy: храним буфер, но для совместимости формируем Blob для onFileReceived
const fileBuffer = fileData . buffer ;
const fileBlob = new Blob ( [ fileBuffer ] , { type : receivingState . fileType } ) ;
2025-08-18 21:45:50 -04:00
receivingState . endTime = Date . now ( ) ;
receivingState . status = 'completed' ;
2025-08-21 04:07:16 -04:00
// Сохраняем в кэше до запроса скачивания
this . receivedFileBuffers . set ( receivingState . fileId , {
buffer : fileBuffer ,
type : receivingState . fileType ,
name : receivingState . fileName ,
size : receivingState . fileSize
} ) ;
// Сообщаем UI о готовности файла и даём ленивые методы получения
2025-08-18 21:45:50 -04:00
if ( this . onFileReceived ) {
2025-08-21 04:07:16 -04:00
const getBlob = async ( ) => new Blob ( [ this . receivedFileBuffers . get ( receivingState . fileId ) . buffer ] , { type : receivingState . fileType } ) ;
const getObjectURL = async ( ) => {
const blob = await getBlob ( ) ;
return URL . createObjectURL ( blob ) ;
} ;
const revokeObjectURL = ( url ) => {
try { URL . revokeObjectURL ( url ) ; } catch ( _ ) { }
} ;
2025-08-18 21:45:50 -04:00
this . onFileReceived ( {
fileId : receivingState . fileId ,
fileName : receivingState . fileName ,
fileSize : receivingState . fileSize ,
2025-08-21 04:07:16 -04:00
mimeType : receivingState . fileType ,
transferTime : receivingState . endTime - receivingState . startTime ,
// backward-compatibility for existing UIs
fileBlob ,
getBlob ,
getObjectURL ,
revokeObjectURL
2025-08-18 21:45:50 -04:00
} ) ;
}
// Send completion confirmation
const completionMessage = {
type : 'file_transfer_complete' ,
fileId : receivingState . fileId ,
success : true ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( completionMessage ) ;
// Cleanup
2025-08-21 04:07:16 -04:00
// Не удаляем буфер сразу, оставляем до загрузки пользователем
// Очистим метаданные чанков, оставив итоговый буфер
if ( this . receivingTransfers . has ( receivingState . fileId ) ) {
const rs = this . receivingTransfers . get ( receivingState . fileId ) ;
if ( rs && rs . receivedChunks ) rs . receivedChunks . clear ( ) ;
}
this . receivingTransfers . delete ( receivingState . fileId ) ;
2025-08-18 21:45:50 -04:00
} catch ( error ) {
console . error ( '❌ File assembly failed:' , error ) ;
receivingState . status = 'failed' ;
if ( this . onError ) {
this . onError ( ` File assembly failed: ${ error . message } ` ) ;
}
// Send error notification
2025-08-18 23:56:10 -04:00
const errorMessage = {
type : 'file_transfer_complete' ,
fileId : receivingState . fileId ,
success : false ,
error : error . message ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
2025-08-18 21:45:50 -04:00
// Cleanup failed transfer
this . cleanupReceivingTransfer ( receivingState . fileId ) ;
}
}
async calculateFileHashFromData ( data ) {
try {
2025-08-18 23:56:10 -04:00
const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , data ) ;
2025-08-18 21:45:50 -04:00
const hashArray = Array . from ( new Uint8Array ( hashBuffer ) ) ;
return hashArray . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
} catch ( error ) {
console . error ( '❌ Hash calculation failed:' , error ) ;
throw error ;
}
}
handleTransferResponse ( response ) {
try {
const transferState = this . activeTransfers . get ( response . fileId ) ;
if ( ! transferState ) {
return ;
}
if ( response . accepted ) {
transferState . status = 'accepted' ;
} else {
transferState . status = 'rejected' ;
if ( this . onError ) {
this . onError ( ` Transfer rejected: ${ response . error || 'Unknown reason' } ` ) ;
}
this . cleanupTransfer ( response . fileId ) ;
}
} catch ( error ) {
console . error ( '❌ Failed to handle transfer response:' , error ) ;
}
}
handleChunkConfirmation ( confirmation ) {
try {
const transferState = this . activeTransfers . get ( confirmation . fileId ) ;
if ( ! transferState ) {
return ;
}
transferState . confirmedChunks ++ ;
transferState . lastChunkTime = Date . now ( ) ;
} catch ( error ) {
console . error ( '❌ Failed to handle chunk confirmation:' , error ) ;
}
}
handleTransferComplete ( completion ) {
try {
const transferState = this . activeTransfers . get ( completion . fileId ) ;
if ( ! transferState ) {
return ;
}
if ( completion . success ) {
transferState . status = 'completed' ;
transferState . endTime = Date . now ( ) ;
if ( this . onComplete ) {
this . onComplete ( {
fileId : transferState . fileId ,
fileName : transferState . file . name ,
fileSize : transferState . file . size ,
transferTime : transferState . endTime - transferState . startTime ,
status : 'completed'
} ) ;
}
} else {
transferState . status = 'failed' ;
if ( this . onError ) {
this . onError ( ` Transfer failed: ${ completion . error || 'Unknown error' } ` ) ;
}
}
this . cleanupTransfer ( completion . fileId ) ;
} catch ( error ) {
console . error ( '❌ Failed to handle transfer completion:' , error ) ;
}
}
handleTransferError ( errorMessage ) {
try {
const transferState = this . activeTransfers . get ( errorMessage . fileId ) ;
if ( transferState ) {
transferState . status = 'failed' ;
this . cleanupTransfer ( errorMessage . fileId ) ;
}
const receivingState = this . receivingTransfers . get ( errorMessage . fileId ) ;
if ( receivingState ) {
receivingState . status = 'failed' ;
this . cleanupReceivingTransfer ( errorMessage . fileId ) ;
}
if ( this . onError ) {
this . onError ( ` Transfer error: ${ errorMessage . error || 'Unknown error' } ` ) ;
}
} catch ( error ) {
console . error ( '❌ Failed to handle transfer error:' , error ) ;
}
}
// ============================================
// UTILITY METHODS
// ============================================
getActiveTransfers ( ) {
return Array . from ( this . activeTransfers . values ( ) ) . map ( transfer => ( {
fileId : transfer . fileId ,
fileName : transfer . file ? . name || 'Unknown' ,
fileSize : transfer . file ? . size || 0 ,
progress : Math . round ( ( transfer . sentChunks / transfer . totalChunks ) * 100 ) ,
status : transfer . status ,
startTime : transfer . startTime
} ) ) ;
}
getReceivingTransfers ( ) {
return Array . from ( this . receivingTransfers . values ( ) ) . map ( transfer => ( {
fileId : transfer . fileId ,
fileName : transfer . fileName || 'Unknown' ,
fileSize : transfer . fileSize || 0 ,
progress : Math . round ( ( transfer . receivedCount / transfer . totalChunks ) * 100 ) ,
status : transfer . status ,
startTime : transfer . startTime
} ) ) ;
}
cancelTransfer ( fileId ) {
try {
if ( this . activeTransfers . has ( fileId ) ) {
this . cleanupTransfer ( fileId ) ;
return true ;
}
if ( this . receivingTransfers . has ( fileId ) ) {
this . cleanupReceivingTransfer ( fileId ) ;
return true ;
}
return false ;
} catch ( error ) {
console . error ( '❌ Failed to cancel transfer:' , error ) ;
return false ;
}
}
cleanupTransfer ( fileId ) {
this . activeTransfers . delete ( fileId ) ;
this . sessionKeys . delete ( fileId ) ;
this . transferNonces . delete ( fileId ) ;
// Remove processed chunk IDs for this transfer
for ( const chunkId of this . processedChunks ) {
if ( chunkId . startsWith ( fileId ) ) {
this . processedChunks . delete ( chunkId ) ;
}
}
}
cleanupReceivingTransfer ( fileId ) {
this . pendingChunks . delete ( fileId ) ;
const receivingState = this . receivingTransfers . get ( fileId ) ;
if ( receivingState ) {
// Clear chunk data from memory
receivingState . receivedChunks . clear ( ) ;
}
this . receivingTransfers . delete ( fileId ) ;
this . sessionKeys . delete ( fileId ) ;
2025-08-21 04:07:16 -04:00
// Также очищаем финальный буфер, если он ещё хранится
this . receivedFileBuffers . delete ( fileId ) ;
2025-08-18 21:45:50 -04:00
// Remove processed chunk IDs
for ( const chunkId of this . processedChunks ) {
if ( chunkId . startsWith ( fileId ) ) {
this . processedChunks . delete ( chunkId ) ;
}
}
}
getTransferStatus ( fileId ) {
if ( this . activeTransfers . has ( fileId ) ) {
const transfer = this . activeTransfers . get ( fileId ) ;
return {
type : 'sending' ,
fileId : transfer . fileId ,
fileName : transfer . file . name ,
progress : Math . round ( ( transfer . sentChunks / transfer . totalChunks ) * 100 ) ,
status : transfer . status ,
startTime : transfer . startTime
} ;
}
if ( this . receivingTransfers . has ( fileId ) ) {
const transfer = this . receivingTransfers . get ( fileId ) ;
return {
type : 'receiving' ,
fileId : transfer . fileId ,
fileName : transfer . fileName ,
progress : Math . round ( ( transfer . receivedCount / transfer . totalChunks ) * 100 ) ,
status : transfer . status ,
startTime : transfer . startTime
} ;
}
return null ;
}
getSystemStatus ( ) {
return {
initialized : true ,
activeTransfers : this . activeTransfers . size ,
receivingTransfers : this . receivingTransfers . size ,
totalTransfers : this . activeTransfers . size + this . receivingTransfers . size ,
maxConcurrentTransfers : this . MAX _CONCURRENT _TRANSFERS ,
maxFileSize : this . MAX _FILE _SIZE ,
chunkSize : this . CHUNK _SIZE ,
hasWebrtcManager : ! ! this . webrtcManager ,
2025-08-21 17:40:17 -04:00
isConnected : this . webrtcManager ? . isConnected ? . ( ) || false ,
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Дополнительная диагностика
hasDataChannel : ! ! this . webrtcManager ? . dataChannel ,
dataChannelState : this . webrtcManager ? . dataChannel ? . readyState ,
isVerified : this . webrtcManager ? . isVerified ,
hasEncryptionKey : ! ! this . webrtcManager ? . encryptionKey ,
hasMacKey : ! ! this . webrtcManager ? . macKey ,
linkedToWebRTCManager : this . webrtcManager ? . fileTransferSystem === this ,
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Информация о поддерживаемых типах файлов
supportedFileTypes : this . getSupportedFileTypes ( ) ,
fileTypeInfo : this . getFileTypeInfo ( )
2025-08-18 21:45:50 -04:00
} ;
}
cleanup ( ) {
2025-08-20 18:19:42 -04:00
// ИСПРАВЛЕНИЕ: Очищаем глобальные флаги
window . FILE _TRANSFER _ACTIVE = false ;
window . fileTransferSystem = null ;
// ИСПРАВЛЕНИЕ: Восстанавливаем ВСЕ перехваченные методы
if ( this . webrtcManager && this . webrtcManager . dataChannel && this . originalOnMessage ) {
this . webrtcManager . dataChannel . onmessage = this . originalOnMessage ;
this . originalOnMessage = null ;
}
if ( this . webrtcManager && this . originalProcessMessage ) {
this . webrtcManager . processMessage = this . originalProcessMessage ;
this . originalProcessMessage = null ;
}
if ( this . webrtcManager && this . originalRemoveSecurityLayers ) {
this . webrtcManager . removeSecurityLayers = this . originalRemoveSecurityLayers ;
this . originalRemoveSecurityLayers = null ;
}
2025-08-18 21:45:50 -04:00
// Cleanup all active transfers
for ( const fileId of this . activeTransfers . keys ( ) ) {
this . cleanupTransfer ( fileId ) ;
}
for ( const fileId of this . receivingTransfers . keys ( ) ) {
this . cleanupReceivingTransfer ( fileId ) ;
}
// Clear all state
this . pendingChunks . clear ( ) ;
this . activeTransfers . clear ( ) ;
this . receivingTransfers . clear ( ) ;
this . transferQueue . length = 0 ;
this . sessionKeys . clear ( ) ;
this . transferNonces . clear ( ) ;
this . processedChunks . clear ( ) ;
2025-08-18 23:56:10 -04:00
}
// ============================================
// SESSION UPDATE HANDLER - FIXED
// ============================================
onSessionUpdate ( sessionData ) {
// Clear session keys cache for resync
this . sessionKeys . clear ( ) ;
2025-08-18 21:45:50 -04:00
}
// ============================================
// DEBUGGING AND DIAGNOSTICS
// ============================================
2025-08-21 17:40:17 -04:00
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Метод для диагностики проблем с передачей файлов
diagnoseFileTransferIssue ( ) {
const diagnosis = {
timestamp : new Date ( ) . toISOString ( ) ,
fileTransferSystem : {
initialized : ! ! this ,
hasWebrtcManager : ! ! this . webrtcManager ,
webrtcManagerType : this . webrtcManager ? . constructor ? . name ,
linkedToWebRTCManager : this . webrtcManager ? . fileTransferSystem === this
} ,
webrtcManager : {
hasDataChannel : ! ! this . webrtcManager ? . dataChannel ,
dataChannelState : this . webrtcManager ? . dataChannel ? . readyState ,
isConnected : this . webrtcManager ? . isConnected ? . ( ) || false ,
isVerified : this . webrtcManager ? . isVerified ,
hasEncryptionKey : ! ! this . webrtcManager ? . encryptionKey ,
hasMacKey : ! ! this . webrtcManager ? . macKey ,
hasKeyFingerprint : ! ! this . webrtcManager ? . keyFingerprint ,
hasSessionSalt : ! ! this . webrtcManager ? . sessionSalt
} ,
globalState : {
fileTransferActive : window . FILE _TRANSFER _ACTIVE ,
hasGlobalFileTransferSystem : ! ! window . fileTransferSystem ,
globalFileTransferSystemType : window . fileTransferSystem ? . constructor ? . name
} ,
transfers : {
activeTransfers : this . activeTransfers . size ,
receivingTransfers : this . receivingTransfers . size ,
pendingChunks : this . pendingChunks . size ,
sessionKeys : this . sessionKeys . size
} ,
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Информация о поддерживаемых типах файлов
fileTypeSupport : {
supportedTypes : this . getSupportedFileTypes ( ) ,
generalMaxSize : this . formatFileSize ( this . MAX _FILE _SIZE ) ,
restrictions : Object . keys ( this . FILE _TYPE _RESTRICTIONS )
}
} ;
return diagnosis ;
}
2025-08-18 23:56:10 -04:00
async debugKeyDerivation ( fileId ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-20 18:19:42 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'Session data not available' ) ;
2025-08-18 23:56:10 -04:00
}
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test sender derivation
const senderResult = await this . deriveFileSessionKey ( fileId ) ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test receiver derivation with same salt
const receiverKey = await this . deriveFileSessionKeyFromSalt ( fileId , senderResult . salt ) ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test encryption/decryption
const testData = new TextEncoder ( ) . encode ( 'test data' ) ;
2025-08-18 21:45:50 -04:00
const nonce = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
const encrypted = await crypto . subtle . encrypt (
{ name : 'AES-GCM' , iv : nonce } ,
2025-08-18 23:56:10 -04:00
senderResult . key ,
testData
2025-08-18 21:45:50 -04:00
) ;
const decrypted = await crypto . subtle . decrypt (
{ name : 'AES-GCM' , iv : nonce } ,
2025-08-18 23:56:10 -04:00
receiverKey ,
2025-08-18 21:45:50 -04:00
encrypted
) ;
const decryptedText = new TextDecoder ( ) . decode ( decrypted ) ;
2025-08-18 23:56:10 -04:00
if ( decryptedText === 'test data' ) {
return { success : true , message : 'All tests passed' } ;
2025-08-18 21:45:50 -04:00
} else {
throw new Error ( 'Decryption verification failed' ) ;
}
} catch ( error ) {
2025-08-18 23:56:10 -04:00
console . error ( '❌ Key derivation test failed:' , error ) ;
2025-08-18 21:45:50 -04:00
return { success : false , error : error . message } ;
}
}
2025-08-20 18:19:42 -04:00
// ============================================
// АЛЬТЕРНАТИВНЫЙ МЕТОД ИНИЦИАЛИЗАЦИИ ОБРАБОТЧИКОВ
// ============================================
// Если переопределение processMessage не работает,
// используйте этот метод для явной регистрации обработчика
registerWithWebRTCManager ( ) {
if ( ! this . webrtcManager ) {
throw new Error ( 'WebRTC manager not available' ) ;
}
// Сохраняем ссылку на файловую систему в WebRTC менеджере
this . webrtcManager . fileTransferSystem = this ;
// КРИТИЧЕСКИ ВАЖНО: Устанавливаем обработчик файловых сообщений
this . webrtcManager . setFileMessageHandler = ( handler ) => {
this . webrtcManager . _fileMessageHandler = handler ;
} ;
// Регистрируем наш обработчик
this . webrtcManager . setFileMessageHandler ( ( message ) => {
return this . handleFileMessage ( message ) ;
} ) ;
}
// Метод для прямого вызова из WebRTC менеджера
static createFileMessageFilter ( fileTransferSystem ) {
return async ( event ) => {
try {
if ( typeof event . data === 'string' ) {
const parsed = JSON . parse ( event . data ) ;
if ( fileTransferSystem . isFileTransferMessage ( parsed ) ) {
await fileTransferSystem . handleFileMessage ( parsed ) ;
return true ; // Сообщение обработано
}
}
} catch ( error ) {
// Не файловое сообщение или ошибка парсинга
}
return false ; // Сообщение не обработано
} ;
}
2025-08-18 21:45:50 -04:00
}
export { EnhancedSecureFileTransfer } ;