fix(descriptor): rebuild SDP that Firefox accepts; release v5.8.1
Live cross-browser testing found three defects in the SBQ2 SDP template, all
invisible to Chrome and all fatal to Firefox.
The candidate lines omitted raddr/rport on srflx and relay candidates. RFC 8839
section 5.1 makes rel-addr and rel-port mandatory for non-host candidates even
though ICE never reads them; Chrome tolerates the omission and Firefox drops the
candidate. Relay-only connections to Firefox failed 0/8 against 8/8 for the
browser's own SDP. The STUN and TURN profiles hid it because a host pair
connected instead -- the relay candidates were never actually needed there.
The template also advertised ice-options:trickle without ever closing the
candidate set, though a descriptor is a complete one-shot set with no channel to
trickle over, and hard-coded the m= port and c= line to the 9 / 0.0.0.0 null
default candidate, which is the trickle convention for "nothing gathered yet".
Both are now correct: no trickle, an explicit a=end-of-candidates, and the most
publicly reachable candidate as the default.
All three are serializer-side and cost zero descriptor bytes; sizes are
unchanged at 98-149 bytes, QR version 6-8. Verified 48/48 across all 16
combinations of {Chrome, Firefox} squared and four network profiles, with every
relay-only pair now connecting over the relay.
This commit is contained in:
1 parent
6e82cfcae2
commit
fb959d7ca7
12 files changed
+207
-42
No files matched your search
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## v5.8.1 — SBQ2 rebuilds SDP that Firefox accepts
|
||||
|
||||
Still nothing in the application calls the SBQ2 descriptor; this fixes defects in
|
||||
it found by running live connections between real browsers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The rebuilt SDP omitted `raddr`/`rport` on srflx and relay candidates. RFC 8839
|
||||
§5.1 makes them mandatory for non-host candidates, and while Chrome tolerates
|
||||
the omission, **Firefox drops the candidate entirely**. Relay-only connections
|
||||
to Firefox failed 0/8 where the browser's own SDP succeeded 8/8. The STUN and
|
||||
TURN profiles hid it, because a host candidate pair connected instead.
|
||||
|
||||
- The template advertised `a=ice-options:trickle` and never closed the candidate
|
||||
set. A descriptor is a complete one-shot set with no channel to trickle over,
|
||||
so this promised candidates that could never arrive. Trickle is gone and
|
||||
`a=end-of-candidates` is emitted.
|
||||
|
||||
- The `m=` port and `c=` line were hard-coded to the `9` / `0.0.0.0` null default
|
||||
candidate, which is the trickle-ICE "nothing gathered yet" convention and false
|
||||
here. The most publicly reachable candidate is advertised instead — relay, then
|
||||
srflx, then host — falling back to the null form only when every candidate is
|
||||
mDNS, as Chrome does.
|
||||
|
||||
None of these change the descriptor: all three are serializer-side and cost zero
|
||||
bytes. Sizes are unchanged at 98–149 bytes, QR version 6–8.
|
||||
|
||||
Verified across all 16 combinations of {Chrome, Firefox} x {Chrome, Firefox} and
|
||||
four network profiles: **48/48 connections**, with every relay-only pair now
|
||||
connecting over the relay.
|
||||
|
||||
|
||||
## v5.8.0 — A connection descriptor that fits in a small QR code
|
||||
|
||||
No change to how messages are protected, and no change to how a connection is
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
[](#forward-secrecy)
|
||||
|
||||
Vendored
+1
-1
@@ -19984,7 +19984,7 @@ var SecureMasterKeyManager = class {
|
||||
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
|
||||
|
||||
// package.json
|
||||
var version = "5.8.0";
|
||||
var version = "5.8.1";
|
||||
|
||||
// src/components/ui/Header.jsx
|
||||
var APP_VERSION = `v${version}`;
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../node_modules/dompurify/src/utils.ts", "../node_modules/dompurify/src/tags.ts", "../node_modules/dompurify/src/attrs.ts", "../node_modules/dompurify/src/regexp.ts", "../node_modules/dompurify/src/purify.ts", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/webrtc/config.js", "../src/network/webrtc/sdp.js", "../src/network/webrtc/audio.js", "../src/network/webrtc/video.js", "../src/network/webrtc/adaptation/metrics.js", "../src/network/webrtc/adaptation/controller.js", "../src/crypto/DoubleRatchet.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../package.json", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/CommunityCTA.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.jsx", "../src/components/ui/CallUI.jsx"],
|
||||
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
|
||||
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
|
||||
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AASA,QAAM,gCAAN,MAAoC;AAAA,MAClC,YAAY,SAAS,CAAC,GAAG;AAEvB,aAAK,aAAc,OAAO,iBAAiB,eAAe,gBAAgB,OAAO,aAAa,eAAe,WACzG,aAAa,aACb;AACJ,aAAK,cAAc,KAAK,eAAe;AACvC,aAAK,cAAc;AACnB,aAAK,gBAAgB,SAAS;AAC9B,aAAK,oBAAoB,CAAC;AAC1B,aAAK,eAAe,OAAO,gBAAgB;AAC3C,aAAK,cAAc,OAAO,eAAe;AACzC,aAAK,uBAAuB;AAC5B,aAAK,iBAAiB,OAAO,kBAAkB,CAAC;AAGhD,aAAK,kBAAkB,OAAO;AAG9B,aAAK,SAAS,KAAK,kBAAkB;AACrC,aAAK,mBAAmB,KAAK,yBAAyB;AAEtD,aAAK,uBAAuB;AAC5B,aAAK,mBAAmB;AAAA,MAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,qBAAqB;AAAA,MAErB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,oBAAoB;AAClB,YAAI,OAAO,SAAS,WAAW,aAAa;AAC1C,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,aAAa,aAAa;AACnD,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,iBAAiB,aAAa;AACvD,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,2BAA2B;AACzB,YAAI,OAAO,SAAS,WAAW,aAAa;AAC1C,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,aAAa,aAAa;AACnD,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,iBAAiB,aAAa;AACvD,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,iBAAiB;AAEf,YAAI,KAAK,UAAU,OAAO,SAAS,KAAK,MAAM,MAAM,aAAa;AAC/D,iBAAO,CAAC,SAAS,KAAK,MAAM;AAAA,QAC9B;AAGA,YAAI,OAAO,SAAS,aAAa,YAAY;AAC3C,iBAAO,SAAS,SAAS;AAAA,QAC3B;AAGA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,yBAAyB;AAEvB,YAAI,OAAO,SAAS,qBAAqB,eAAe,OAAO,SAAS,KAAK,MAAM,MAAM,aAAa;AACpG,mBAAS,iBAAiB,KAAK,kBAAkB,MAAM;AACrD,iBAAK,cAAc,KAAK,eAAe;AAEvC,gBAAI,KAAK,aAAa;AACpB,mBAAK,iBAAiB;AACtB,mBAAK,uBAAuB;AAAA,YAC9B;AAAA,UACF,CAAC;AAAA,QACH;AAGA,eAAO,iBAAiB,SAAS,MAAM;AACrC,eAAK,cAAc,KAAK,eAAe;AACvC,cAAI,KAAK,aAAa;AACpB,iBAAK,iBAAiB;AAAA,UACxB;AAAA,QACF,CAAC;AAED,eAAO,iBAAiB,QAAQ,MAAM;AACpC,eAAK,cAAc,KAAK,eAAe;AAAA,QACzC,CAAC;AAGD,eAAO,iBAAiB,gBAAgB,MAAM;AAC5C,eAAK,uBAAuB;AAAA,QAC9B,CAAC;AAAA,MACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,MAAM,oBAAoB;AAExB,YAAI,CAAC,KAAK,mBAAmB,EAAE,kBAAkB,SAAS;AACxD,iBAAO;AAAA,QACT;AAEA,YAAI,KAAK,eAAe,WAAW;AACjC,iBAAO;AAAA,QACT;AAEA,YAAI,KAAK,eAAe,UAAU;AAChC,iBAAO;AAAA,QACT;AAEA,YAAI;AACF,eAAK,aAAa,MAAM,aAAa,kBAAkB;AACvD,iBAAO,KAAK,eAAe;AAAA,QAC7B,SAAS,OAAO;AACd,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,cAAc;AACZ,YAAI,KAAK,cAAc,GAAG;AACxB,mBAAS,QAAQ,IAAI,KAAK,WAAW,KAAK,KAAK,aAAa;AAAA,QAC9D,OAAO;AACL,mBAAS,QAAQ,KAAK;AAAA,QACxB;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,aAAaA,OAAM;AACjB,YAAI,OAAOA,UAAS,UAAU;AAC5B,iBAAO;AAAA,QACT;AAGA,cAAM,MAAM,SAAS,cAAc,KAAK;AACxC,YAAI,cAAcA;AAClB,eAAO,IAAI,UACR,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ,EACtB,UAAU,GAAG,GAAG;AAAA,MACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,gBAAgB,KAAK;AACnB,YAAI,CAAC,IAAK,QAAO;AAEjB,YAAI;AACF,gBAAM,YAAY,IAAI,IAAI,KAAK,OAAO,SAAS,MAAM;AAGrD,cAAI,UAAU,aAAa,YAAY,UAAU,aAAa,SAAS;AAErE,gBAAI,KAAK,eAAe,SAAS,GAAG;AAClC,oBAAM,YAAY,KAAK,eAAe;AAAA,gBAAK,YACzC,UAAU,WAAW;AAAA,cACvB;AACA,qBAAO,YAAY,UAAU,OAAO;AAAA,YACtC;AACA,mBAAO,UAAU;AAAA,UACnB;AAEA,iBAAO;AAAA,QACT,SAAS,OAAO;AACd,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,iBAAiB;AACf,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,KAAK,uBAAuB,KAAK,aAAa;AACtD,iBAAO;AAAA,QACT;AACA,aAAK,uBAAuB;AAC5B,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,OAAO,YAAY,SAAS,UAAU,CAAC,GAAG;AAExC,YAAI,OAAO,iBAAiB,aAAa;AACvC,iBAAO;AAAA,QACT;AAEA,aAAK,cAAc,KAAK,eAAe;AAGvC,YAAI,KAAK,aAAa;AACpB,iBAAO;AAAA,QACT;AAGA,YAAI,KAAK,eAAe,WAAW;AACjC,iBAAO;AAAA,QACT;AAGA,YAAI,CAAC,KAAK,eAAe,GAAG;AAC1B,iBAAO;AAAA,QACT;AAGA,cAAM,iBAAiB,KAAK,aAAa,cAAc,SAAS;AAChE,cAAM,cAAc,KAAK,aAAa,WAAW,EAAE;AACnD,cAAM,WAAW,KAAK,gBAAgB,QAAQ,IAAI,KAAK;AAGvD,YAAI,KAAK,kBAAkB,UAAU,KAAK,cAAc;AACtD,eAAK,uBAAuB;AAAA,QAC9B;AAEA,YAAI;AAEF,gBAAM,eAAe,IAAI;AAAA,YACvB,GAAG,cAAc;AAAA,YACjB;AAAA,cACE,MAAM,YAAY,UAAU,GAAG,GAAG;AAAA;AAAA,cAClC,MAAM;AAAA,cACN,OAAO;AAAA,cACP,KAAK,QAAQ,QAAQ,YAAY,SAAS;AAAA;AAAA,cAC1C,oBAAoB;AAAA;AAAA,cACpB,QAAQ,QAAQ,UAAU;AAAA;AAAA,cAE1B,SAAS,UAAU,UAAU,CAAC,KAAK,KAAK,GAAG,IAAI;AAAA;AAAA,cAE/C,MAAM;AAAA,gBACJ,UAAU,KAAK,aAAa,QAAQ,QAAQ;AAAA,gBAC5C,WAAW,KAAK,IAAI;AAAA;AAAA,cAEtB;AAAA,YACF;AAAA,UACF;AAGA,eAAK;AACL,eAAK,YAAY;AAGjB,eAAK,kBAAkB,KAAK,YAAY;AAGxC,uBAAa,UAAU,CAAC,UAAU;AAChC,kBAAM,eAAe;AACrB,mBAAO,MAAM;AACb,yBAAa,MAAM;AAGnB,gBAAI,OAAO,QAAQ,YAAY,YAAY;AACzC,kBAAI;AACF,wBAAQ,QAAQ,QAAQ,QAAQ;AAAA,cAClC,SAAS,OAAO;AACd,wBAAQ,MAAM,6CAA6C,KAAK;AAAA,cAClE;AAAA,YACF;AAAA,UACF;AAGA,uBAAa,UAAU,CAAC,UAAU;AAChC,oBAAQ,MAAM,+CAA+C,KAAK;AAAA,UACpE;AAGA,gBAAM,mBAAmB,KAAK,IAAI,QAAQ,aAAa,KAAM,GAAK;AAClE,qBAAW,MAAM;AACf,yBAAa,MAAM;AACnB,iBAAK,gBAAgB,YAAY;AAAA,UACnC,GAAG,gBAAgB;AAEnB,iBAAO;AAAA,QAET,SAAS,OAAO;AACd,kBAAQ,MAAM,kDAAkD,KAAK;AACrE,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,gBAAgB,cAAc;AAC5B,cAAM,QAAQ,KAAK,kBAAkB,QAAQ,YAAY;AACzD,YAAI,QAAQ,IAAI;AACd,eAAK,kBAAkB,OAAO,OAAO,CAAC;AAAA,QACxC;AAAA,MACF;AAAA;AAAA;AAAA;AAAA,MAKA,yBAAyB;AACvB,aAAK,kBAAkB,QAAQ,kBAAgB;AAC7C,cAAI;AACF,yBAAa,MAAM;AAAA,UACrB,SAAS,OAAO;AAAA,UAEhB;AAAA,QACF,CAAC;AACD,aAAK,oBAAoB,CAAC;AAAA,MAC5B;AAAA;AAAA;AAAA;AAAA,MAKA,mBAAmB;AACjB,aAAK,cAAc;AACnB,aAAK,YAAY;AAAA,MACnB;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,YAAY;AACV,eAAO;AAAA,UACL,YAAY,KAAK;AAAA,UACjB,aAAa,KAAK;AAAA,UAClB,aAAa,KAAK;AAAA,UACLine truncated
|
||||
"names": ["text", "NotificationIntegration", "entries", "Object", "setPrototypeOf", "isFrozen", "getPrototypeOf", "getOwnPropertyDescriptor", "freeze", "seal", "create", "_ref", "Reflect", "apply", "construct", "x", "func", "thisArg", "_len", "arguments", "length", "args", "Array", "_key", "Func", "_len2", "_key2", "arrayForEach", "unapply", "prototype", "forEach", "arrayLastIndexOf", "lastIndexOf", "arrayPop", "pop", "arrayPush", "push", "arraySplice", "splice", "arrayIsArray", "isArray", "stringToLowerCase", "String", "toLowerCase", "stringToString", "toString", "stringMatch", "match", "stringReplace", "replace", "stringIndexOf", "indexOf", "stringTrim", "trim", "numberToString", "Number", "booleanToString", "Boolean", "bigintToString", "BigInt", "symbolToString", "Symbol", "objectHasOwnProperty", "hasOwnProperty", "objectToString", "regExpTest", "RegExp", "test", "typeErrorCreate", "unconstruct", "TypeError", "lastIndex", "_len3", "_key3", "_len4", "_key4", "addToSet", "set", "array", "transformCaseFunc", "l", "element", "lcElement", "cleanArray", "index", "isPropertyExist", "clone", "object", "newObject", "_ref2", "_ref3", "_slicedToArray", "property", "value", "constructor", "stringifyValue", "valueAsRecord", "valueToString", "lookupGetter", "stringified", "prop", "desc", "get", "fallbackValue", "isRegex", "_unused", "html", "svg", "svgFilters", "svgDisallowed", "mathMl", "mathMlDisallowed", "text", "xml", "MUSTACHE_EXPR", "ERB_EXPR", "TMPLIT_EXPR", "DATA_ATTR", "ARIA_ATTR", "IS_ALLOWED_URI", "IS_SCRIPT_OR_DATA", "ATTR_WHITESPACE", "DOCTYPE_NAME", "CUSTOM_ELEMENT", "ELEMENT_MARKUP_PROBE", "COMMENT_MARKUP_PROBE", "FALLBACK_TAG_CLOSE", "SELF_CLOSING_TAG", "NODE_TYPE", "attribute", "cdataSection", "entityReference", "entityNode", "processingInstruction", "comment", "document", "documentType", "documentFragment", "notation", "getGlobal", "window", "_createTrustedTypesPolicy", "trustedTypes", "purifyHostElement", "createPolicy", "suffix", "ATTR_NAME", "hasAttribute", "getAttribute", "policyName", "createHTML", "createScriptURL", "scriptUrl", "_", "console", "warn", "_createHooksMap", "afterSanitizeAttributes", "afterSanitizeElements", "afterSanitizeShadowDOM", "beforeSanitizeAttributes", "beforeSanitizeElements", "beforeSanitizeShadowDOM", "uponSanitizeAttribute", "uponSanitizeElement", "uponSanitizeShadowNode", "_resolveSetOption", "cfg", "key", "fallback", "options", "base", "transform", "createDOMPurify", "undefined", "DOMPurify", "root", "version", "VERSION", "removed", "nodeType", "Element", "isSupported", "originalDocument", "currentScript", "DocumentFragment", "HTMLTemplateElement", "Node", "NodeFilter", "_window$NamedNodeMap", "NamedNodeMap", "MozNamedAttrMap", "HTMLFormElement", "DOMParser", "ElementPrototype", "cloneNode", "remove", "getNextSibling", "getChildNodes", "getParentNode", "getShadowRoot", "getAttributes", "getNodeType", "getNodeName", "template", "createElement", "content", "ownerDocument", "trustedTypesPolicy", "emptyHTML", "defaultTrustedTypesPolicy", "defaultTrustedTypesPolicyResolved", "IN_TRUSTED_TYPES_POLICY", "_assertNotInTrustedTypesPolicy", "_createTrustedHTML", "_createTrustedScriptURL", "_getDefaultTrustedTypesPolicy", "_document", "implementation", "createNodeIterator", "createDocumentFragment", "getElementsByTagName", "importNode", "hooks", "createHTMLDocument", "EXPRESSIONS", "ALLOWED_TAGS", "DEFAULT_ALLOWED_TAGS", "TAGS", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "ATTRS", "CUSTOM_ELEMENT_HANDLING", "tagNameCheck", "writable", "configurable", "enumerable", "attributeNameCheck", "allowCustomizedBuiltInElements", "FORBID_TAGS", "FORBID_ATTR", "EXTRA_ELEMENT_HANDLING", "tagCheck", "attributeCheck", "ALLOW_ARIA_ATTR", "ALLOW_DATA_ATTR", "ALLOW_UNKNOWN_PROTOCOLS", "ALLOW_SELF_CLOSE_IN_ATTR", "SAFE_FOR_TEMPLATES", "SAFE_FOR_XML", "WHOLE_DOCUMENT", "SET_CONFIG", "FORCE_BODY", "RETURN_DOM", "RETURN_DOM_FRAGMENT", "RETURN_TRUSTED_TYPE", "SANITIZE_DOM", "SANITIZE_NAMED_PROPS", "SANITIZE_NAMED_PROPS_PREFIX", "KEEP_CONTENT", "IN_PLACE", "USE_PROFILES", "FORBID_CONTENTS", "DEFAULT_FORBID_CONTENTS", "DATA_URI_TAGS", "DEFAULT_DATA_URI_TAGS", "URI_SAFE_ATTRIBUTES", "DEFAULT_URI_SAFE_ATTRIBUTES", "MATHML_NAMESPACE", "SVG_NAMESPACE", "HTML_NAMESPACE", "NAMESPACE", "IS_EMPTY_INPUT", "ALLOWED_NAMESPACES", "DEFAULT_ALLOWED_NAMESPACES", "DEFAULT_MATHML_TEXT_INTEGRATION_POINTS", "MATHML_TEXT_INTEGRATION_POINTS", "DEFAULT_HTML_INTEGRATION_POINTS", "HTML_INTEGRATION_POINTS", "COMMON_SVG_AND_HTML_ELEMENTS", "PARSER_MEDIA_TYPE", "SUPPORTED_PARSER_MEDIA_TYPES", "DEFAULT_PARSER_MEDIA_TYPE", "CONFIG", "formElement", "isRegexOrFunction", "testValue", "Function", "_parseConfig", "ALLOWED_URI_REGEXP", "customElementHandling", "ADD_TAGS", "ADD_ATTR", "ADD_URI_SAFE_ATTR", "ADD_FORBID_CONTENTS", "table", "tbody", "TRUSTED_TYPES_POLICY", "previousTrustedTypesPolicy", "error", "ALL_SVG_TAGS", "ALL_MATHML_TAGS", "_checkSvgNamespace", "tagName", "parent", "parentTagName", "namespaceURI", "_checkMathMlNamespaceLine truncated
|
||||
}
|
||||
+2
-2
@@ -7,7 +7,7 @@ this document describes.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Release | v5.8.0 |
|
||||
| Release | v5.8.1 |
|
||||
| Protocol version | 4.1 |
|
||||
| Ratchet wire version | 1 |
|
||||
|
||||
@@ -214,5 +214,5 @@ worse than one that reports nothing.
|
||||
|
||||
## Scope
|
||||
|
||||
This describes the browser implementation as it stands in v5.8.0. It is not a
|
||||
This describes the browser implementation as it stands in v5.8.1. It is not a
|
||||
substitute for independent cryptographic review.
|
||||
+29
-3
@@ -83,9 +83,35 @@ Foundation and priority are **not** transmitted. Priority only orders
|
||||
connectivity checks, and each peer computes its own local priorities anyway; the
|
||||
serializer re-derives RFC 8445 §5.1.2.1 values with `localPref = 65535 - index`,
|
||||
so the sender's ordering intent survives at zero cost. Foundations are grouped by
|
||||
kind and transport, satisfying both halves of §5.1.1.3. `raddr`/`rport` are
|
||||
diagnostics that ICE does not consume, and `generation`/`network-cost` are Chrome
|
||||
extensions.
|
||||
kind and transport, satisfying both halves of §5.1.1.3. `generation` and
|
||||
`network-cost` are Chrome extensions and are dropped.
|
||||
|
||||
### What the serializer must emit anyway
|
||||
|
||||
Three things are not in the descriptor but must appear in the rebuilt SDP. All
|
||||
three are constants or derived from data already present, so none costs a byte —
|
||||
and each was found by a live cross-browser test rather than by reading the spec.
|
||||
|
||||
- **`raddr` / `rport` on every srflx, prflx and relay candidate.** RFC 8839 §5.1
|
||||
makes rel-addr and rel-port mandatory for non-host candidates even though ICE's
|
||||
own algorithm never reads them. An earlier draft left them out as
|
||||
"diagnostics". Chrome accepts such a line; **Firefox drops the candidate.**
|
||||
Measured: relay-only connections to Firefox went 0/8 while the browser's own
|
||||
SDP went 8/8, and the fault was invisible in the STUN and TURN profiles because
|
||||
a host pair connected instead. `0.0.0.0`/`0` (or `::`/`0`) is emitted — the
|
||||
same placeholder Chrome uses when it has no base address to disclose.
|
||||
|
||||
- **No `a=ice-options:trickle`, and an explicit `a=end-of-candidates`.** A
|
||||
descriptor is a complete one-shot candidate set; there is no channel to trickle
|
||||
over. Advertising trickle promises candidates that can never arrive and leaves
|
||||
the peer waiting for them.
|
||||
|
||||
- **A real default candidate on the `m=` port and `c=` line.** `m=application 9`
|
||||
with `c=IN IP4 0.0.0.0` is the trickle convention for "nothing gathered yet"
|
||||
(RFC 8840 §4.1) and is simply false here. The serializer advertises the most
|
||||
publicly reachable candidate — relay, then srflx, then host, which is the order
|
||||
Chrome uses and the reverse of ICE priority — falling back to the null form
|
||||
only when every candidate is mDNS, which is also what Chrome does.
|
||||
|
||||
### TLV extension area
|
||||
|
||||
|
||||
+22
-22
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786031423211">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786051485967">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -90,7 +90,7 @@
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786031423211">
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786051485967">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
|
||||
@@ -99,7 +99,7 @@
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786031423211">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786051485967">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -183,7 +183,7 @@
|
||||
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
||||
both execute in document order after parsing, so React still runs before the
|
||||
app modules below, but the parser / first paint is no longer blocked. -->
|
||||
<script defer src="config/ice-servers.js?v=1786031423211"></script>
|
||||
<script defer src="config/ice-servers.js?v=1786051485967"></script>
|
||||
<script defer src="libs/react/react.production.min.js"></script>
|
||||
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
|
||||
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
||||
@@ -191,8 +191,8 @@
|
||||
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
||||
<script defer src="libs/prism/prism.js"></script>
|
||||
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786031423211">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786031423211">
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786051485967">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786051485967">
|
||||
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
|
||||
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
|
||||
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
|
||||
@@ -200,31 +200,31 @@
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786051485967">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786051485967">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786051485967">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786031423211"></script>
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786051485967"></script>
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="libs/prism/prism.css">
|
||||
</noscript>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786031423211"></script>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786051485967"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script defer src="src/utils/updateManager.js?v=1786031423211"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786031423211"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786031423211"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786031423211"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1786051485967"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786051485967"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786051485967"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786051485967"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1786031423211"></script>
|
||||
<script type="module" src="dist/app.js?v=1786031423211"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1786051485967"></script>
|
||||
<script type="module" src="dist/app.js?v=1786051485967"></script>
|
||||
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786031423211"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786031423211" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786031423211" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786031423211"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786031423211">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786051485967"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786051485967" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786051485967" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786051485967"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786051485967">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1786031423211",
|
||||
"buildVersion": "1786031423211",
|
||||
"appVersion": "5.8.0",
|
||||
"buildTime": "2026-08-06T15:50:23.257Z",
|
||||
"buildId": "1786031423211-3212138",
|
||||
"gitHash": "3212138",
|
||||
"version": "1786051485967",
|
||||
"buildVersion": "1786051485967",
|
||||
"appVersion": "5.8.1",
|
||||
"buildTime": "2026-08-06T21:24:46.003Z",
|
||||
"buildId": "1786051485967-6e82cfc",
|
||||
"gitHash": "6e82cfc",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-08-06T15:50:23.258Z"
|
||||
"generatedAt": "2026-08-06T21:24:46.005Z"
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "5.8.0",
|
||||
"version": "5.8.1",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
|
||||
@@ -659,6 +659,37 @@ function renderAddr(kind, addr) {
|
||||
*/
|
||||
export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
const isOffer = desc.type === TYPE.OFFER;
|
||||
|
||||
// The m-line port and c-line carry the DEFAULT CANDIDATE, and they must be
|
||||
// real whenever we have one.
|
||||
//
|
||||
// `m=application 9` with `c=IN IP4 0.0.0.0` is the trickle-ICE convention
|
||||
// for "no candidate yet" (RFC 8840 §4.1). A descriptor is never trickle —
|
||||
// the candidate set is complete by construction — so a null default
|
||||
// candidate here is simply a lie, and Firefox acts on it: with a relay-only
|
||||
// Chrome offer, rebuilding with 9/0.0.0.0 connected 0/4 while the browser's
|
||||
// own SDP connected 4/4. Advertising the highest-priority routable candidate
|
||||
// instead, exactly as Chrome and Firefox do, costs zero descriptor bytes
|
||||
// because that candidate is already in the list.
|
||||
//
|
||||
// An mDNS candidate cannot appear here (c= needs a literal address), so if
|
||||
// that is all we have we fall back to the null form — which is also what
|
||||
// Chrome emits in that case.
|
||||
// Preference order is relay > srflx > host — the most publicly reachable
|
||||
// address, which is what the default candidate is for, and the order Chrome
|
||||
// itself uses (its STUN-profile SDP names the srflx address and its TURN
|
||||
// profile names the relay). That is the reverse of ICE priority, which
|
||||
// ranks host first.
|
||||
const DEFAULT_RANK = { relay: 0, srflx: 1, host: 2 };
|
||||
const def = desc.candidates
|
||||
.filter((c) => c.kind !== KIND.HOST_MDNS && c.tcptype === 0)
|
||||
.sort((x, y) => DEFAULT_RANK[KIND_TYPE[x.kind]] - DEFAULT_RANK[KIND_TYPE[y.kind]])[0];
|
||||
const defIsV6 = def && KIND_FAMILY[def.kind] === 'v6';
|
||||
const mPort = def ? def.port : 9;
|
||||
const cLine = def
|
||||
? `c=IN IP${defIsV6 ? '6' : '4'} ${renderAddr(def.kind, def.addr)}`
|
||||
: 'c=IN IP4 0.0.0.0';
|
||||
|
||||
const lines = [
|
||||
'v=0',
|
||||
`o=- ${sessionId} 2 IN IP4 127.0.0.1`,
|
||||
@@ -666,8 +697,8 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
't=0 0',
|
||||
'a=group:BUNDLE 0',
|
||||
'a=msid-semantic: WMS',
|
||||
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'c=IN IP4 0.0.0.0',
|
||||
`m=application ${mPort} UDP/DTLS/SCTP webrtc-datachannel`,
|
||||
cLine,
|
||||
'a=ice-ufrag:' + desc.ufrag,
|
||||
'a=ice-pwd:' + desc.pwd,
|
||||
// Deliberately NOT `a=ice-options:trickle`. A descriptor is a complete,
|
||||
@@ -696,6 +727,18 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
// transport satisfies both halves of that.
|
||||
const foundation = String(c.kind * 4 + c.tcptype + 1);
|
||||
let line = `a=candidate:${foundation} 1 ${transport} ${priority} ${renderAddr(c.kind, c.addr)} ${c.port} typ ${ctype}`;
|
||||
// rel-addr/rel-port are MANDATORY for srflx, prflx and relay candidates
|
||||
// (RFC 8839 §5.1), even though ICE's own algorithm never reads them.
|
||||
// Omitting them is not a harmless economy: Chrome accepts such a line,
|
||||
// Firefox drops the candidate outright. That cost 0/8 relay-only
|
||||
// connections to Firefox while the browser's own SDP made 8/8, and it
|
||||
// stayed invisible in the STUN and TURN profiles because a host pair
|
||||
// connected instead. The value is not transmitted — 0.0.0.0/0 is what
|
||||
// Chrome itself emits when it has no base address to disclose, and it
|
||||
// satisfies the grammar at zero descriptor bytes.
|
||||
if (ctype !== 'host') {
|
||||
line += KIND_FAMILY[c.kind] === 'v6' ? ' raddr :: rport 0' : ' raddr 0.0.0.0 rport 0';
|
||||
}
|
||||
if (transport === 'tcp') line += ` tcptype ${TCPTYPE[c.tcptype]}`;
|
||||
return line;
|
||||
});
|
||||
@@ -703,7 +746,7 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
// (RFC 8838 §14), so the peer stops waiting for more and can start failing
|
||||
// pairs promptly instead of sitting in checking until a timeout.
|
||||
candLines.push('a=end-of-candidates');
|
||||
const at = lines.indexOf('c=IN IP4 0.0.0.0') + 1;
|
||||
const at = lines.indexOf(cLine) + 1;
|
||||
lines.splice(at, 0, ...candLines);
|
||||
|
||||
return { type: isOffer ? 'offer' : 'answer', sdp: lines.join('\r\n') + '\r\n' };
|
||||
|
||||
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
|
||||
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
|
||||
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
|
||||
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
|
||||
const SW_BUILD_VERSION = '1786031423211';
|
||||
const SW_BUILD_VERSION = '1786051485967';
|
||||
|
||||
// Load version from meta.json on install
|
||||
async function getAppVersion() {
|
||||
|
||||
@@ -191,6 +191,70 @@ for (const [browser, fixtures] of [['chrome', chrome], ['firefox', firefox]]) {
|
||||
assert.deepEqual([...re.candidates[2].addr].slice(12), [198, 51, 100, 9], 'NAT64 tail round-trips');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SDP grammar conformance of the rebuilt candidate lines
|
||||
//
|
||||
// Regression guard. rel-addr/rel-port are mandatory for srflx/prflx/relay
|
||||
// (RFC 8839 §5.1). Leaving them out is silently fine in Chrome and fatal in
|
||||
// Firefox, which drops the candidate: measured 0/8 relay-only connections to
|
||||
// Firefox versus 8/8 for the browser's own SDP, and invisible in the STUN and
|
||||
// TURN profiles because a host pair connected instead.
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const sdp = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 192.168.1.9 40000 typ host',
|
||||
'a=candidate:2 1 udp 1677729535 203.0.113.4 40001 typ srflx raddr 192.168.1.9 rport 40000',
|
||||
'a=candidate:3 1 udp 50340351 144.172.96.126 40002 typ relay raddr 203.0.113.4 rport 40001',
|
||||
'a=candidate:4 1 udp 50341375 2001:db8::5 40003 typ relay',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
|
||||
const out = serializeSdp(decodeDescriptor(await build(sdp, TYPE.OFFER))).sdp;
|
||||
for (const line of out.split('\r\n').filter((l) => l.startsWith('a=candidate:'))) {
|
||||
const type = line.split(' typ ')[1].split(' ')[0];
|
||||
if (type === 'host') {
|
||||
assert.ok(!line.includes('raddr'), `host candidates must not carry raddr: ${line}`);
|
||||
} else {
|
||||
assert.match(line, / raddr (0\.0\.0\.0|::) rport 0/, `${type} candidate needs rel-addr/rel-port: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The default candidate must be a real, routable address whenever one
|
||||
// exists — `m=... 9` with `c=IN IP4 0.0.0.0` is the trickle "none yet"
|
||||
// form, and a descriptor is never trickle.
|
||||
assert.ok(!out.includes('a=ice-options:trickle'), 'a complete candidate set must not advertise trickle');
|
||||
assert.match(out, /^a=end-of-candidates$/m, 'the candidate set is explicitly closed');
|
||||
|
||||
// relay beats srflx beats host: the default candidate is the most publicly
|
||||
// reachable one, matching what Chrome emits. Asserted on a v4-only set so
|
||||
// the expected winner is unambiguous.
|
||||
const v4only = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 192.168.1.9 40000 typ host',
|
||||
'a=candidate:2 1 udp 1677729535 203.0.113.4 40001 typ srflx raddr 192.168.1.9 rport 40000',
|
||||
'a=candidate:3 1 udp 50340351 144.172.96.126 40002 typ relay raddr 203.0.113.4 rport 40001',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
const vOut = serializeSdp(decodeDescriptor(await build(v4only, TYPE.OFFER))).sdp;
|
||||
assert.match(vOut, /^m=application 40002 /m, 'm-line carries the relay port as default');
|
||||
assert.match(vOut, /^c=IN IP4 144\.172\.96\.126$/m, 'c-line carries the relay address as default');
|
||||
|
||||
// With only an mDNS candidate there is no literal address to advertise, so
|
||||
// the null form is correct — and it is what Chrome emits in that case too.
|
||||
const mdnsOnly = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 7d9c00c2-bcef-48b6-9166-428899e0582e.local 40000 typ host',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
const mOut = serializeSdp(decodeDescriptor(await build(mdnsOnly, TYPE.OFFER))).sdp;
|
||||
assert.match(mOut, /^m=application 9 /m);
|
||||
assert.match(mOut, /^c=IN IP4 0\.0\.0\.0$/m);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TCP candidates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user