feat: user-configurable STUN/TURN servers (advanced network settings)
- add iceServers.js: allowlist-based validation/normalization of user-supplied STUN/TURN URLs (rejects javascript:/data:/http/ws, control chars, enforces limits) - add iceSettingsStore.js: opt-in persistence encrypted at rest with a non-extractable AES-GCM device key in IndexedDB; load/save/clear - add IceServerSettings.jsx modal: public vs custom servers, JSON/line input, live validation, relay-only toggle, 'Test servers' connectivity check, save-on-device prompt, forget-saved action - wire chosen servers/privacy mode into EnhancedSecureWebRTCManager construction (priority: custom > operator override > built-in defaults) - entry point on the connection-creation screen next to the relay-only toggle - add ice-servers-validation.test.mjs to the suite
This commit is contained in:
1 parent
366f080128
commit
7f2ecce57f
15 files changed
+1307
-23
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@mediLine truncated
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@mediLine truncated
|
||||
Vendored
+381
@@ -19071,6 +19071,387 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles
|
||||
};
|
||||
window.FileTransferComponent = FileTransferComponent;
|
||||
|
||||
// src/network/iceServers.js
|
||||
var ICE_LIMITS = Object.freeze({
|
||||
MAX_SERVERS: 10,
|
||||
MAX_URLS_PER_SERVER: 8,
|
||||
MAX_STRING_LENGTH: 512
|
||||
});
|
||||
var ALLOWED_ICE_SCHEMES = Object.freeze(["stun", "stuns", "turn", "turns"]);
|
||||
var SCHEME_RE = /^(stuns?|turns?):/i;
|
||||
var HOST_RE = /^(\[[0-9a-f:]+\]|[a-z0-9.-]+)(:\d{1,5})?$/i;
|
||||
var TRANSPORT_RE = /^transport=(udp|tcp)$/i;
|
||||
function hasControlChars(value) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
if (code < 32 || code === 127) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function validateIceUrl(url) {
|
||||
if (typeof url !== "string") return "URL must be a string";
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return "URL is empty";
|
||||
if (trimmed.length > ICE_LIMITS.MAX_STRING_LENGTH) return "URL is too long";
|
||||
if (hasControlChars(trimmed)) return "URL contains invalid characters";
|
||||
const scheme = trimmed.match(SCHEME_RE);
|
||||
if (!scheme) {
|
||||
return "URL must start with stun:, stuns:, turn: or turns:";
|
||||
}
|
||||
const rest = trimmed.slice(scheme[0].length);
|
||||
const [hostPort, query, ...extra] = rest.split("?");
|
||||
if (extra.length > 0) return "URL has an invalid query";
|
||||
if (!hostPort) return "URL is missing a host";
|
||||
if (!HOST_RE.test(hostPort)) return "URL has an invalid host or port";
|
||||
if (query !== void 0 && !TRANSPORT_RE.test(query)) {
|
||||
return "URL query must be transport=udp or transport=tcp";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isTurnUrl(url) {
|
||||
return typeof url === "string" && /^turns?:/i.test(url.trim());
|
||||
}
|
||||
function validateSecret(value, label) {
|
||||
if (value === void 0 || value === null || value === "") return null;
|
||||
if (typeof value !== "string") return `${label} must be a string`;
|
||||
if (value.length > ICE_LIMITS.MAX_STRING_LENGTH) return `${label} is too long`;
|
||||
if (hasControlChars(value)) return `${label} contains invalid characters`;
|
||||
return null;
|
||||
}
|
||||
function normalizeIceServers(entries2) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const servers = [];
|
||||
if (!Array.isArray(entries2)) {
|
||||
return { servers: [], errors: ["Server list must be an array"], warnings: [] };
|
||||
}
|
||||
if (entries2.length === 0) {
|
||||
return { servers: [], errors: [], warnings: [] };
|
||||
}
|
||||
if (entries2.length > ICE_LIMITS.MAX_SERVERS) {
|
||||
errors.push(`Too many servers (max ${ICE_LIMITS.MAX_SERVERS})`);
|
||||
return { servers: [], errors, warnings };
|
||||
}
|
||||
entries2.forEach((entry, index) => {
|
||||
const label = `Server #${index + 1}`;
|
||||
if (!entry || typeof entry !== "object") {
|
||||
errors.push(`${label}: invalid entry`);
|
||||
return;
|
||||
}
|
||||
const rawUrls = Array.isArray(entry.urls) ? entry.urls : [entry.urls];
|
||||
if (rawUrls.length === 0 || rawUrls.length > ICE_LIMITS.MAX_URLS_PER_SERVER) {
|
||||
errors.push(`${label}: between 1 and ${ICE_LIMITS.MAX_URLS_PER_SERVER} URLs required`);
|
||||
return;
|
||||
}
|
||||
const cleanUrls = [];
|
||||
let entryHasTurn = false;
|
||||
for (const rawUrl of rawUrls) {
|
||||
const err = validateIceUrl(rawUrl);
|
||||
if (err) {
|
||||
errors.push(`${label}: ${err}`);
|
||||
continue;
|
||||
}
|
||||
const trimmed = rawUrl.trim();
|
||||
cleanUrls.push(trimmed);
|
||||
if (isTurnUrl(trimmed)) entryHasTurn = true;
|
||||
}
|
||||
if (cleanUrls.length === 0) return;
|
||||
const userErr = validateSecret(entry.username, `${label} username`);
|
||||
if (userErr) errors.push(userErr);
|
||||
const credErr = validateSecret(entry.credential, `${label} credential`);
|
||||
if (credErr) errors.push(credErr);
|
||||
const server = { urls: cleanUrls.length === 1 ? cleanUrls[0] : cleanUrls };
|
||||
if (entry.username) server.username = String(entry.username);
|
||||
if (entry.credential) server.credential = String(entry.credential);
|
||||
if (entryHasTurn && (!server.username || !server.credential)) {
|
||||
warnings.push(`${label}: TURN servers usually require a username and credential`);
|
||||
}
|
||||
servers.push(server);
|
||||
});
|
||||
return { servers, errors, warnings };
|
||||
}
|
||||
function parseIceServersInput(text2) {
|
||||
if (typeof text2 !== "string" || !text2.trim()) {
|
||||
return { servers: [], errors: [], warnings: [] };
|
||||
}
|
||||
const trimmed = text2.trim();
|
||||
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch {
|
||||
return { servers: [], errors: ["Invalid JSON"], warnings: [] };
|
||||
}
|
||||
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
||||
return normalizeIceServers(arr);
|
||||
}
|
||||
const entries2 = trimmed.split("\n").map((line) => line.trim()).filter(Boolean).map((url) => ({ urls: url }));
|
||||
return normalizeIceServers(entries2);
|
||||
}
|
||||
function listHasTurn(servers) {
|
||||
if (!Array.isArray(servers)) return false;
|
||||
return servers.some((server) => {
|
||||
const urls = Array.isArray(server?.urls) ? server.urls : [server?.urls];
|
||||
return urls.some(isTurnUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// src/components/ui/IceServerSettings.jsx
|
||||
var React2 = window.React;
|
||||
var PLACEHOLDER = [
|
||||
"# One URL per line, e.g.:",
|
||||
"stun:stun.example.com:3478",
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"",
|
||||
"# Or paste JSON for servers with credentials:",
|
||||
'[{"urls":"turns:turn.example.com:5349","username":"user","credential":"secret"}]'
|
||||
].join("\n");
|
||||
async function testIceServers(servers, timeoutMs = 6e3) {
|
||||
const found = { host: 0, srflx: 0, relay: 0 };
|
||||
if (typeof RTCPeerConnection === "undefined") {
|
||||
return { ...found, error: "WebRTC is not available in this browser" };
|
||||
}
|
||||
let pc;
|
||||
try {
|
||||
pc = new RTCPeerConnection({ iceServers: servers });
|
||||
} catch (error) {
|
||||
return { ...found, error: error.message || "Invalid server configuration" };
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
pc.close();
|
||||
} catch {
|
||||
}
|
||||
resolve(found);
|
||||
};
|
||||
const timer = setTimeout(finish, timeoutMs);
|
||||
pc.onicecandidate = (event) => {
|
||||
if (!event.candidate) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const c = event.candidate.candidate || "";
|
||||
if (/ typ host/.test(c)) found.host++;
|
||||
else if (/ typ srflx/.test(c)) found.srflx++;
|
||||
else if (/ typ relay/.test(c)) found.relay++;
|
||||
};
|
||||
try {
|
||||
pc.createDataChannel("securebit-ice-test");
|
||||
pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => finish());
|
||||
} catch {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
var IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget }) => {
|
||||
if (!isOpen) return null;
|
||||
const [useCustom, setUseCustom] = React2.useState(initial?.useCustom || false);
|
||||
const [serversText, setServersText] = React2.useState(initial?.serversText || "");
|
||||
const [relayOnly, setRelayOnly] = React2.useState(initial?.privacyMode === "relay-only");
|
||||
const [persist, setPersist] = React2.useState(initial?.persisted || false);
|
||||
const [testState, setTestState] = React2.useState("idle");
|
||||
const [testResult, setTestResult] = React2.useState(null);
|
||||
const parsed = useCustom ? parseIceServersInput(serversText) : { servers: [], errors: [], warnings: [] };
|
||||
const hasTurn = listHasTurn(parsed.servers);
|
||||
const canApply = !useCustom || parsed.servers.length > 0 && parsed.errors.length === 0;
|
||||
const handleTest = async () => {
|
||||
setTestState("running");
|
||||
setTestResult(null);
|
||||
const result = await testIceServers(parsed.servers);
|
||||
setTestResult(result);
|
||||
setTestState("done");
|
||||
};
|
||||
const handleApply = () => {
|
||||
if (!canApply) return;
|
||||
onApply(
|
||||
{
|
||||
useCustom,
|
||||
servers: useCustom ? parsed.servers : [],
|
||||
privacyMode: relayOnly ? "relay-only" : "standard",
|
||||
serversText
|
||||
},
|
||||
persist
|
||||
);
|
||||
};
|
||||
const handleForget = async () => {
|
||||
if (onForget) await onForget();
|
||||
setPersist(false);
|
||||
};
|
||||
const labelCls = "block text-sm font-medium text-primary";
|
||||
const descCls = "block text-sm text-secondary";
|
||||
const children = [];
|
||||
children.push(React2.createElement("div", { key: "header", className: "flex items-center mb-4" }, [
|
||||
React2.createElement("div", {
|
||||
key: "icon",
|
||||
className: "w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3"
|
||||
}, [React2.createElement("i", { className: "fas fa-network-wired accent-purple" })]),
|
||||
React2.createElement("h3", { key: "title", className: "text-lg font-medium text-primary" }, "Advanced network settings")
|
||||
]));
|
||||
children.push(React2.createElement(
|
||||
"p",
|
||||
{ key: "intro", className: "text-sm text-secondary mb-4" },
|
||||
"By default SecureBit uses public STUN servers. You can supply your own STUN/TURN servers \u2014 useful if you self-host a TURN relay and do not want to rely on public infrastructure. Servers are configured locally on your side only; you do not need to share them with your peer."
|
||||
));
|
||||
children.push(React2.createElement("div", { key: "mode", className: "space-y-2 mb-4" }, [
|
||||
React2.createElement("label", { key: "public", className: "flex items-start gap-3" }, [
|
||||
React2.createElement("input", {
|
||||
key: "r",
|
||||
type: "radio",
|
||||
name: "ice-mode",
|
||||
checked: !useCustom,
|
||||
onChange: () => setUseCustom(false),
|
||||
className: "mt-1"
|
||||
}),
|
||||
React2.createElement("span", { key: "s" }, [
|
||||
React2.createElement("span", { key: "t", className: labelCls }, "Public servers (default)"),
|
||||
React2.createElement("span", { key: "d", className: descCls }, "Zero-config. Good for most users.")
|
||||
])
|
||||
]),
|
||||
React2.createElement("label", { key: "custom", className: "flex items-start gap-3" }, [
|
||||
React2.createElement("input", {
|
||||
key: "r",
|
||||
type: "radio",
|
||||
name: "ice-mode",
|
||||
checked: useCustom,
|
||||
onChange: () => setUseCustom(true),
|
||||
className: "mt-1"
|
||||
}),
|
||||
React2.createElement("span", { key: "s" }, [
|
||||
React2.createElement("span", { key: "t", className: labelCls }, "My own STUN/TURN servers"),
|
||||
React2.createElement("span", { key: "d", className: descCls }, `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`)
|
||||
])
|
||||
])
|
||||
]));
|
||||
if (useCustom) {
|
||||
children.push(React2.createElement("textarea", {
|
||||
key: "textarea",
|
||||
value: serversText,
|
||||
onChange: (e) => setServersText(e.target.value),
|
||||
placeholder: PLACEHOLDER,
|
||||
spellCheck: false,
|
||||
autoComplete: "off",
|
||||
className: "w-full h-36 mb-2 p-3 rounded-lg bg-black/30 border border-purple-500/20 text-sm text-primary font-mono"
|
||||
}));
|
||||
if (parsed.errors.length > 0) {
|
||||
children.push(React2.createElement(
|
||||
"ul",
|
||||
{ key: "errors", className: "mb-2 text-sm text-red-400 list-disc pl-5" },
|
||||
parsed.errors.slice(0, 6).map((err, i) => React2.createElement("li", { key: i }, err))
|
||||
));
|
||||
}
|
||||
if (parsed.warnings.length > 0) {
|
||||
children.push(React2.createElement(
|
||||
"ul",
|
||||
{ key: "warnings", className: "mb-2 text-sm text-yellow-400 list-disc pl-5" },
|
||||
parsed.warnings.slice(0, 6).map((w, i) => React2.createElement("li", { key: i }, w))
|
||||
));
|
||||
}
|
||||
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
|
||||
children.push(React2.createElement(
|
||||
"p",
|
||||
{ key: "ok", className: "mb-2 text-sm text-green-400" },
|
||||
`${parsed.servers.length} server(s) parsed${hasTurn ? " (TURN present)" : " (STUN only \u2014 does not hide IP)"}.`
|
||||
));
|
||||
}
|
||||
children.push(React2.createElement(
|
||||
"p",
|
||||
{ key: "disclaimer", className: "mb-3 text-xs text-secondary" },
|
||||
"Privacy note: a TURN relay sees the IP addresses and traffic timing of both peers (never your message contents, which stay end-to-end encrypted). Only a TURN server you trust or self-host improves privacy \u2014 pointing this at a random public relay does not. Prefer turns: (TLS)."
|
||||
));
|
||||
children.push(React2.createElement("div", { key: "test", className: "mb-3" }, [
|
||||
React2.createElement("button", {
|
||||
key: "btn",
|
||||
type: "button",
|
||||
disabled: !canApply || testState === "running",
|
||||
onClick: handleTest,
|
||||
className: "px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary disabled:opacity-50"
|
||||
}, testState === "running" ? "Testing\u2026" : "Test servers"),
|
||||
testState === "done" && testResult ? React2.createElement(
|
||||
"span",
|
||||
{
|
||||
key: "res",
|
||||
className: "ml-3 text-sm " + (testResult.error ? "text-red-400" : "text-secondary")
|
||||
},
|
||||
testResult.error ? `Test failed: ${testResult.error}` : `STUN ${testResult.srflx > 0 ? "OK" : "none"} \xB7 TURN ${testResult.relay > 0 ? "OK" : "none"} \xB7 host ${testResult.host}`
|
||||
) : null
|
||||
]));
|
||||
}
|
||||
children.push(React2.createElement("label", { key: "relay", className: "flex items-start gap-3 mb-3 rounded-lg border border-purple-500/20 bg-purple-500/10 p-3" }, [
|
||||
React2.createElement("input", {
|
||||
key: "i",
|
||||
type: "checkbox",
|
||||
checked: relayOnly,
|
||||
onChange: (e) => setRelayOnly(e.target.checked),
|
||||
className: "mt-1"
|
||||
}),
|
||||
React2.createElement("span", { key: "s" }, [
|
||||
React2.createElement("span", { key: "t", className: labelCls }, "Relay-only mode (maximum privacy)"),
|
||||
React2.createElement("span", { key: "d", className: descCls }, "Routes all traffic through TURN so your IP is not exposed to the peer. Requires a TURN server; connections cannot start without one.")
|
||||
])
|
||||
]));
|
||||
if (relayOnly && useCustom && !hasTurn) {
|
||||
children.push(React2.createElement(
|
||||
"p",
|
||||
{ key: "relaywarn", className: "mb-3 text-sm text-yellow-400" },
|
||||
"Relay-only is enabled but no TURN server is configured. The connection will not be able to start."
|
||||
));
|
||||
}
|
||||
children.push(React2.createElement("label", { key: "persist", className: "flex items-start gap-3 mb-4" }, [
|
||||
React2.createElement("input", {
|
||||
key: "i",
|
||||
type: "checkbox",
|
||||
checked: persist,
|
||||
onChange: (e) => setPersist(e.target.checked),
|
||||
className: "mt-1"
|
||||
}),
|
||||
React2.createElement("span", { key: "s" }, [
|
||||
React2.createElement("span", { key: "t", className: labelCls }, "Save on this device"),
|
||||
React2.createElement("span", { key: "d", className: descCls }, "Stored encrypted in this browser. Leave off to use only for this session.")
|
||||
])
|
||||
]));
|
||||
const actions = [
|
||||
React2.createElement("button", {
|
||||
key: "cancel",
|
||||
type: "button",
|
||||
onClick: onClose,
|
||||
className: "px-4 py-2 text-sm rounded-lg border border-white/10 text-secondary"
|
||||
}, "Cancel"),
|
||||
React2.createElement("button", {
|
||||
key: "apply",
|
||||
type: "button",
|
||||
onClick: handleApply,
|
||||
disabled: !canApply,
|
||||
className: "px-4 py-2 text-sm rounded-lg bg-purple-500/20 border border-purple-500/30 text-primary disabled:opacity-50"
|
||||
}, "Apply")
|
||||
];
|
||||
if (hasSaved) {
|
||||
actions.unshift(React2.createElement("button", {
|
||||
key: "forget",
|
||||
type: "button",
|
||||
onClick: handleForget,
|
||||
className: "px-4 py-2 text-sm rounded-lg border border-red-500/30 text-red-400 mr-auto"
|
||||
}, "Forget saved"));
|
||||
}
|
||||
children.push(React2.createElement("div", { key: "actions", className: "flex items-center gap-2 flex-wrap" }, actions));
|
||||
return React2.createElement("div", {
|
||||
className: "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4",
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}
|
||||
}, [
|
||||
React2.createElement("div", {
|
||||
key: "modal",
|
||||
className: "card-minimal rounded-xl p-6 max-w-lg w-full border-purple-500/20 max-h-[90vh] overflow-y-auto"
|
||||
}, children)
|
||||
]);
|
||||
};
|
||||
window.IceServerSettings = IceServerSettings;
|
||||
|
||||
// src/scripts/app-boot.js
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
|
||||
Vendored
+4
-4
@@ -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/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.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
|
||||
"mappings": "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 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
|
||||
"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/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.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
|
||||
"mappings": "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 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
|
||||
}
|
||||
Vendored
+199
-1
@@ -31,6 +31,126 @@ function installDebugWindowHooks({
|
||||
};
|
||||
}
|
||||
|
||||
// src/network/iceSettingsStore.js
|
||||
var DB_NAME = "securebit-net";
|
||||
var DB_VERSION = 1;
|
||||
var STORE = "kv";
|
||||
var KEY_RECORD = "ice-device-key";
|
||||
var SETTINGS_RECORD = "ice-settings";
|
||||
var SETTINGS_VERSION = 1;
|
||||
function isSupported() {
|
||||
return typeof indexedDB !== "undefined" && typeof crypto !== "undefined" && !!crypto.subtle;
|
||||
}
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request;
|
||||
try {
|
||||
request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE)) {
|
||||
db.createObjectStore(STORE);
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
function idbGet(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readonly");
|
||||
const req = tx.objectStore(STORE).get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
function idbPut(db, key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
tx.objectStore(STORE).put(value, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
function idbDelete(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
tx.objectStore(STORE).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
async function getOrCreateDeviceKey(db) {
|
||||
const existing = await idbGet(db, KEY_RECORD);
|
||||
if (existing instanceof CryptoKey) {
|
||||
return existing;
|
||||
}
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
// non-extractable
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
await idbPut(db, KEY_RECORD, key);
|
||||
return key;
|
||||
}
|
||||
async function saveIceSettings(settings) {
|
||||
if (!isSupported()) throw new Error("Persistent storage is not available in this browser");
|
||||
const db = await openDb();
|
||||
const key = await getOrCreateDeviceKey(db);
|
||||
const payload = JSON.stringify({
|
||||
version: SETTINGS_VERSION,
|
||||
servers: Array.isArray(settings?.servers) ? settings.servers : [],
|
||||
privacyMode: settings?.privacyMode === "relay-only" ? "relay-only" : "standard"
|
||||
});
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
new TextEncoder().encode(payload)
|
||||
);
|
||||
await idbPut(db, SETTINGS_RECORD, {
|
||||
iv: Array.from(iv),
|
||||
data: Array.from(new Uint8Array(ciphertext))
|
||||
});
|
||||
}
|
||||
async function loadIceSettings() {
|
||||
if (!isSupported()) return null;
|
||||
try {
|
||||
const db = await openDb();
|
||||
const record = await idbGet(db, SETTINGS_RECORD);
|
||||
if (!record || !Array.isArray(record.iv) || !Array.isArray(record.data)) {
|
||||
return null;
|
||||
}
|
||||
const key = await idbGet(db, KEY_RECORD);
|
||||
if (!(key instanceof CryptoKey)) return null;
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: new Uint8Array(record.iv) },
|
||||
key,
|
||||
new Uint8Array(record.data)
|
||||
);
|
||||
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
||||
return {
|
||||
servers: Array.isArray(parsed.servers) ? parsed.servers : [],
|
||||
privacyMode: parsed.privacyMode === "relay-only" ? "relay-only" : "standard"
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function clearIceSettings() {
|
||||
if (!isSupported()) return;
|
||||
try {
|
||||
const db = await openDb();
|
||||
await idbDelete(db, SETTINGS_RECORD);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
// src/app.jsx
|
||||
var EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -506,6 +626,38 @@ var EnhancedConnectionSetup = ({
|
||||
}, "Uses TURN relay-only when configured. Without TURN, direct WebRTC may expose IP addresses and relay-only connections cannot start.")
|
||||
])
|
||||
]),
|
||||
React.createElement("div", {
|
||||
key: "advanced-network",
|
||||
className: "mb-6 mx-auto max-w-2xl flex flex-wrap items-center justify-between gap-3"
|
||||
}, [
|
||||
React.createElement("span", {
|
||||
key: "status",
|
||||
className: "text-sm text-secondary"
|
||||
}, Array.isArray(customIceServers) && customIceServers.length ? `Using ${customIceServers.length} custom ICE server(s)` : "Using public ICE servers"),
|
||||
React.createElement("button", {
|
||||
key: "btn",
|
||||
type: "button",
|
||||
onClick: () => setShowIceSettings(true),
|
||||
className: "px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary"
|
||||
}, [
|
||||
React.createElement("i", { key: "i", className: "fas fa-network-wired mr-2" }),
|
||||
"Advanced network settings"
|
||||
])
|
||||
]),
|
||||
typeof window !== "undefined" && window.IceServerSettings ? React.createElement(window.IceServerSettings, {
|
||||
key: "ice-settings-modal",
|
||||
isOpen: showIceSettings,
|
||||
onClose: () => setShowIceSettings(false),
|
||||
initial: {
|
||||
useCustom: Array.isArray(customIceServers) && customIceServers.length > 0,
|
||||
serversText: iceServersText,
|
||||
privacyMode: relayOnlyMode ? "relay-only" : "standard",
|
||||
persisted: iceSettingsPersisted
|
||||
},
|
||||
hasSaved: iceSettingsPersisted,
|
||||
onApply: handleApplyIceSettings,
|
||||
onForget: handleForgetIceSettings
|
||||
}) : null,
|
||||
React.createElement("div", {
|
||||
key: "options",
|
||||
className: "flex flex-col md:flex-row items-center justify-center gap-6 max-w-3xl mx-auto"
|
||||
@@ -1481,6 +1633,51 @@ var EnhancedSecureP2PChat = () => {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [customIceServers2, setCustomIceServers] = React.useState(null);
|
||||
const [iceServersText2, setIceServersText] = React.useState("");
|
||||
const [iceSettingsPersisted2, setIceSettingsPersisted] = React.useState(false);
|
||||
const [showIceSettings2, setShowIceSettings2] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadIceSettings().then((saved) => {
|
||||
if (cancelled || !saved) return;
|
||||
if (Array.isArray(saved.servers) && saved.servers.length > 0) {
|
||||
setCustomIceServers(saved.servers);
|
||||
setIceServersText(JSON.stringify(saved.servers, null, 2));
|
||||
}
|
||||
if (saved.privacyMode === "relay-only") {
|
||||
setRelayOnlyMode(true);
|
||||
}
|
||||
setIceSettingsPersisted(true);
|
||||
}).catch(() => {
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
const handleApplyIceSettings2 = React.useCallback((next, persist) => {
|
||||
const servers = next.useCustom && Array.isArray(next.servers) ? next.servers : null;
|
||||
setCustomIceServers(servers && servers.length ? servers : null);
|
||||
setIceServersText(next.serversText || "");
|
||||
setRelayOnlyMode(next.privacyMode === "relay-only");
|
||||
setShowIceSettings2(false);
|
||||
if (persist) {
|
||||
setIceSettingsPersisted(true);
|
||||
saveIceSettings({ servers: servers || [], privacyMode: next.privacyMode }).catch(() => {
|
||||
});
|
||||
} else if (iceSettingsPersisted2) {
|
||||
setIceSettingsPersisted(false);
|
||||
clearIceSettings().catch(() => {
|
||||
});
|
||||
}
|
||||
}, [iceSettingsPersisted2]);
|
||||
const handleForgetIceSettings2 = React.useCallback(async () => {
|
||||
await clearIceSettings().catch(() => {
|
||||
});
|
||||
setIceSettingsPersisted(false);
|
||||
setCustomIceServers(null);
|
||||
setIceServersText("");
|
||||
}, []);
|
||||
const [messageInput, setMessageInput] = React.useState("");
|
||||
const [offerData, setOfferData] = React.useState("");
|
||||
const [answerData, setAnswerData] = React.useState("");
|
||||
@@ -1806,7 +2003,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
{
|
||||
webrtc: {
|
||||
relayOnly: relayOnlyMode,
|
||||
iceServers: Array.isArray(window.SECUREBIT_ICE_SERVERS) ? window.SECUREBIT_ICE_SERVERS : void 0
|
||||
// Priority: user's custom servers > operator override > built-in defaults.
|
||||
iceServers: Array.isArray(customIceServers2) && customIceServers2.length ? customIceServers2 : Array.isArray(window.SECUREBIT_ICE_SERVERS) ? window.SECUREBIT_ICE_SERVERS : void 0
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/utils/debugWindowHooks.js", "../src/app.jsx"],
|
||||
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "import { installDebugWindowHooks } from './utils/debugWindowHooks.js';\n // Enhanced Copy Button with better UX\n const EnhancedCopyButton = ({ text, className = \"\", children }) => {\n const [copied, setCopied] = React.useState(false);\n \n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (error) {\n console.error('Copy failed:', error);\n // Fallback for older browsers\n const textArea = document.createElement('textarea');\n textArea.value = text;\n document.body.appendChild(textArea);\n textArea.select();\n document.execCommand('copy');\n document.body.removeChild(textArea);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }\n };\n \n return React.createElement('button', {\n onClick: handleCopy,\n className: `${className} transition-all duration-200`\n }, [\n React.createElement('i', {\n key: 'icon',\n className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`\n }),\n copied ? 'Copied!' : children\n ]);\n };\n \n // Verification Component\n const VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {\n const [sasInput, setSasInput] = React.useState('');\n const [error, setError] = React.useState('');\n const normalizedExpectedLength = (verificationCode || '').replace(/[-\\s]/g, '').length;\n const normalizedInputLength = sasInput.replace(/[-\\s]/g, '').length;\n const canConfirm = !localConfirmed && normalizedExpectedLength > 0 && normalizedInputLength === normalizedExpectedLength;\n\n React.useEffect(() => {\n setSasInput('');\n setError('');\n }, [verificationCode]);\n\n const handleConfirm = async () => {\n try {\n setError('');\n await onConfirm(sasInput);\n } catch (confirmationError) {\n setSasInput('');\n if (confirmationError?.message === 'SAS_MAX_ATTEMPTS') {\n setError('Too many incorrect attempts. Session reset for safety.');\n } else {\n setError('Incorrect code. Check it with your peer and try again.');\n }\n }\n };\n\n return React.createElement('div', {\n className: \"card-minimal rounded-xl p-6 border-purple-500/20\"\n }, [\n React.createElement('div', {\n key: 'header',\n className: \"flex items-center mb-4\"\n }, [\n React.createElement('div', {\n key: 'icon',\n className: \"w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-centeLine truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["offerData", "payload", "UpdateCheckerWrapper"]
|
||||
"sources": ["../src/utils/debugWindowHooks.js", "../src/network/iceSettingsStore.js", "../src/app.jsx"],
|
||||
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)\n// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when\n// the user explicitly opts in (\"Save on this device\"). Session-only use never\n// touches this store \u2014 the settings live in React state and vanish on reload.\n//\n// At-rest protection model:\n// - A non-extractable AES-GCM device key is generated once and kept in\n// IndexedDB. It can never be exported back into JS, so a copy of the\n// on-disk database is useless without executing code in this exact origin.\n// - The settings blob (which may contain TURN credentials) is encrypted with\n// that key before being written.\n// This protects against disk/profile inspection. It does NOT protect against a\n// live code-execution compromise of the page (consistent with the project's\n// stated threat model \u2014 see SECURITY.md). Credentials are never persisted in\n// plaintext, and the user can delete them at any time via clearIceSettings().\n\nconst DB_NAME = 'securebit-net';\nconst DB_VERSION = 1;\nconst STORE = 'kv';\nconst KEY_RECORD = 'ice-device-key';\nconst SETTINGS_RECORD = 'ice-settings';\nconst SETTINGS_VERSION = 1;\n\nfunction isSupported() {\n return typeof indexedDB !== 'undefined' &&\n typeof crypto !== 'undefined' &&\n !!crypto.subtle;\n}\n\nfunction openDb() {\n return new Promise((resolve, reject) => {\n let request;\n try {\n request = indexedDB.open(DB_NAME, DB_VERSION);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction idbGet(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(key);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nfunction idbPut(db, key, value) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nfunction idbDelete(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nasync function getOrCreateDeviceKey(db) {\n const existing = await idbGet(db, KEY_RECORD);\n if (existing instanceof CryptoKey) {\n return existing;\n }\n const key = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable\n ['encrypt', 'decrypt']\n );\n await idbPut(db, KEY_RECORD, key);\n return key;\n}\n\n/**\n * Persist custom ICE settings, encrypted at rest.\n * @param {{ servers: Array, privacyMode: string }} settings\n */\nexport async function saveIceSettings(settings) {\n if (!isSupported()) throw new Error('Persistent storage is not available in this browser');\n\n const db = await openDb();\n const key = await getOrCreateDeviceKey(db);\n\n const payload = JSON.stringify({\n version: SETTINGS_VERSION,\n servers: Array.isArray(settings?.servers) ? settings.servers : [],\n privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'\n });\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n keLine truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["customIceServers", "iceServersText", "iceSettingsPersisted", "showIceSettings", "setShowIceSettings", "handleApplyIceSettings", "handleForgetIceSettings", "offerData", "payload", "UpdateCheckerWrapper"]
|
||||
}
|
||||
+4
-4
@@ -148,13 +148,13 @@
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script src="src/utils/updateManager.js"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1781550335673"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1781550335673"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1781552284668"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1781552284668"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1781550335673"></script>
|
||||
<script type="module" src="dist/app.js?v=1781550335673"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1781552284668"></script>
|
||||
<script type="module" src="dist/app.js?v=1781552284668"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1781550335673",
|
||||
"buildVersion": "1781550335673",
|
||||
"version": "1781552284668",
|
||||
"buildVersion": "1781552284668",
|
||||
"appVersion": "4.8.9",
|
||||
"buildTime": "2026-06-15T19:05:35.714Z",
|
||||
"buildId": "1781550335673-d11f250",
|
||||
"gitHash": "d11f250",
|
||||
"buildTime": "2026-06-15T19:38:04.713Z",
|
||||
"buildId": "1781552284668-366f080",
|
||||
"gitHash": "366f080",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-06-15T19:05:35.717Z"
|
||||
"generatedAt": "2026-06-15T19:38:04.715Z"
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"dev": "npm run build && python -m http.server 8000",
|
||||
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
||||
"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 && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.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 && node tests/ice-servers-validation.test.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"p2p",
|
||||
|
||||
+86
-2
@@ -1,4 +1,5 @@
|
||||
import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||
import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/iceSettingsStore.js';
|
||||
// Enhanced Copy Button with better UX
|
||||
const EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -530,7 +531,42 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||
}, 'Uses TURN relay-only when configured. Without TURN, direct WebRTC may expose IP addresses and relay-only connections cannot start.')
|
||||
])
|
||||
]),
|
||||
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'advanced-network',
|
||||
className: "mb-6 mx-auto max-w-2xl flex flex-wrap items-center justify-between gap-3"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "text-sm text-secondary"
|
||||
}, (Array.isArray(customIceServers) && customIceServers.length)
|
||||
? `Using ${customIceServers.length} custom ICE server(s)`
|
||||
: 'Using public ICE servers'),
|
||||
React.createElement('button', {
|
||||
key: 'btn',
|
||||
type: 'button',
|
||||
onClick: () => setShowIceSettings(true),
|
||||
className: "px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary"
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-network-wired mr-2' }),
|
||||
'Advanced network settings'
|
||||
])
|
||||
]),
|
||||
(typeof window !== 'undefined' && window.IceServerSettings) ? React.createElement(window.IceServerSettings, {
|
||||
key: 'ice-settings-modal',
|
||||
isOpen: showIceSettings,
|
||||
onClose: () => setShowIceSettings(false),
|
||||
initial: {
|
||||
useCustom: Array.isArray(customIceServers) && customIceServers.length > 0,
|
||||
serversText: iceServersText,
|
||||
privacyMode: relayOnlyMode ? 'relay-only' : 'standard',
|
||||
persisted: iceSettingsPersisted
|
||||
},
|
||||
hasSaved: iceSettingsPersisted,
|
||||
onApply: handleApplyIceSettings,
|
||||
onForget: handleForgetIceSettings
|
||||
}) : null,
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'options',
|
||||
className: "flex flex-col md:flex-row items-center justify-center gap-6 max-w-3xl mx-auto"
|
||||
@@ -1561,6 +1597,51 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||
const [relayOnlyMode, setRelayOnlyMode] = React.useState(() => {
|
||||
try { return localStorage.getItem('securebit_relay_only_mode') === 'true'; } catch { return false; }
|
||||
});
|
||||
// Custom ICE (STUN/TURN) servers — advanced network settings.
|
||||
const [customIceServers, setCustomIceServers] = React.useState(null); // null => use public defaults
|
||||
const [iceServersText, setIceServersText] = React.useState('');
|
||||
const [iceSettingsPersisted, setIceSettingsPersisted] = React.useState(false);
|
||||
const [showIceSettings, setShowIceSettings] = React.useState(false);
|
||||
|
||||
// Load any previously saved (encrypted) custom ICE settings on mount.
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadIceSettings().then((saved) => {
|
||||
if (cancelled || !saved) return;
|
||||
if (Array.isArray(saved.servers) && saved.servers.length > 0) {
|
||||
setCustomIceServers(saved.servers);
|
||||
setIceServersText(JSON.stringify(saved.servers, null, 2));
|
||||
}
|
||||
if (saved.privacyMode === 'relay-only') {
|
||||
setRelayOnlyMode(true);
|
||||
}
|
||||
setIceSettingsPersisted(true);
|
||||
}).catch(() => { /* fail closed: keep defaults */ });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const handleApplyIceSettings = React.useCallback((next, persist) => {
|
||||
const servers = next.useCustom && Array.isArray(next.servers) ? next.servers : null;
|
||||
setCustomIceServers(servers && servers.length ? servers : null);
|
||||
setIceServersText(next.serversText || '');
|
||||
setRelayOnlyMode(next.privacyMode === 'relay-only');
|
||||
setShowIceSettings(false);
|
||||
if (persist) {
|
||||
setIceSettingsPersisted(true);
|
||||
saveIceSettings({ servers: servers || [], privacyMode: next.privacyMode }).catch(() => { /* surfaced as no-op */ });
|
||||
} else if (iceSettingsPersisted) {
|
||||
// User turned persistence off — remove the stored copy.
|
||||
setIceSettingsPersisted(false);
|
||||
clearIceSettings().catch(() => {});
|
||||
}
|
||||
}, [iceSettingsPersisted]);
|
||||
|
||||
const handleForgetIceSettings = React.useCallback(async () => {
|
||||
await clearIceSettings().catch(() => {});
|
||||
setIceSettingsPersisted(false);
|
||||
setCustomIceServers(null);
|
||||
setIceServersText('');
|
||||
}, []);
|
||||
|
||||
// Moved scrollToBottom logic to be available globally
|
||||
const [messageInput, setMessageInput] = React.useState('');
|
||||
@@ -1999,7 +2080,10 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||
{
|
||||
webrtc: {
|
||||
relayOnly: relayOnlyMode,
|
||||
iceServers: Array.isArray(window.SECUREBIT_ICE_SERVERS) ? window.SECUREBIT_ICE_SERVERS : undefined
|
||||
// Priority: user's custom servers > operator override > built-in defaults.
|
||||
iceServers: (Array.isArray(customIceServers) && customIceServers.length)
|
||||
? customIceServers
|
||||
: (Array.isArray(window.SECUREBIT_ICE_SERVERS) ? window.SECUREBIT_ICE_SERVERS : undefined)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
// Advanced network settings: lets a user supply their own STUN/TURN servers
|
||||
// instead of the bundled public defaults, and toggle relay-only privacy mode.
|
||||
// Free / power-user feature, hidden behind an explicit "Advanced" entry point.
|
||||
//
|
||||
// All input is validated through src/network/iceServers.js before it can reach
|
||||
// RTCPeerConnection. Persistence is opt-in and encrypted at rest (see
|
||||
// src/network/iceSettingsStore.js).
|
||||
import {
|
||||
parseIceServersInput,
|
||||
listHasTurn,
|
||||
ICE_LIMITS
|
||||
} from '../../network/iceServers.js';
|
||||
|
||||
const React = window.React;
|
||||
|
||||
const PLACEHOLDER = [
|
||||
'# One URL per line, e.g.:',
|
||||
'stun:stun.example.com:3478',
|
||||
'turn:turn.example.com:3478?transport=udp',
|
||||
'',
|
||||
'# Or paste JSON for servers with credentials:',
|
||||
'[{"urls":"turns:turn.example.com:5349","username":"user","credential":"secret"}]'
|
||||
].join('\n');
|
||||
|
||||
async function testIceServers(servers, timeoutMs = 6000) {
|
||||
const found = { host: 0, srflx: 0, relay: 0 };
|
||||
if (typeof RTCPeerConnection === 'undefined') {
|
||||
return { ...found, error: 'WebRTC is not available in this browser' };
|
||||
}
|
||||
let pc;
|
||||
try {
|
||||
pc = new RTCPeerConnection({ iceServers: servers });
|
||||
} catch (error) {
|
||||
return { ...found, error: error.message || 'Invalid server configuration' };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
try { pc.close(); } catch { /* noop */ }
|
||||
resolve(found);
|
||||
};
|
||||
const timer = setTimeout(finish, timeoutMs);
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
if (!event.candidate) { finish(); return; }
|
||||
const c = event.candidate.candidate || '';
|
||||
if (/ typ host/.test(c)) found.host++;
|
||||
else if (/ typ srflx/.test(c)) found.srflx++;
|
||||
else if (/ typ relay/.test(c)) found.relay++;
|
||||
};
|
||||
|
||||
try {
|
||||
pc.createDataChannel('securebit-ice-test');
|
||||
pc.createOffer()
|
||||
.then((offer) => pc.setLocalDescription(offer))
|
||||
.catch(() => finish());
|
||||
} catch {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const [useCustom, setUseCustom] = React.useState(initial?.useCustom || false);
|
||||
const [serversText, setServersText] = React.useState(initial?.serversText || '');
|
||||
const [relayOnly, setRelayOnly] = React.useState(initial?.privacyMode === 'relay-only');
|
||||
const [persist, setPersist] = React.useState(initial?.persisted || false);
|
||||
const [testState, setTestState] = React.useState('idle'); // idle | running | done
|
||||
const [testResult, setTestResult] = React.useState(null);
|
||||
|
||||
const parsed = useCustom ? parseIceServersInput(serversText) : { servers: [], errors: [], warnings: [] };
|
||||
const hasTurn = listHasTurn(parsed.servers);
|
||||
const canApply = !useCustom || (parsed.servers.length > 0 && parsed.errors.length === 0);
|
||||
|
||||
const handleTest = async () => {
|
||||
setTestState('running');
|
||||
setTestResult(null);
|
||||
const result = await testIceServers(parsed.servers);
|
||||
setTestResult(result);
|
||||
setTestState('done');
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
if (!canApply) return;
|
||||
onApply(
|
||||
{
|
||||
useCustom,
|
||||
servers: useCustom ? parsed.servers : [],
|
||||
privacyMode: relayOnly ? 'relay-only' : 'standard',
|
||||
serversText
|
||||
},
|
||||
persist
|
||||
);
|
||||
};
|
||||
|
||||
const handleForget = async () => {
|
||||
if (onForget) await onForget();
|
||||
setPersist(false);
|
||||
};
|
||||
|
||||
const labelCls = 'block text-sm font-medium text-primary';
|
||||
const descCls = 'block text-sm text-secondary';
|
||||
|
||||
const children = [];
|
||||
|
||||
// Header
|
||||
children.push(React.createElement('div', { key: 'header', className: 'flex items-center mb-4' }, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
|
||||
}, [React.createElement('i', { className: 'fas fa-network-wired accent-purple' })]),
|
||||
React.createElement('h3', { key: 'title', className: 'text-lg font-medium text-primary' }, 'Advanced network settings')
|
||||
]));
|
||||
|
||||
// Explainer
|
||||
children.push(React.createElement('p', { key: 'intro', className: 'text-sm text-secondary mb-4' },
|
||||
'By default SecureBit uses public STUN servers. You can supply your own STUN/TURN servers — useful if you self-host a TURN relay and do not want to rely on public infrastructure. Servers are configured locally on your side only; you do not need to share them with your peer.'
|
||||
));
|
||||
|
||||
// Mode radios
|
||||
children.push(React.createElement('div', { key: 'mode', className: 'space-y-2 mb-4' }, [
|
||||
React.createElement('label', { key: 'public', className: 'flex items-start gap-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'r', type: 'radio', name: 'ice-mode', checked: !useCustom,
|
||||
onChange: () => setUseCustom(false), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Public servers (default)'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Zero-config. Good for most users.')
|
||||
])
|
||||
]),
|
||||
React.createElement('label', { key: 'custom', className: 'flex items-start gap-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'r', type: 'radio', name: 'ice-mode', checked: useCustom,
|
||||
onChange: () => setUseCustom(true), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'My own STUN/TURN servers'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`)
|
||||
])
|
||||
])
|
||||
]));
|
||||
|
||||
// Textarea + validation (only in custom mode)
|
||||
if (useCustom) {
|
||||
children.push(React.createElement('textarea', {
|
||||
key: 'textarea',
|
||||
value: serversText,
|
||||
onChange: (e) => setServersText(e.target.value),
|
||||
placeholder: PLACEHOLDER,
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
className: 'w-full h-36 mb-2 p-3 rounded-lg bg-black/30 border border-purple-500/20 text-sm text-primary font-mono'
|
||||
}));
|
||||
|
||||
if (parsed.errors.length > 0) {
|
||||
children.push(React.createElement('ul', { key: 'errors', className: 'mb-2 text-sm text-red-400 list-disc pl-5' },
|
||||
parsed.errors.slice(0, 6).map((err, i) => React.createElement('li', { key: i }, err))
|
||||
));
|
||||
}
|
||||
if (parsed.warnings.length > 0) {
|
||||
children.push(React.createElement('ul', { key: 'warnings', className: 'mb-2 text-sm text-yellow-400 list-disc pl-5' },
|
||||
parsed.warnings.slice(0, 6).map((w, i) => React.createElement('li', { key: i }, w))
|
||||
));
|
||||
}
|
||||
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
|
||||
children.push(React.createElement('p', { key: 'ok', className: 'mb-2 text-sm text-green-400' },
|
||||
`${parsed.servers.length} server(s) parsed${hasTurn ? ' (TURN present)' : ' (STUN only — does not hide IP)'}.`
|
||||
));
|
||||
}
|
||||
|
||||
// Privacy disclaimer about third-party relays
|
||||
children.push(React.createElement('p', { key: 'disclaimer', className: 'mb-3 text-xs text-secondary' },
|
||||
'Privacy note: a TURN relay sees the IP addresses and traffic timing of both peers (never your message contents, which stay end-to-end encrypted). Only a TURN server you trust or self-host improves privacy — pointing this at a random public relay does not. Prefer turns: (TLS).'
|
||||
));
|
||||
|
||||
// Test button + result
|
||||
children.push(React.createElement('div', { key: 'test', className: 'mb-3' }, [
|
||||
React.createElement('button', {
|
||||
key: 'btn',
|
||||
type: 'button',
|
||||
disabled: !canApply || testState === 'running',
|
||||
onClick: handleTest,
|
||||
className: 'px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary disabled:opacity-50'
|
||||
}, testState === 'running' ? 'Testing…' : 'Test servers'),
|
||||
testState === 'done' && testResult ? React.createElement('span', {
|
||||
key: 'res',
|
||||
className: 'ml-3 text-sm ' + (testResult.error ? 'text-red-400' : 'text-secondary')
|
||||
}, testResult.error
|
||||
? `Test failed: ${testResult.error}`
|
||||
: `STUN ${testResult.srflx > 0 ? 'OK' : 'none'} · TURN ${testResult.relay > 0 ? 'OK' : 'none'} · host ${testResult.host}`
|
||||
) : null
|
||||
]));
|
||||
}
|
||||
|
||||
// Relay-only privacy toggle
|
||||
children.push(React.createElement('label', { key: 'relay', className: 'flex items-start gap-3 mb-3 rounded-lg border border-purple-500/20 bg-purple-500/10 p-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'i', type: 'checkbox', checked: relayOnly,
|
||||
onChange: (e) => setRelayOnly(e.target.checked), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Relay-only mode (maximum privacy)'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Routes all traffic through TURN so your IP is not exposed to the peer. Requires a TURN server; connections cannot start without one.')
|
||||
])
|
||||
]));
|
||||
if (relayOnly && useCustom && !hasTurn) {
|
||||
children.push(React.createElement('p', { key: 'relaywarn', className: 'mb-3 text-sm text-yellow-400' },
|
||||
'Relay-only is enabled but no TURN server is configured. The connection will not be able to start.'
|
||||
));
|
||||
}
|
||||
|
||||
// Save on device
|
||||
children.push(React.createElement('label', { key: 'persist', className: 'flex items-start gap-3 mb-4' }, [
|
||||
React.createElement('input', {
|
||||
key: 'i', type: 'checkbox', checked: persist,
|
||||
onChange: (e) => setPersist(e.target.checked), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Save on this device'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Stored encrypted in this browser. Leave off to use only for this session.')
|
||||
])
|
||||
]));
|
||||
|
||||
// Action buttons
|
||||
const actions = [
|
||||
React.createElement('button', {
|
||||
key: 'cancel', type: 'button', onClick: onClose,
|
||||
className: 'px-4 py-2 text-sm rounded-lg border border-white/10 text-secondary'
|
||||
}, 'Cancel'),
|
||||
React.createElement('button', {
|
||||
key: 'apply', type: 'button', onClick: handleApply, disabled: !canApply,
|
||||
className: 'px-4 py-2 text-sm rounded-lg bg-purple-500/20 border border-purple-500/30 text-primary disabled:opacity-50'
|
||||
}, 'Apply')
|
||||
];
|
||||
if (hasSaved) {
|
||||
actions.unshift(React.createElement('button', {
|
||||
key: 'forget', type: 'button', onClick: handleForget,
|
||||
className: 'px-4 py-2 text-sm rounded-lg border border-red-500/30 text-red-400 mr-auto'
|
||||
}, 'Forget saved'));
|
||||
}
|
||||
children.push(React.createElement('div', { key: 'actions', className: 'flex items-center gap-2 flex-wrap' }, actions));
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4',
|
||||
onClick: (e) => { if (e.target === e.currentTarget) onClose(); }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-lg w-full border-purple-500/20 max-h-[90vh] overflow-y-auto'
|
||||
}, children)
|
||||
]);
|
||||
};
|
||||
|
||||
window.IceServerSettings = IceServerSettings;
|
||||
|
||||
export { IceServerSettings, testIceServers };
|
||||
@@ -0,0 +1,183 @@
|
||||
// Pure, dependency-free validation and normalization of user-supplied ICE
|
||||
// (STUN/TURN) servers. No DOM, no browser APIs — safe to unit-test in Node.
|
||||
//
|
||||
// Security intent: a user can paste arbitrary text here. We must never hand an
|
||||
// un-vetted value to RTCPeerConnection, and we must reject anything that is not
|
||||
// a real STUN/TURN URL (e.g. javascript:, data:, http(s):, ws(s):) so the field
|
||||
// cannot be abused as an injection surface or silently break the connection.
|
||||
// Validation is allowlist-based: only known-good schemes, host shapes, and the
|
||||
// standard transport query are accepted; everything else is rejected.
|
||||
|
||||
export const ICE_LIMITS = Object.freeze({
|
||||
MAX_SERVERS: 10,
|
||||
MAX_URLS_PER_SERVER: 8,
|
||||
MAX_STRING_LENGTH: 512
|
||||
});
|
||||
|
||||
// RTCIceServer only accepts these schemes. Everything else is rejected.
|
||||
export const ALLOWED_ICE_SCHEMES = Object.freeze(['stun', 'stuns', 'turn', 'turns']);
|
||||
|
||||
const SCHEME_RE = /^(stuns?|turns?):/i;
|
||||
// Positive allowlist for the host portion: hostname/IPv4, or bracketed IPv6,
|
||||
// with an optional numeric port. Anything outside this shape is rejected.
|
||||
const HOST_RE = /^(\[[0-9a-f:]+\]|[a-z0-9.-]+)(:\d{1,5})?$/i;
|
||||
const TRANSPORT_RE = /^transport=(udp|tcp)$/i;
|
||||
|
||||
// True if the string contains any ASCII control character (0x00–0x1F or 0x7F).
|
||||
// Implemented via char codes to keep the source free of literal control bytes.
|
||||
function hasControlChars(value) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
if (code < 0x20 || code === 0x7f) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single ICE URL string.
|
||||
* @returns {string|null} error message, or null if valid.
|
||||
*/
|
||||
export function validateIceUrl(url) {
|
||||
if (typeof url !== 'string') return 'URL must be a string';
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return 'URL is empty';
|
||||
if (trimmed.length > ICE_LIMITS.MAX_STRING_LENGTH) return 'URL is too long';
|
||||
if (hasControlChars(trimmed)) return 'URL contains invalid characters';
|
||||
|
||||
const scheme = trimmed.match(SCHEME_RE);
|
||||
if (!scheme) {
|
||||
return 'URL must start with stun:, stuns:, turn: or turns:';
|
||||
}
|
||||
|
||||
// Validate the part after "<scheme>:" — host[:port][?transport=udp|tcp].
|
||||
const rest = trimmed.slice(scheme[0].length);
|
||||
const [hostPort, query, ...extra] = rest.split('?');
|
||||
if (extra.length > 0) return 'URL has an invalid query';
|
||||
if (!hostPort) return 'URL is missing a host';
|
||||
if (!HOST_RE.test(hostPort)) return 'URL has an invalid host or port';
|
||||
|
||||
if (query !== undefined && !TRANSPORT_RE.test(query)) {
|
||||
return 'URL query must be transport=udp or transport=tcp';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isTurnUrl(url) {
|
||||
return typeof url === 'string' && /^turns?:/i.test(url.trim());
|
||||
}
|
||||
|
||||
function validateSecret(value, label) {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
if (typeof value !== 'string') return `${label} must be a string`;
|
||||
if (value.length > ICE_LIMITS.MAX_STRING_LENGTH) return `${label} is too long`;
|
||||
if (hasControlChars(value)) return `${label} contains invalid characters`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize a list of ICE server entries.
|
||||
* Each entry: { urls: string | string[], username?: string, credential?: string }
|
||||
* @returns {{ servers: Array, errors: string[], warnings: string[] }}
|
||||
*/
|
||||
export function normalizeIceServers(entries) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const servers = [];
|
||||
|
||||
if (!Array.isArray(entries)) {
|
||||
return { servers: [], errors: ['Server list must be an array'], warnings: [] };
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
return { servers: [], errors: [], warnings: [] };
|
||||
}
|
||||
if (entries.length > ICE_LIMITS.MAX_SERVERS) {
|
||||
errors.push(`Too many servers (max ${ICE_LIMITS.MAX_SERVERS})`);
|
||||
return { servers: [], errors, warnings };
|
||||
}
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
const label = `Server #${index + 1}`;
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
errors.push(`${label}: invalid entry`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawUrls = Array.isArray(entry.urls) ? entry.urls : [entry.urls];
|
||||
if (rawUrls.length === 0 || rawUrls.length > ICE_LIMITS.MAX_URLS_PER_SERVER) {
|
||||
errors.push(`${label}: between 1 and ${ICE_LIMITS.MAX_URLS_PER_SERVER} URLs required`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanUrls = [];
|
||||
let entryHasTurn = false;
|
||||
for (const rawUrl of rawUrls) {
|
||||
const err = validateIceUrl(rawUrl);
|
||||
if (err) {
|
||||
errors.push(`${label}: ${err}`);
|
||||
continue;
|
||||
}
|
||||
const trimmed = rawUrl.trim();
|
||||
cleanUrls.push(trimmed);
|
||||
if (isTurnUrl(trimmed)) entryHasTurn = true;
|
||||
}
|
||||
if (cleanUrls.length === 0) return;
|
||||
|
||||
const userErr = validateSecret(entry.username, `${label} username`);
|
||||
if (userErr) errors.push(userErr);
|
||||
const credErr = validateSecret(entry.credential, `${label} credential`);
|
||||
if (credErr) errors.push(credErr);
|
||||
|
||||
const server = { urls: cleanUrls.length === 1 ? cleanUrls[0] : cleanUrls };
|
||||
if (entry.username) server.username = String(entry.username);
|
||||
if (entry.credential) server.credential = String(entry.credential);
|
||||
|
||||
if (entryHasTurn && (!server.username || !server.credential)) {
|
||||
warnings.push(`${label}: TURN servers usually require a username and credential`);
|
||||
}
|
||||
|
||||
servers.push(server);
|
||||
});
|
||||
|
||||
return { servers, errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse free-form user input into ICE server entries.
|
||||
* Accepts either a JSON array of RTCIceServer-like objects, or one URL per line
|
||||
* (URL-only servers, e.g. public STUN). Returns normalized + validated output.
|
||||
*/
|
||||
export function parseIceServersInput(text) {
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
return { servers: [], errors: [], warnings: [] };
|
||||
}
|
||||
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch {
|
||||
return { servers: [], errors: ['Invalid JSON'], warnings: [] };
|
||||
}
|
||||
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
||||
return normalizeIceServers(arr);
|
||||
}
|
||||
|
||||
// Line-based: each non-empty line is a single URL-only server.
|
||||
const entries = trimmed
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(url => ({ urls: url }));
|
||||
return normalizeIceServers(entries);
|
||||
}
|
||||
|
||||
/** Does a normalized server list contain at least one TURN/TURNS server? */
|
||||
export function listHasTurn(servers) {
|
||||
if (!Array.isArray(servers)) return false;
|
||||
return servers.some(server => {
|
||||
const urls = Array.isArray(server?.urls) ? server.urls : [server?.urls];
|
||||
return urls.some(isTurnUrl);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)
|
||||
// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when
|
||||
// the user explicitly opts in ("Save on this device"). Session-only use never
|
||||
// touches this store — the settings live in React state and vanish on reload.
|
||||
//
|
||||
// At-rest protection model:
|
||||
// - A non-extractable AES-GCM device key is generated once and kept in
|
||||
// IndexedDB. It can never be exported back into JS, so a copy of the
|
||||
// on-disk database is useless without executing code in this exact origin.
|
||||
// - The settings blob (which may contain TURN credentials) is encrypted with
|
||||
// that key before being written.
|
||||
// This protects against disk/profile inspection. It does NOT protect against a
|
||||
// live code-execution compromise of the page (consistent with the project's
|
||||
// stated threat model — see SECURITY.md). Credentials are never persisted in
|
||||
// plaintext, and the user can delete them at any time via clearIceSettings().
|
||||
|
||||
const DB_NAME = 'securebit-net';
|
||||
const DB_VERSION = 1;
|
||||
const STORE = 'kv';
|
||||
const KEY_RECORD = 'ice-device-key';
|
||||
const SETTINGS_RECORD = 'ice-settings';
|
||||
const SETTINGS_VERSION = 1;
|
||||
|
||||
function isSupported() {
|
||||
return typeof indexedDB !== 'undefined' &&
|
||||
typeof crypto !== 'undefined' &&
|
||||
!!crypto.subtle;
|
||||
}
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request;
|
||||
try {
|
||||
request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE)) {
|
||||
db.createObjectStore(STORE);
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
function idbGet(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readonly');
|
||||
const req = tx.objectStore(STORE).get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
function idbPut(db, key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readwrite');
|
||||
tx.objectStore(STORE).put(value, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
function idbDelete(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readwrite');
|
||||
tx.objectStore(STORE).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async function getOrCreateDeviceKey(db) {
|
||||
const existing = await idbGet(db, KEY_RECORD);
|
||||
if (existing instanceof CryptoKey) {
|
||||
return existing;
|
||||
}
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false, // non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
await idbPut(db, KEY_RECORD, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist custom ICE settings, encrypted at rest.
|
||||
* @param {{ servers: Array, privacyMode: string }} settings
|
||||
*/
|
||||
export async function saveIceSettings(settings) {
|
||||
if (!isSupported()) throw new Error('Persistent storage is not available in this browser');
|
||||
|
||||
const db = await openDb();
|
||||
const key = await getOrCreateDeviceKey(db);
|
||||
|
||||
const payload = JSON.stringify({
|
||||
version: SETTINGS_VERSION,
|
||||
servers: Array.isArray(settings?.servers) ? settings.servers : [],
|
||||
privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'
|
||||
});
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
new TextEncoder().encode(payload)
|
||||
);
|
||||
|
||||
await idbPut(db, SETTINGS_RECORD, {
|
||||
iv: Array.from(iv),
|
||||
data: Array.from(new Uint8Array(ciphertext))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and decrypt previously saved ICE settings.
|
||||
* Fails closed: returns null if absent, unsupported, or undecryptable.
|
||||
* @returns {Promise<{ servers: Array, privacyMode: string }|null>}
|
||||
*/
|
||||
export async function loadIceSettings() {
|
||||
if (!isSupported()) return null;
|
||||
|
||||
try {
|
||||
const db = await openDb();
|
||||
const record = await idbGet(db, SETTINGS_RECORD);
|
||||
if (!record || !Array.isArray(record.iv) || !Array.isArray(record.data)) {
|
||||
return null;
|
||||
}
|
||||
const key = await idbGet(db, KEY_RECORD);
|
||||
if (!(key instanceof CryptoKey)) return null;
|
||||
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: new Uint8Array(record.iv) },
|
||||
key,
|
||||
new Uint8Array(record.data)
|
||||
);
|
||||
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
||||
return {
|
||||
servers: Array.isArray(parsed.servers) ? parsed.servers : [],
|
||||
privacyMode: parsed.privacyMode === 'relay-only' ? 'relay-only' : 'standard'
|
||||
};
|
||||
} catch {
|
||||
// Corrupted or tampered record: fail closed.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete any persisted ICE settings (the device key is left in place). */
|
||||
export async function clearIceSettings() {
|
||||
if (!isSupported()) return;
|
||||
try {
|
||||
const db = await openDb();
|
||||
await idbDelete(db, SETTINGS_RECORD);
|
||||
} catch {
|
||||
// Best-effort deletion; nothing to surface to the user.
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasSavedIceSettings() {
|
||||
if (!isSupported()) return false;
|
||||
try {
|
||||
const db = await openDb();
|
||||
const record = await idbGet(db, SETTINGS_RECORD);
|
||||
return !!record;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import '../components/ui/Testimonials.jsx';
|
||||
import '../components/ui/ComparisonTable.jsx';
|
||||
import '../components/ui/Roadmap.jsx';
|
||||
import '../components/ui/FileTransfer.jsx';
|
||||
import '../components/ui/IceServerSettings.jsx';
|
||||
|
||||
// Expose to global for legacy usage inside app code
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
|
||||
Vendored
+1
@@ -36,6 +36,7 @@
|
||||
import(`../components/ui/Testimonials.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/Roadmap.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/FileTransfer.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/IceServerSettings.jsx?v=${timestamp}`),
|
||||
]);
|
||||
|
||||
// Components are automatically registered on window by their respective modules
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user