Add the browser version, a toolbar, description previews and hints
Browser version - Self-hosted proxy (server/) that serves the web build and bridges WebSocket connections to Mumble servers over TLS, with a server allowlist or allow-any mode - WebSocket transport and a browser platform layer; identities live in the browser - npm run build:web, dev:web, proxy and test:e2e:web; proxy unit tests Interface - Toolbar next to mute and deafen: description editor, settings, expand or collapse all - Channels with a description show a marker; resting on the row previews it - Collapse all keeps channels with people open - Hints (title tooltips) can be switched on in a new Accessibility settings tab - New identities can set the username suggested when connecting - Own user information tells the client address from the one the server sees Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,4 +51,7 @@ export interface ServerCertInfo {
|
||||
authorized: boolean; // true if it chains to a trusted CA
|
||||
authError: string | null; // why it is not trusted, e.g. DEPTH_ZERO_SELF_SIGNED_CERT
|
||||
chain: CertDetails[]; // leaf first
|
||||
// Browser build only: this client's address as the web proxy saw it. The Mumble server
|
||||
// sees the proxy's address instead and cannot be told the original one.
|
||||
clientAddress?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Transport, ServerCertInfo } from './transport.ts';
|
||||
|
||||
// Transport through the mumh5 web proxy: one WebSocket per Mumble connection. The proxy opens
|
||||
// the TLS socket and presents the client certificate, since a browser can do neither.
|
||||
// `secure` resolves once the proxy has finished the TLS handshake, before any Mumble data is
|
||||
// sent, so the caller can check the server certificate before sending a password.
|
||||
// There is no UDP here; voice goes through the TCP tunnel.
|
||||
export class WebSocketTransport implements Transport {
|
||||
onData: ((chunk: Uint8Array) => void) | null = null;
|
||||
onClose: ((reason: string) => void) | null = null;
|
||||
readonly secure: Promise<ServerCertInfo>;
|
||||
private ws: WebSocket;
|
||||
|
||||
constructor(url: string, target: { host: string; port: number }, identity: { certPem: string; keyPem: string }) {
|
||||
let resolveSecure!: (i: ServerCertInfo) => void;
|
||||
let rejectSecure!: (e: Error) => void;
|
||||
this.secure = new Promise((res, rej) => { resolveSecure = res; rejectSecure = rej; });
|
||||
// Callers that never await `secure` still get onClose
|
||||
this.secure.catch(() => {});
|
||||
let reason = '';
|
||||
let opened = false;
|
||||
const ws = new WebSocket(url);
|
||||
this.ws = ws;
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = () => {
|
||||
opened = true;
|
||||
ws.send(JSON.stringify({ host: target.host, port: target.port, certPem: identity.certPem, keyPem: identity.keyPem }));
|
||||
};
|
||||
ws.onmessage = e => {
|
||||
if (typeof e.data !== 'string') return this.onData?.(new Uint8Array(e.data as ArrayBuffer));
|
||||
let msg: any;
|
||||
try { msg = JSON.parse(e.data); } catch { return; }
|
||||
if (msg.type === 'secure') resolveSecure(msg.info);
|
||||
else if (msg.type === 'close') reason = String(msg.reason ?? '');
|
||||
};
|
||||
ws.onclose = () => {
|
||||
reason ||= opened ? 'Connection closed' : 'Could not reach the proxy';
|
||||
rejectSecure(new Error(reason));
|
||||
this.onClose?.(reason);
|
||||
};
|
||||
}
|
||||
|
||||
send(bytes: Uint8Array): void {
|
||||
if (this.ws.readyState === WebSocket.OPEN) this.ws.send(bytes as Uint8Array<ArrayBuffer>);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user