Replace free TURN with self-hosted coturn; fix Safari connectivity; release v5.4.10
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

- config/ice-servers.prod.js: swap ExpressTURN for self-hosted coturn at
  turn.securebit.chat (TURN udp/tcp on 3478, TURNS/TLS on 443). Long-lived
  REST-API credential (expiry capped at int32 max for coturn compatibility).
- Add raw-IP STUN/TURN fallback (144.172.96.126): Safari's WebRTC layer fails
  to resolve STUN/TURN hostnames on some networks and gathers no srflx/relay
  candidates; reaching the server by IP fixes cross-browser (Safari<->Chrome)
  connections. Harmless to other browsers.
- deploy/nginx.conf: never long-cache /config/ice-servers.js so clients don't
  lock onto a stale server list.
- Bump version to 5.4.10 (header + init banner).
This commit is contained in:
lockbitchat
2026-07-22 18:10:17 -04:00
parent c98a01b1d5
commit 0de8ab2d54
13 changed files with 83 additions and 41 deletions
+39 -1
View File
@@ -6,5 +6,43 @@
window.SECUREBIT_ICE_SERVERS = [ window.SECUREBIT_ICE_SERVERS = [
{ urls: 'stun:stun.cloudflare.com:3478' }, { urls: 'stun:stun.cloudflare.com:3478' },
{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' } { urls: 'stun:stun1.l.google.com:19302' },
// Raw-IP STUN (same coturn box as turn.securebit.chat). Required fallback: some
// clients — notably Safari on certain networks — fail to resolve STUN/TURN
// hostnames inside their WebRTC layer even though normal page DNS works, so they
// gather zero srflx/relay candidates and can't connect. Reaching the server by IP
// bypasses that. Harmless to other browsers.
{ urls: 'stun:144.172.96.126:3478' },
{
// SecureBit self-hosted coturn relay (turn.securebit.chat) — used only when
// direct P2P fails (strict NAT / STUN limits). Media stays E2E-encrypted over
// DTLS; the relay forwards only ciphertext. Multiple transports are offered so
// every browser finds one that works:
// - turn:3478?transport=udp — plain UDP, the most universally compatible
// transport (notably the one Safari reliably gathers relay candidates on).
// - turn:3478?transport=tcp — TCP fallback when UDP is blocked.
// - turns:443?transport=tcp — TURN over TLS on 443, bypasses restrictive
// firewalls that only allow HTTPS.
//
// The credential below is a long-lived token derived from the server's TURN
// REST API secret (static-auth-secret) — username is "<expiry-unix-ts>:label".
// Expiry 2147483647 = 2038-01-19 (max value coturn accepts; it parses the
// timestamp as int32). The master secret itself never leaves the server; only
// this derived username/credential pair is public (by design, like any
// browser-side TURN credential). To revoke/renew, rotate the server's
// static-auth-secret and re-issue with generate_turn_credentials.py.
urls: [
'turn:turn.securebit.chat:3478?transport=udp',
'turn:turn.securebit.chat:3478?transport=tcp',
// Raw-IP TURN relay — the DNS-bypass fallback that lets Safari (and any client
// whose WebRTC layer can't resolve the hostname) obtain a relay candidate.
// Plain turn: (no TLS) so no cert/hostname check is needed; the REST-API
// credential is host-independent, and relayed traffic is already E2E ciphertext.
'turn:144.172.96.126:3478?transport=udp',
'turn:144.172.96.126:3478?transport=tcp',
'turns:turn.securebit.chat:443?transport=tcp'
],
username: '2147483647:securebit',
credential: 'tNrw3h2p/+OE0uLMLVc+ech7T6o='
}
]; ];
+4
View File
@@ -45,6 +45,9 @@ http {
~^/sw\.js$ "no-cache, no-store, must-revalidate"; ~^/sw\.js$ "no-cache, no-store, must-revalidate";
~^/manifest\.json$ "no-cache, no-store, must-revalidate"; ~^/manifest\.json$ "no-cache, no-store, must-revalidate";
~^/meta\.json$ "no-cache, no-store, must-revalidate"; ~^/meta\.json$ "no-cache, no-store, must-revalidate";
# Operator ICE/TURN config: must never be cached long, or the browser
# locks onto a stale server list (default was max-age=1y immutable).
~^/config/ice-servers\.js$ "no-cache, no-store, must-revalidate";
# dist/ bundles are query-versioned (?v=) in index.html. "no-cache" forces # dist/ bundles are query-versioned (?v=) in index.html. "no-cache" forces
# revalidation on every load, but dropping "no-store" lets the browser reuse # revalidation on every load, but dropping "no-store" lets the browser reuse
# the cached copy on a 304 — avoiding a full re-download of the large bundles # the cached copy on a 304 — avoiding a full re-download of the large bundles
@@ -66,6 +69,7 @@ http {
~^/sw\.js$ "no-store"; ~^/sw\.js$ "no-store";
~^/manifest\.json$ "no-store"; ~^/manifest\.json$ "no-store";
~^/meta\.json$ "no-store"; ~^/meta\.json$ "no-store";
~^/config/ice-servers\.js$ "no-store";
~^/dist/ "no-store"; ~^/dist/ "no-store";
} }
+1 -1
View File
@@ -17721,7 +17721,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [ React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [ React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [
React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"), React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"),
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.4.5") React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.4.10")
]), ]),
React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted") React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted")
]) ])
+2 -2
View File
File diff suppressed because one or more lines are too long
Vendored
+1 -1
View File
@@ -3523,7 +3523,7 @@ var EnhancedSecureP2PChat = () => {
} catch (error) { } catch (error) {
} }
} }
handleMessage(" SecureBit.chat Enhanced Security Edition v5.4.5 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system"); handleMessage(" SecureBit.chat Enhanced Security Edition v5.4.10 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
manager.setFileTransferCallbacks( manager.setFileTransferCallbacks(
// Progress callback — drives the voice-note upload/download ring. // Progress callback — drives the voice-note upload/download ring.
(progress) => { (progress) => {
+2 -2
View File
File diff suppressed because one or more lines are too long
+22 -22
View File
@@ -23,7 +23,7 @@
<!-- PWA Manifest --> <!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json"> <link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1784694927450"> <link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1784757947117">
<!-- PWA Meta Tags --> <!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
@@ -89,7 +89,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"> <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 --> <!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1784694927450"> <link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1784757947117">
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png"> <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="60x60" href="./logo/icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
@@ -98,7 +98,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png"> <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="144x144" href="./logo/icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.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=1784694927450"> <link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1784757947117">
<!-- Microsoft Tiles --> <!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35"> <meta name="msapplication-TileColor" content="#ff6b35">
@@ -182,7 +182,7 @@
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts <!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
both execute in document order after parsing, so React still runs before the 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. --> app modules below, but the parser / first paint is no longer blocked. -->
<script defer src="config/ice-servers.js"></script> <script defer src="config/ice-servers.js?v=1784757947117"></script>
<script defer src="libs/react/react.production.min.js"></script> <script defer src="libs/react/react.production.min.js"></script>
<script defer src="libs/react-dom/react-dom.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 — <!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
@@ -190,8 +190,8 @@
Its CSS is loaded async via load-async-css.js (not paint-critical). --> Its CSS is loaded async via load-async-css.js (not paint-critical). -->
<script defer src="libs/prism/prism.js"></script> <script defer src="libs/prism/prism.js"></script>
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). --> <!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
<link rel="stylesheet" href="assets/tailwind.css?v=1784694927450"> <link rel="stylesheet" href="assets/tailwind.css?v=1784757947117">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1784694927450"> <link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1784757947117">
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI <!-- 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 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. --> first screen). Inter latin 400/700 cover body text and headings/buttons. -->
@@ -199,31 +199,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-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="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="/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="src/styles/main.css?v=1784694927450"> <link rel="stylesheet" href="src/styles/main.css?v=1784757947117">
<link rel="stylesheet" href="src/styles/animations.css?v=1784694927450"> <link rel="stylesheet" href="src/styles/animations.css?v=1784757947117">
<link rel="stylesheet" href="src/styles/components.css?v=1784694927450"> <link rel="stylesheet" href="src/styles/components.css?v=1784757947117">
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. --> <!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="src/scripts/load-async-css.js?v=1784694927450"></script> <script defer src="src/scripts/load-async-css.js?v=1784757947117"></script>
<noscript> <noscript>
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="libs/prism/prism.css"> <link rel="stylesheet" href="libs/prism/prism.css">
</noscript> </noscript>
<script defer src="src/scripts/fa-check.js?v=1784694927450"></script> <script defer src="src/scripts/fa-check.js?v=1784757947117"></script>
<!-- Update Manager - система принудительного обновления --> <!-- Update Manager - система принудительного обновления -->
<script defer src="src/utils/updateManager.js?v=1784694927450"></script> <script defer src="src/utils/updateManager.js?v=1784757947117"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1784694927450"></script> <script type="module" src="src/components/UpdateChecker.jsx?v=1784757947117"></script>
<script type="module" src="dist/qr-local.js?v=1784694927450"></script> <script type="module" src="dist/qr-local.js?v=1784757947117"></script>
<script type="module" src="src/components/QRScanner.js?v=1784694927450"></script> <script type="module" src="src/components/QRScanner.js?v=1784757947117"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1784694927450"></script> <script type="module" src="dist/app-boot.js?v=1784757947117"></script>
<script type="module" src="dist/app.js?v=1784694927450"></script> <script type="module" src="dist/app.js?v=1784757947117"></script>
<script defer src="src/scripts/pwa-register.js?v=1784694927450"></script> <script defer src="src/scripts/pwa-register.js?v=1784757947117"></script>
<script src="./src/pwa/install-prompt.js?v=1784694927450" type="module"></script> <script src="./src/pwa/install-prompt.js?v=1784757947117" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1784694927450" type="module"></script> <script src="./src/pwa/pwa-manager.js?v=1784757947117" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1784694927450"></script> <script defer src="./src/scripts/pwa-offline-test.js?v=1784757947117"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1784694927450"> <link rel="stylesheet" href="./src/styles/pwa.css?v=1784757947117">
</body> </body>
</html> </html>
+7 -7
View File
@@ -1,10 +1,10 @@
{ {
"version": "1784694927450", "version": "1784757947117",
"buildVersion": "1784694927450", "buildVersion": "1784757947117",
"appVersion": "5.4.5", "appVersion": "5.4.10",
"buildTime": "2026-07-22T04:35:27.501Z", "buildTime": "2026-07-22T22:05:47.156Z",
"buildId": "1784694927450-0e3e3a2", "buildId": "1784757947117-c98a01b",
"gitHash": "0e3e3a2", "gitHash": "c98a01b",
"generated": true, "generated": true,
"generatedAt": "2026-07-22T04:35:27.502Z" "generatedAt": "2026-07-22T22:05:47.157Z"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "securebit-chat", "name": "securebit-chat",
"version": "5.4.5", "version": "5.4.10",
"description": "Secure P2P Communication Application with End-to-End Encryption", "description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html", "main": "index.html",
"scripts": { "scripts": {
+1 -1
View File
@@ -3447,7 +3447,7 @@ import {
} }
} }
handleMessage(' SecureBit.chat Enhanced Security Edition v5.4.5 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system'); handleMessage(' SecureBit.chat Enhanced Security Edition v5.4.10 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
// Setup file transfer callbacks (id-bound to THIS session's manager). // Setup file transfer callbacks (id-bound to THIS session's manager).
manager.setFileTransferCallbacks( manager.setFileTransferCallbacks(
+1 -1
View File
@@ -559,7 +559,7 @@ const EnhancedMinimalHeader = ({
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [ React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [ React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'), React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.4.5') React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.4.10')
]), ]),
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted') React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
]) ])
+1 -1
View File
@@ -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 // 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, // 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. // reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
const SW_BUILD_VERSION = '1784694927450'; const SW_BUILD_VERSION = '1784757947117';
// Load version from meta.json on install // Load version from meta.json on install
async function getAppVersion() { async function getAppVersion() {