Initial commit: mumh5, a modern Mumble client
Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import tls from 'node:tls';
|
||||
import { describeCert, type CertDetails } from './certs.ts';
|
||||
|
||||
export interface TlsHandlers {
|
||||
onSecure(info: { fingerprint: string; authorized: boolean; authError: string | null; chain: CertDetails[] }): void;
|
||||
onData(chunk: Uint8Array): void;
|
||||
onClose(reason: string): void;
|
||||
}
|
||||
|
||||
// Opens a TLS connection to a Mumble server. Most servers use self-signed certificates,
|
||||
// so verification is left to the caller (trust on first use via the sha256 fingerprint).
|
||||
export function openTls(host: string, port: number, cert: string, key: string, h: TlsHandlers) {
|
||||
let closeReason = 'Connection closed';
|
||||
const socket = tls.connect({
|
||||
host, port, cert, key,
|
||||
rejectUnauthorized: false,
|
||||
servername: /^[\d.:]+$/.test(host) ? undefined : host
|
||||
});
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
socket.setTimeout(20000, () => {
|
||||
closeReason = 'Connection timed out';
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('secureConnect', () => {
|
||||
socket.setTimeout(0);
|
||||
// Leaf first, then the issuers the server sent (the root links to itself)
|
||||
const chain: CertDetails[] = [];
|
||||
let cur = socket.getPeerCertificate(true) as tls.DetailedPeerCertificate | undefined;
|
||||
const seen = new Set<string>();
|
||||
while (cur?.raw && !seen.has(cur.fingerprint256) && chain.length < 8) {
|
||||
seen.add(cur.fingerprint256);
|
||||
chain.push(describeCert(cur.raw));
|
||||
cur = cur.issuerCertificate;
|
||||
}
|
||||
h.onSecure({
|
||||
chain,
|
||||
fingerprint: chain[0]?.fingerprint256 ?? '',
|
||||
authorized: socket.authorized,
|
||||
authError: socket.authorizationError ? String(socket.authorizationError) : null
|
||||
});
|
||||
});
|
||||
socket.on('data', (chunk: Buffer) => h.onData(new Uint8Array(chunk)));
|
||||
socket.on('error', (e: Error) => { closeReason = e.message; });
|
||||
socket.on('close', () => h.onClose(closeReason));
|
||||
return {
|
||||
send(bytes: Uint8Array) { if (!socket.destroyed) socket.write(bytes); },
|
||||
close() { closeReason = 'Disconnected'; socket.destroy(); }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user