Compare commits
6
Commits
026c81b775
...
f2a4276b31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a4276b31 | ||
|
|
86a96b0121 | ||
|
|
33f3764ec5 | ||
|
|
a04a70eb97 | ||
|
|
0fbcc240be | ||
|
|
18022c6b68 |
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"dev": "npm run build && python -m http.server 8000",
|
"dev": "npm run build && python -m http.server 8000",
|
||||||
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
||||||
"serve": "npx http-server -p 8000",
|
"serve": "npx http-server -p 8000",
|
||||||
"test": "node tests/sas-verification.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs"
|
"test": "node tests/sas-verification.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"p2p",
|
"p2p",
|
||||||
|
|||||||
+12
-26
@@ -1,4 +1,4 @@
|
|||||||
|
import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||||
// Enhanced Copy Button with better UX
|
// Enhanced Copy Button with better UX
|
||||||
const EnhancedCopyButton = ({ text, className = "", children }) => {
|
const EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
@@ -1639,32 +1639,18 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global functions for cleanup
|
|
||||||
React.useEffect(() => {
|
|
||||||
window.forceCleanup = () => {
|
|
||||||
handleClearData();
|
|
||||||
if (webrtcManagerRef.current) {
|
|
||||||
webrtcManagerRef.current.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.clearLogs = () => {
|
|
||||||
if (typeof console.clear === 'function') {
|
|
||||||
console.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
delete window.forceCleanup;
|
|
||||||
delete window.clearLogs;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const webrtcManagerRef = React.useRef(null);
|
const webrtcManagerRef = React.useRef(null);
|
||||||
const notificationIntegrationRef = React.useRef(null);
|
const notificationIntegrationRef = React.useRef(null);
|
||||||
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
|
|
||||||
// Safe because it's a ref object and we maintain it centrally here
|
// Development-only debug helpers. Production never exposes
|
||||||
window.webrtcManagerRef = webrtcManagerRef;
|
// manager internals or cleanup controls on `window`.
|
||||||
|
React.useEffect(() => {
|
||||||
|
return installDebugWindowHooks({
|
||||||
|
targetWindow: window,
|
||||||
|
webrtcManagerRef,
|
||||||
|
onClearData: handleClearData
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
@@ -1766,7 +1752,7 @@
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
try { localStorage.setItem('securebit_relay_only_mode', String(relayOnlyMode)); } catch {}
|
try { localStorage.setItem('securebit_relay_only_mode', String(relayOnlyMode)); } catch {}
|
||||||
if (webrtcManagerRef.current?._config?.webrtc) {
|
if (webrtcManagerRef.current?._config?.webrtc) {
|
||||||
webrtcManagerRef.current._config.webrtc.relayOnly = relayOnlyMode;
|
webrtcManagerRef.current._setRelayOnlyMode(relayOnlyMode);
|
||||||
}
|
}
|
||||||
}, [relayOnlyMode]);
|
}, [relayOnlyMode]);
|
||||||
|
|
||||||
|
|||||||
@@ -153,12 +153,13 @@ class EnhancedSecureWebRTCManager {
|
|||||||
useRandomHeaders: config.antiFingerprinting?.useRandomHeaders ?? false
|
useRandomHeaders: config.antiFingerprinting?.useRandomHeaders ?? false
|
||||||
},
|
},
|
||||||
webrtc: {
|
webrtc: {
|
||||||
// `privacyMode` is the explicit operator-facing setting.
|
// `privacyMode` is canonical; `relayOnly` remains a
|
||||||
// Keep `relayOnly` as a backward-compatible alias.
|
// backward-compatible input alias at construction time.
|
||||||
privacyMode: config.webrtc?.privacyMode
|
privacyMode: config.webrtc?.privacyMode
|
||||||
?? (config.webrtc?.relayOnly ? 'relay-only' : 'standard'),
|
?? (config.webrtc?.relayOnly ? 'relay-only' : 'standard'),
|
||||||
relayOnly: config.webrtc?.relayOnly
|
relayOnly: config.webrtc?.privacyMode
|
||||||
?? config.webrtc?.privacyMode === 'relay-only',
|
? config.webrtc.privacyMode === 'relay-only'
|
||||||
|
: config.webrtc?.relayOnly ?? false,
|
||||||
iceServers: config.webrtc?.iceServers
|
iceServers: config.webrtc?.iceServers
|
||||||
?? EnhancedSecureWebRTCManager.DEFAULT_ICE_SERVERS.map(server => ({ ...server }))
|
?? EnhancedSecureWebRTCManager.DEFAULT_ICE_SERVERS.map(server => ({ ...server }))
|
||||||
}
|
}
|
||||||
@@ -1486,6 +1487,47 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated receiver-side limiter. Keep separate from outbound quotas so a
|
||||||
|
* noisy peer cannot consume local send capacity or force decrypt/render work.
|
||||||
|
*/
|
||||||
|
_checkInboundRateLimit(context = 'incoming_message') {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!this._inboundRateLimiter) {
|
||||||
|
this._inboundRateLimiter = {
|
||||||
|
messageCount: 0,
|
||||||
|
lastReset: now,
|
||||||
|
burstCount: 0,
|
||||||
|
lastBurstReset: now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - this._inboundRateLimiter.lastReset > 60000) {
|
||||||
|
this._inboundRateLimiter.messageCount = 0;
|
||||||
|
this._inboundRateLimiter.lastReset = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - this._inboundRateLimiter.lastBurstReset > 1000) {
|
||||||
|
this._inboundRateLimiter.burstCount = 0;
|
||||||
|
this._inboundRateLimiter.lastBurstReset = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._inboundRateLimiter.burstCount >= this._inputValidationLimits.rateLimitBurstSize) {
|
||||||
|
this._secureLog('warn', '⚠️ Inbound message burst limit exceeded; dropping message', { context });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._inboundRateLimiter.messageCount >= this._inputValidationLimits.rateLimitMessagesPerMinute) {
|
||||||
|
this._secureLog('warn', '⚠️ Inbound message rate limit exceeded; dropping message', { context });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._inboundRateLimiter.messageCount++;
|
||||||
|
this._inboundRateLimiter.burstCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// SECURE KEY STORAGE MANAGEMENT
|
// SECURE KEY STORAGE MANAGEMENT
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -6416,6 +6458,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
if (parsed.type === 'enhanced_message') {
|
if (parsed.type === 'enhanced_message') {
|
||||||
this._secureLog('debug', '🔐 Enhanced message detected in processMessage');
|
this._secureLog('debug', '🔐 Enhanced message detected in processMessage');
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:enhanced_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decrypt enhanced message
|
// Decrypt enhanced message
|
||||||
@@ -6458,6 +6503,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
if (parsed.type === 'message') {
|
if (parsed.type === 'message') {
|
||||||
this._secureLog('debug', '📝 Regular user message detected in processMessage');
|
this._secureLog('debug', '📝 Regular user message detected in processMessage');
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage && parsed.data) {
|
if (this.onMessage && parsed.data) {
|
||||||
this.deliverMessageToUI(parsed.data, 'received');
|
this.deliverMessageToUI(parsed.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -6484,6 +6532,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// Not JSON — treat as text WITHOUT mutex
|
// Not JSON — treat as text WITHOUT mutex
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:text')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(data, 'received');
|
this.deliverMessageToUI(data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7213,8 +7264,13 @@ async processMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isRelayOnlyMode() {
|
_isRelayOnlyMode() {
|
||||||
return this._config.webrtc.privacyMode === 'relay-only'
|
return this._config.webrtc.privacyMode === 'relay-only';
|
||||||
|| this._config.webrtc.relayOnly === true;
|
}
|
||||||
|
|
||||||
|
_setRelayOnlyMode(relayOnly) {
|
||||||
|
const enabled = relayOnly === true;
|
||||||
|
this._config.webrtc.privacyMode = enabled ? 'relay-only' : 'standard';
|
||||||
|
this._config.webrtc.relayOnly = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
_warnIfTurnMissing() {
|
_warnIfTurnMissing() {
|
||||||
@@ -7448,6 +7504,9 @@ async processMessage(data) {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
if (parsed.type === 'message' && parsed.data) {
|
if (parsed.type === 'message' && parsed.data) {
|
||||||
|
if (!this._checkInboundRateLimit('dataChannel:message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(parsed.data, 'received');
|
this.deliverMessageToUI(parsed.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7466,6 +7525,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// Not JSON — treat as regular text message
|
// Not JSON — treat as regular text message
|
||||||
|
if (!this._checkInboundRateLimit('dataChannel:text')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(event.data, 'received');
|
this.deliverMessageToUI(event.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7484,6 +7546,9 @@ async processMessage(data) {
|
|||||||
// FIX 4: New method for processing binary data WITHOUT mutex
|
// FIX 4: New method for processing binary data WITHOUT mutex
|
||||||
async _processBinaryDataWithoutMutex(data) {
|
async _processBinaryDataWithoutMutex(data) {
|
||||||
try {
|
try {
|
||||||
|
if (!this._checkInboundRateLimit('binary_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply security layers WITHOUT mutex
|
// Apply security layers WITHOUT mutex
|
||||||
let processedData = data;
|
let processedData = data;
|
||||||
@@ -7546,6 +7611,9 @@ async processMessage(data) {
|
|||||||
// FIX 3: New method for processing enhanced messages WITHOUT mutex
|
// FIX 3: New method for processing enhanced messages WITHOUT mutex
|
||||||
async _processEnhancedMessageWithoutMutex(parsedMessage) {
|
async _processEnhancedMessageWithoutMutex(parsedMessage) {
|
||||||
try {
|
try {
|
||||||
|
if (!this._checkInboundRateLimit('enhanced_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
||||||
this._secureLog('error', 'Missing encryption keys for enhanced message');
|
this._secureLog('error', 'Missing encryption keys for enhanced message');
|
||||||
@@ -11667,9 +11735,6 @@ async processMessage(data) {
|
|||||||
this.intentionalDisconnect = true;
|
this.intentionalDisconnect = true;
|
||||||
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting intentional disconnect');
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting intentional disconnect');
|
||||||
this.sendDisconnectNotification();
|
this.sendDisconnectNotification();
|
||||||
setTimeout(() => {
|
|
||||||
this.sendDisconnectNotification();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Stop every timer-backed subsystem first.
|
// Stop every timer-backed subsystem first.
|
||||||
this._stopAllTimers();
|
this._stopAllTimers();
|
||||||
|
|||||||
@@ -336,6 +336,9 @@ class EnhancedSecureFileTransfer {
|
|||||||
this.transferQueue = []; // Queue for pending transfers
|
this.transferQueue = []; // Queue for pending transfers
|
||||||
this.pendingChunks = new Map();
|
this.pendingChunks = new Map();
|
||||||
this.incomingOfferLimiter = new RateLimiter(5, 60000);
|
this.incomingOfferLimiter = new RateLimiter(5, 60000);
|
||||||
|
this.incomingChunkLimiter = new RateLimiter(240, 60000);
|
||||||
|
this.incomingTransferChunkLimiters = new Map();
|
||||||
|
this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE = 120;
|
||||||
this.MAX_PENDING_INCOMING_TRANSFERS = 3;
|
this.MAX_PENDING_INCOMING_TRANSFERS = 3;
|
||||||
|
|
||||||
// Session key derivation
|
// Session key derivation
|
||||||
@@ -1224,6 +1227,12 @@ class EnhancedSecureFileTransfer {
|
|||||||
if (!receivingState) {
|
if (!receivingState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this._isIncomingChunkAllowed(chunkMessage.fileId)) {
|
||||||
|
console.warn('⚠️ Incoming file chunk rate limit exceeded; cleaning up transfer:', chunkMessage.fileId);
|
||||||
|
this.cleanupReceivingTransfer(chunkMessage.fileId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update last chunk time
|
// Update last chunk time
|
||||||
receivingState.lastChunkTime = Date.now();
|
receivingState.lastChunkTime = Date.now();
|
||||||
@@ -1310,6 +1319,35 @@ class EnhancedSecureFileTransfer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isIncomingChunkAllowed(fileId) {
|
||||||
|
const clientId = this.getClientIdentifier();
|
||||||
|
if (!this.incomingChunkLimiter.isAllowed(clientId)) {
|
||||||
|
SecurityErrorHandler.logSecurityEvent('incoming_chunk_aggregate_rate_limit_exceeded', {
|
||||||
|
clientId,
|
||||||
|
fileId
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.incomingTransferChunkLimiters.has(fileId)) {
|
||||||
|
this.incomingTransferChunkLimiters.set(
|
||||||
|
fileId,
|
||||||
|
new RateLimiter(this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE, 60000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferLimiter = this.incomingTransferChunkLimiters.get(fileId);
|
||||||
|
if (!transferLimiter.isAllowed(fileId)) {
|
||||||
|
SecurityErrorHandler.logSecurityEvent('incoming_chunk_transfer_rate_limit_exceeded', {
|
||||||
|
clientId,
|
||||||
|
fileId
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async assembleFile(receivingState) {
|
async assembleFile(receivingState) {
|
||||||
try {
|
try {
|
||||||
receivingState.status = 'assembling';
|
receivingState.status = 'assembling';
|
||||||
@@ -1651,6 +1689,7 @@ class EnhancedSecureFileTransfer {
|
|||||||
this.activeTransfers.delete(fileId);
|
this.activeTransfers.delete(fileId);
|
||||||
this.sessionKeys.delete(fileId);
|
this.sessionKeys.delete(fileId);
|
||||||
this.transferNonces.delete(fileId);
|
this.transferNonces.delete(fileId);
|
||||||
|
this.incomingTransferChunkLimiters.delete(fileId);
|
||||||
|
|
||||||
// Remove processed chunk IDs for this transfer
|
// Remove processed chunk IDs for this transfer
|
||||||
for (const chunkId of this.processedChunks) {
|
for (const chunkId of this.processedChunks) {
|
||||||
@@ -1751,6 +1790,7 @@ class EnhancedSecureFileTransfer {
|
|||||||
// Удаляем из основных коллекций
|
// Удаляем из основных коллекций
|
||||||
this.receivingTransfers.delete(fileId);
|
this.receivingTransfers.delete(fileId);
|
||||||
this.sessionKeys.delete(fileId);
|
this.sessionKeys.delete(fileId);
|
||||||
|
this.incomingTransferChunkLimiters.delete(fileId);
|
||||||
|
|
||||||
// ✅ БЕЗОПАСНАЯ очистка финального буфера файла
|
// ✅ БЕЗОПАСНАЯ очистка финального буфера файла
|
||||||
const fileBuffer = this.receivedFileBuffers.get(fileId);
|
const fileBuffer = this.receivedFileBuffers.get(fileId);
|
||||||
@@ -1903,6 +1943,10 @@ class EnhancedSecureFileTransfer {
|
|||||||
if (this.rateLimiter) {
|
if (this.rateLimiter) {
|
||||||
this.rateLimiter.requests.clear();
|
this.rateLimiter.requests.clear();
|
||||||
}
|
}
|
||||||
|
if (this.incomingChunkLimiter) {
|
||||||
|
this.incomingChunkLimiter.requests.clear();
|
||||||
|
}
|
||||||
|
this.incomingTransferChunkLimiters.clear();
|
||||||
|
|
||||||
// Clear all state
|
// Clear all state
|
||||||
this.pendingChunks.clear();
|
this.pendingChunks.clear();
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
function isSecureBitDebugEnabled(targetWindow = globalThis.window) {
|
||||||
|
return targetWindow?.SECUREBIT_DEBUG === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installDebugWindowHooks({
|
||||||
|
targetWindow = globalThis.window,
|
||||||
|
webrtcManagerRef,
|
||||||
|
onClearData,
|
||||||
|
clearConsole = () => {
|
||||||
|
if (typeof console.clear === 'function') {
|
||||||
|
console.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (!isSecureBitDebugEnabled(targetWindow)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
targetWindow.forceCleanup = () => {
|
||||||
|
onClearData();
|
||||||
|
if (webrtcManagerRef.current) {
|
||||||
|
webrtcManagerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
targetWindow.clearLogs = clearConsole;
|
||||||
|
targetWindow.webrtcManagerRef = webrtcManagerRef;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete targetWindow.forceCleanup;
|
||||||
|
delete targetWindow.clearLogs;
|
||||||
|
delete targetWindow.webrtcManagerRef;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { installDebugWindowHooks, isSecureBitDebugEnabled };
|
||||||
@@ -87,6 +87,30 @@ const CACHE_FIRST_PATTERNS = [
|
|||||||
/src\/scripts\/pwa-.*\.js$/
|
/src\/scripts\/pwa-.*\.js$/
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Explicit allowlist for any response that may be written to Cache Storage.
|
||||||
|
// Unknown GET responses are deliberately excluded by default.
|
||||||
|
const CACHEABLE_PATHS = new Set([
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/manifest.json',
|
||||||
|
'/src/styles/pwa.css',
|
||||||
|
'/logo/icon-192x192.png',
|
||||||
|
'/logo/icon-512x512.png',
|
||||||
|
'/logo/favicon.ico',
|
||||||
|
'/src/pwa/pwa-manager.js',
|
||||||
|
'/src/pwa/install-prompt.js',
|
||||||
|
'/src/scripts/pwa-register.js',
|
||||||
|
'/src/scripts/pwa-offline-test.js'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isSensitivePath(pathname) {
|
||||||
|
return SENSITIVE_PATTERNS.some(pattern => pattern.test(pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCacheableStaticPath(pathname) {
|
||||||
|
return CACHEABLE_PATHS.has(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener('message', (event) => {
|
||||||
if (event.data && event.data.type === 'PWA_INSTALLED') {
|
if (event.data && event.data.type === 'PWA_INSTALLED') {
|
||||||
self.clients.matchAll().then(clients => {
|
self.clients.matchAll().then(clients => {
|
||||||
@@ -191,7 +215,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip sensitive endpoints
|
// Skip sensitive endpoints
|
||||||
if (SENSITIVE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
if (isSensitivePath(url.pathname)) {
|
||||||
console.log('🔒 Skipping cache for sensitive endpoint:', url.pathname);
|
console.log('🔒 Skipping cache for sensitive endpoint:', url.pathname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -304,11 +328,16 @@ async function cacheFirst(request) {
|
|||||||
|
|
||||||
// Network First strategy with Response cloning fix
|
// Network First strategy with Response cloning fix
|
||||||
async function networkFirst(request) {
|
async function networkFirst(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
try {
|
try {
|
||||||
const networkResponse = await fetch(request);
|
const networkResponse = await fetch(request);
|
||||||
if (networkResponse && networkResponse.ok) {
|
if (networkResponse && networkResponse.ok) {
|
||||||
// Only cache non-sensitive successful responses
|
// Only cache explicitly known-safe static responses.
|
||||||
if (!SENSITIVE_PATTERNS.some(pattern => pattern.test(request.url))) {
|
if (
|
||||||
|
url.origin === self.location.origin &&
|
||||||
|
isCacheableStaticPath(url.pathname) &&
|
||||||
|
!isSensitivePath(url.pathname)
|
||||||
|
) {
|
||||||
// Clone the response before caching
|
// Clone the response before caching
|
||||||
const responseToCache = networkResponse.clone();
|
const responseToCache = networkResponse.clone();
|
||||||
const cache = await caches.open(DYNAMIC_CACHE);
|
const cache = await caches.open(DYNAMIC_CACHE);
|
||||||
@@ -336,12 +365,18 @@ async function networkFirst(request) {
|
|||||||
|
|
||||||
// Stale While Revalidate strategy with Response cloning fix
|
// Stale While Revalidate strategy with Response cloning fix
|
||||||
async function staleWhileRevalidate(request) {
|
async function staleWhileRevalidate(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
const cachedResponse = await caches.match(request);
|
const cachedResponse = await caches.match(request);
|
||||||
|
|
||||||
const networkResponsePromise = fetch(request)
|
const networkResponsePromise = fetch(request)
|
||||||
.then((networkResponse) => {
|
.then((networkResponse) => {
|
||||||
if (networkResponse && networkResponse.ok &&
|
if (
|
||||||
!SENSITIVE_PATTERNS.some(pattern => pattern.test(request.url))) {
|
networkResponse &&
|
||||||
|
networkResponse.ok &&
|
||||||
|
url.origin === self.location.origin &&
|
||||||
|
isCacheableStaticPath(url.pathname) &&
|
||||||
|
!isSensitivePath(url.pathname)
|
||||||
|
) {
|
||||||
// Clone the response before caching
|
// Clone the response before caching
|
||||||
const responseToCache = networkResponse.clone();
|
const responseToCache = networkResponse.clone();
|
||||||
caches.open(DYNAMIC_CACHE)
|
caches.open(DYNAMIC_CACHE)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
const { installDebugWindowHooks, isSecureBitDebugEnabled } = await import('../src/utils/debugWindowHooks.js');
|
||||||
|
|
||||||
|
// Production mode does not expose debug/control globals.
|
||||||
|
{
|
||||||
|
const targetWindow = {};
|
||||||
|
const managerRef = { current: { disconnect() {} } };
|
||||||
|
const cleanup = installDebugWindowHooks({
|
||||||
|
targetWindow,
|
||||||
|
webrtcManagerRef: managerRef,
|
||||||
|
onClearData() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(isSecureBitDebugEnabled(targetWindow), false);
|
||||||
|
assert.equal('forceCleanup' in targetWindow, false);
|
||||||
|
assert.equal('clearLogs' in targetWindow, false);
|
||||||
|
assert.equal('webrtcManagerRef' in targetWindow, false);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug mode exposes hooks only when explicitly requested.
|
||||||
|
{
|
||||||
|
let clearDataCalls = 0;
|
||||||
|
let disconnectCalls = 0;
|
||||||
|
let clearLogCalls = 0;
|
||||||
|
const targetWindow = { SECUREBIT_DEBUG: true };
|
||||||
|
const managerRef = {
|
||||||
|
current: {
|
||||||
|
disconnect() {
|
||||||
|
disconnectCalls += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const cleanup = installDebugWindowHooks({
|
||||||
|
targetWindow,
|
||||||
|
webrtcManagerRef: managerRef,
|
||||||
|
onClearData() {
|
||||||
|
clearDataCalls += 1;
|
||||||
|
},
|
||||||
|
clearConsole() {
|
||||||
|
clearLogCalls += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(isSecureBitDebugEnabled(targetWindow), true);
|
||||||
|
assert.equal(targetWindow.webrtcManagerRef, managerRef);
|
||||||
|
targetWindow.forceCleanup();
|
||||||
|
targetWindow.clearLogs();
|
||||||
|
assert.equal(clearDataCalls, 1);
|
||||||
|
assert.equal(disconnectCalls, 1);
|
||||||
|
assert.equal(clearLogCalls, 1);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
assert.equal('forceCleanup' in targetWindow, false);
|
||||||
|
assert.equal('clearLogs' in targetWindow, false);
|
||||||
|
assert.equal('webrtcManagerRef' in targetWindow, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal cleanup remains available through the app-owned callback path.
|
||||||
|
{
|
||||||
|
let clearDataCalls = 0;
|
||||||
|
const onClearData = () => {
|
||||||
|
clearDataCalls += 1;
|
||||||
|
};
|
||||||
|
installDebugWindowHooks({
|
||||||
|
targetWindow: {},
|
||||||
|
webrtcManagerRef: { current: null },
|
||||||
|
onClearData
|
||||||
|
});
|
||||||
|
onClearData();
|
||||||
|
assert.equal(clearDataCalls, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Debug window hook tests passed');
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { EnhancedSecureFileTransfer } from '../src/transfer/EnhancedSecureFileTransfer.js';
|
||||||
|
|
||||||
|
function createSystem() {
|
||||||
|
const manager = {
|
||||||
|
dataChannel: { onmessage: null, send() {}, readyState: 'open' },
|
||||||
|
isVerified: true,
|
||||||
|
fileTransferSystem: null,
|
||||||
|
isConnected: () => true,
|
||||||
|
connectionId: 'peer-1'
|
||||||
|
};
|
||||||
|
const system = new EnhancedSecureFileTransfer(manager);
|
||||||
|
system.sendSecureMessage = async () => {};
|
||||||
|
system._storeReceivedFileBuffer = () => {};
|
||||||
|
system.calculateFileHashFromData = async () => 'hash';
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createReceivingState(fileId = 'file_1', totalChunks = 10) {
|
||||||
|
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 128 }, false, ['encrypt', 'decrypt']);
|
||||||
|
return {
|
||||||
|
fileId,
|
||||||
|
fileName: 'report.pdf',
|
||||||
|
fileSize: totalChunks,
|
||||||
|
fileType: 'application/pdf',
|
||||||
|
fileHash: 'hash',
|
||||||
|
totalChunks,
|
||||||
|
receivedChunks: new Map(),
|
||||||
|
receivedCount: 0,
|
||||||
|
sessionKey: key,
|
||||||
|
salt: new Array(32).fill(1),
|
||||||
|
startTime: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptedChunk(system, receivingState, chunkIndex) {
|
||||||
|
const nonce = new Uint8Array(12);
|
||||||
|
nonce[11] = chunkIndex;
|
||||||
|
const plaintext = new Uint8Array([chunkIndex + 1]);
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: nonce },
|
||||||
|
receivingState.sessionKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
fileId: receivingState.fileId,
|
||||||
|
chunkIndex,
|
||||||
|
nonce: Array.from(nonce),
|
||||||
|
encryptedData: Array.from(new Uint8Array(encrypted)),
|
||||||
|
chunkSize: plaintext.byteLength
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal transfer pace is accepted.
|
||||||
|
{
|
||||||
|
const system = createSystem();
|
||||||
|
const state = await createReceivingState('normal');
|
||||||
|
system.receivingTransfers.set(state.fileId, state);
|
||||||
|
await system.handleFileChunk(await encryptedChunk(system, state, 0));
|
||||||
|
assert.equal(state.receivedCount, 1);
|
||||||
|
assert.equal(system.receivingTransfers.has(state.fileId), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-transfer floods are rejected and cleaned up.
|
||||||
|
{
|
||||||
|
const system = createSystem();
|
||||||
|
system.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE = 1;
|
||||||
|
const state = await createReceivingState('per-transfer');
|
||||||
|
system.receivingTransfers.set(state.fileId, state);
|
||||||
|
await system.handleFileChunk(await encryptedChunk(system, state, 0));
|
||||||
|
await system.handleFileChunk(await encryptedChunk(system, state, 1));
|
||||||
|
assert.equal(system.receivingTransfers.has(state.fileId), false);
|
||||||
|
assert.equal(system.incomingTransferChunkLimiters.has(state.fileId), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate floods across transfers are rejected and clean only the affected transfer.
|
||||||
|
{
|
||||||
|
const system = createSystem();
|
||||||
|
system.incomingChunkLimiter.maxRequests = 1;
|
||||||
|
const first = await createReceivingState('aggregate-a');
|
||||||
|
const second = await createReceivingState('aggregate-b');
|
||||||
|
system.receivingTransfers.set(first.fileId, first);
|
||||||
|
system.receivingTransfers.set(second.fileId, second);
|
||||||
|
await system.handleFileChunk(await encryptedChunk(system, first, 0));
|
||||||
|
await system.handleFileChunk(await encryptedChunk(system, second, 0));
|
||||||
|
assert.equal(system.receivingTransfers.has(first.fileId), true);
|
||||||
|
assert.equal(system.receivingTransfers.has(second.fileId), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consent flow still rejects pre-acceptance chunks without allocating buffers.
|
||||||
|
{
|
||||||
|
const system = createSystem();
|
||||||
|
await system.handleFileChunk({ fileId: 'not-accepted', chunkIndex: 0 });
|
||||||
|
assert.equal(system.pendingChunks.size, 0);
|
||||||
|
assert.equal(system.incomingTransferChunkLimiters.has('not-accepted'), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('File transfer chunk rate limit tests passed');
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
globalThis.window = {
|
||||||
|
EnhancedSecureCryptoUtils: {
|
||||||
|
async decryptMessage() {
|
||||||
|
return { message: JSON.stringify({ type: 'message', data: 'enhanced hello' }) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { EnhancedSecureWebRTCManager } = await import('../src/network/EnhancedSecureWebRTCManager.js');
|
||||||
|
|
||||||
|
function fakeManager({ perMinute = 60, burst = 10 } = {}) {
|
||||||
|
return {
|
||||||
|
delivered: [],
|
||||||
|
logs: [],
|
||||||
|
_inputValidationLimits: {
|
||||||
|
rateLimitMessagesPerMinute: perMinute,
|
||||||
|
rateLimitBurstSize: burst
|
||||||
|
},
|
||||||
|
_checkInboundRateLimit: EnhancedSecureWebRTCManager.prototype._checkInboundRateLimit,
|
||||||
|
_secureLog(level, message, context) {
|
||||||
|
this.logs.push({ level, message, context });
|
||||||
|
},
|
||||||
|
onMessage() {},
|
||||||
|
deliverMessageToUI(message, type) {
|
||||||
|
this.delivered.push({ message, type });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal inbound messages are delivered.
|
||||||
|
{
|
||||||
|
const manager = fakeManager();
|
||||||
|
await EnhancedSecureWebRTCManager.prototype.processMessage.call(
|
||||||
|
manager,
|
||||||
|
JSON.stringify({ type: 'message', data: 'hello' })
|
||||||
|
);
|
||||||
|
assert.deepEqual(manager.delivered, [{ message: 'hello', type: 'received' }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burst floods are dropped safely and logged.
|
||||||
|
{
|
||||||
|
const manager = fakeManager({ burst: 1 });
|
||||||
|
await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'first' }));
|
||||||
|
await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'second' }));
|
||||||
|
assert.deepEqual(manager.delivered, [{ message: 'first', type: 'received' }]);
|
||||||
|
assert.match(manager.logs.at(-1).message, /Inbound message burst limit exceeded/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sustained-window floods are rejected independently of burst accounting.
|
||||||
|
{
|
||||||
|
const manager = fakeManager({ perMinute: 1, burst: 10 });
|
||||||
|
await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'first' }));
|
||||||
|
manager._inboundRateLimiter.lastBurstReset = Date.now() - 1001;
|
||||||
|
await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'second' }));
|
||||||
|
assert.deepEqual(manager.delivered, [{ message: 'first', type: 'received' }]);
|
||||||
|
assert.match(manager.logs.at(-1).message, /Inbound message rate limit exceeded/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary and enhanced helpers are guarded before expensive processing.
|
||||||
|
{
|
||||||
|
const binaryManager = {
|
||||||
|
...fakeManager({ burst: 0 }),
|
||||||
|
securityFeatures: {
|
||||||
|
hasNestedEncryption: false,
|
||||||
|
hasPacketPadding: false,
|
||||||
|
hasAntiFingerprinting: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await EnhancedSecureWebRTCManager.prototype._processBinaryDataWithoutMutex.call(
|
||||||
|
binaryManager,
|
||||||
|
new TextEncoder().encode('binary hello').buffer
|
||||||
|
);
|
||||||
|
assert.deepEqual(binaryManager.delivered, []);
|
||||||
|
|
||||||
|
const enhancedManager = {
|
||||||
|
...fakeManager({ burst: 0 }),
|
||||||
|
encryptionKey: {},
|
||||||
|
macKey: {},
|
||||||
|
metadataKey: {}
|
||||||
|
};
|
||||||
|
await EnhancedSecureWebRTCManager.prototype._processEnhancedMessageWithoutMutex.call(
|
||||||
|
enhancedManager,
|
||||||
|
{ data: 'ciphertext' }
|
||||||
|
);
|
||||||
|
assert.deepEqual(enhancedManager.delivered, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outbound limiter remains a separate state machine.
|
||||||
|
{
|
||||||
|
const manager = {
|
||||||
|
_inputValidationLimits: {
|
||||||
|
rateLimitMessagesPerMinute: 1,
|
||||||
|
rateLimitBurstSize: 1
|
||||||
|
},
|
||||||
|
_secureLog() {},
|
||||||
|
_checkRateLimit: EnhancedSecureWebRTCManager.prototype._checkRateLimit,
|
||||||
|
_checkInboundRateLimit: EnhancedSecureWebRTCManager.prototype._checkInboundRateLimit
|
||||||
|
};
|
||||||
|
assert.equal(manager._checkRateLimit('send'), true);
|
||||||
|
assert.equal(manager._checkInboundRateLimit('receive'), true);
|
||||||
|
assert.notEqual(manager._rateLimiter, manager._inboundRateLimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Inbound message rate limit tests passed');
|
||||||
@@ -114,6 +114,62 @@ try {
|
|||||||
assert.equal(scheduled[0].cleared, true);
|
assert.equal(scheduled[0].cleared, true);
|
||||||
assert.equal(disconnectCalls, 0);
|
assert.equal(disconnectCalls, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intentional disconnect performs notification before teardown without leaving a delayed retry timer behind.
|
||||||
|
{
|
||||||
|
let notifications = 0;
|
||||||
|
const manager = {
|
||||||
|
_sessionAlive: true,
|
||||||
|
_activeTimers: new Set(),
|
||||||
|
intentionalDisconnect: false,
|
||||||
|
fileTransferSystem: null,
|
||||||
|
dataChannel: null,
|
||||||
|
heartbeatChannel: null,
|
||||||
|
peerConnection: null,
|
||||||
|
decoyTimers: new Map(),
|
||||||
|
decoyChannels: new Map(),
|
||||||
|
packetBuffer: new Map(),
|
||||||
|
chunkQueue: [],
|
||||||
|
processedMessageIds: new Set(),
|
||||||
|
messageCounter: 0,
|
||||||
|
keyVersions: new Map(),
|
||||||
|
oldKeys: new Map(),
|
||||||
|
currentKeyVersion: 0,
|
||||||
|
lastKeyRotation: 0,
|
||||||
|
sequenceNumber: 0,
|
||||||
|
expectedSequenceNumber: 0,
|
||||||
|
replayWindow: new Set(),
|
||||||
|
messageQueue: [],
|
||||||
|
_heartbeatConfig: {},
|
||||||
|
_secureLog() {},
|
||||||
|
_stopAllTimers: EnhancedSecureWebRTCManager.prototype._stopAllTimers,
|
||||||
|
stopHeartbeat() {},
|
||||||
|
stopFakeTrafficGeneration() {},
|
||||||
|
_wipeEphemeralKeys() {},
|
||||||
|
_hardWipeOldKeys() {},
|
||||||
|
_secureCleanupCryptographicMaterials() {},
|
||||||
|
_clearVerificationStates() {},
|
||||||
|
_secureWipeMemory() {},
|
||||||
|
_forceGarbageCollection() { return Promise.resolve(); },
|
||||||
|
sendDisconnectNotification() { notifications += 1; },
|
||||||
|
onStatusChange() {},
|
||||||
|
onKeyExchange() {},
|
||||||
|
onVerificationRequired() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timersBeforeDisconnect = timers.length;
|
||||||
|
EnhancedSecureWebRTCManager.prototype.disconnect.call(manager);
|
||||||
|
assert.equal(notifications, 1);
|
||||||
|
assert.equal(manager._activeTimers.size, 0);
|
||||||
|
assert.equal(timers.length, timersBeforeDisconnect);
|
||||||
|
|
||||||
|
// A second disconnect/reconnect-style cycle still does not accumulate deferred timers.
|
||||||
|
manager._sessionAlive = true;
|
||||||
|
EnhancedSecureWebRTCManager.prototype.disconnect.call(manager);
|
||||||
|
assert.equal(notifications, 2);
|
||||||
|
assert.equal(manager._activeTimers.size, 0);
|
||||||
|
assert.equal(timers.length, timersBeforeDisconnect);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.setTimeout = realSetTimeout;
|
globalThis.setTimeout = realSetTimeout;
|
||||||
globalThis.clearTimeout = realClearTimeout;
|
globalThis.clearTimeout = realClearTimeout;
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ function fake(config = {}) {
|
|||||||
this.delivered.push({ message, type });
|
this.delivered.push({ message, type });
|
||||||
},
|
},
|
||||||
_hasTurnServer: EnhancedSecureWebRTCManager.prototype._hasTurnServer,
|
_hasTurnServer: EnhancedSecureWebRTCManager.prototype._hasTurnServer,
|
||||||
_isRelayOnlyMode: EnhancedSecureWebRTCManager.prototype._isRelayOnlyMode
|
_isRelayOnlyMode: EnhancedSecureWebRTCManager.prototype._isRelayOnlyMode,
|
||||||
|
_setRelayOnlyMode: EnhancedSecureWebRTCManager.prototype._setRelayOnlyMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,27 @@ function fake(config = {}) {
|
|||||||
assert.equal(config.iceTransportPolicy, 'relay');
|
assert.equal(config.iceTransportPolicy, 'relay');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime toggles keep the canonical privacy state synchronized.
|
||||||
|
{
|
||||||
|
const manager = fake({ privacyMode: 'standard', iceServers: [{ urls: 'turn:turn.example.test:3478' }] });
|
||||||
|
manager._setRelayOnlyMode(true);
|
||||||
|
assert.equal(manager._config.webrtc.privacyMode, 'relay-only');
|
||||||
|
assert.equal(manager._config.webrtc.relayOnly, true);
|
||||||
|
assert.equal(EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager).iceTransportPolicy, 'relay');
|
||||||
|
|
||||||
|
manager._setRelayOnlyMode(false);
|
||||||
|
assert.equal(manager._config.webrtc.privacyMode, 'standard');
|
||||||
|
assert.equal(manager._config.webrtc.relayOnly, false);
|
||||||
|
assert.equal(EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager).iceTransportPolicy, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical privacyMode wins over a stale legacy alias.
|
||||||
|
{
|
||||||
|
const manager = fake({ privacyMode: 'standard', relayOnly: true, iceServers: [{ urls: 'turn:turn.example.test:3478' }] });
|
||||||
|
assert.equal(manager._isRelayOnlyMode(), false);
|
||||||
|
assert.equal(EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager).iceTransportPolicy, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
// Missing TURN in standard mode warns clearly and visibly.
|
// Missing TURN in standard mode warns clearly and visibly.
|
||||||
{
|
{
|
||||||
const manager = fake();
|
const manager = fake();
|
||||||
|
|||||||
Reference in New Issue
Block a user