mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix: proxy LiveKit through HTTPS to fix mixed-content block
- Add reverse proxy at /livekit/* that forwards to LiveKit server - Server sends relative URL "/livekit" in voice_token; client resolves to wss://server:port/livekit using known server host - Fix API secret minimum length (32 chars required by LiveKit) - Pass TLS config to LiveKit process manager for TURN certs
This commit is contained in:
@@ -40,6 +40,8 @@ let ws: WsClient | null = null;
|
||||
let noiseSuppressor: NoiseSuppressor | null = null;
|
||||
let onErrorCallback: ((message: string) => void) | null = null;
|
||||
let currentChannelId: number | null = null;
|
||||
/** Server host (e.g. "192.168.0.247:8443") for constructing LiveKit proxy URL. */
|
||||
let serverHost: string | null = null;
|
||||
|
||||
/** The raw mic stream acquired for RNNoise processing (must be stopped on cleanup). */
|
||||
let rawMicStream: MediaStream | null = null;
|
||||
@@ -243,6 +245,23 @@ export function setWsClient(client: WsClient): void {
|
||||
ws = client;
|
||||
}
|
||||
|
||||
/** Set the server host for constructing LiveKit proxy URLs. */
|
||||
export function setServerHost(host: string): void {
|
||||
serverHost = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a LiveKit URL. If the server sends a relative path like "/livekit",
|
||||
* construct the full wss:// URL using the server host. This proxies LiveKit
|
||||
* signaling through OwnCord's HTTPS to avoid mixed-content blocks.
|
||||
*/
|
||||
function resolveLiveKitUrl(url: string): string {
|
||||
if (url.startsWith("/") && serverHost !== null) {
|
||||
return `wss://${serverHost}${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Set error callback for UI feedback (e.g. toast on connection failure). */
|
||||
export function setOnError(cb: (message: string) => void): void {
|
||||
onErrorCallback = cb;
|
||||
@@ -301,9 +320,10 @@ export async function handleVoiceToken(
|
||||
room.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged);
|
||||
room.on(RoomEvent.Disconnected, handleDisconnected);
|
||||
|
||||
// Connect to LiveKit server
|
||||
await room.connect(url, token);
|
||||
log.info("Connected to LiveKit room", { channelId, url });
|
||||
// Connect to LiveKit server (resolve proxy URL if relative path)
|
||||
const resolvedUrl = resolveLiveKitUrl(url);
|
||||
await room.connect(resolvedUrl, token);
|
||||
log.info("Connected to LiveKit room", { channelId, url: resolvedUrl });
|
||||
|
||||
// Enable microphone: use RNNoise if Enhanced Noise Suppression is on
|
||||
const enhancedNS = loadPref<boolean>("enhancedNoiseSuppression", false);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
setOnRemoteVideoRemoved,
|
||||
clearOnRemoteVideo,
|
||||
setWsClient,
|
||||
setServerHost as setLiveKitServerHost,
|
||||
setOnError as setVoiceOnError,
|
||||
clearOnError as clearVoiceOnError,
|
||||
} from "@lib/livekitSession";
|
||||
@@ -79,10 +80,11 @@ export function createMainPage(options: MainPageOptions): MountableComponent {
|
||||
// Let voiceSession send signaling messages over this WS connection
|
||||
setWsClient(ws);
|
||||
|
||||
// Set server host for resolving relative attachment URLs
|
||||
// Set server host for resolving relative attachment URLs and LiveKit proxy
|
||||
const apiConfig = api.getConfig();
|
||||
if (apiConfig.host) {
|
||||
setServerHost(apiConfig.host);
|
||||
setLiveKitServerHost(apiConfig.host);
|
||||
}
|
||||
|
||||
const limiters = createRateLimiterSet();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// NewLiveKitProxy creates a reverse proxy handler that forwards requests
|
||||
// to the LiveKit server. This allows the client to reach LiveKit through
|
||||
// OwnCord's existing HTTPS server, avoiding mixed-content blocks in
|
||||
// WebView2 (secure page → insecure WebSocket).
|
||||
//
|
||||
// The client connects to wss://server:8443/livekit/ which is proxied to
|
||||
// ws://localhost:7880/ on the LiveKit server.
|
||||
func NewLiveKitProxy(livekitURL string) http.Handler {
|
||||
target, err := url.Parse(livekitURL)
|
||||
if err != nil {
|
||||
// Fall back to default if URL is invalid
|
||||
target, _ = url.Parse("http://localhost:7880")
|
||||
}
|
||||
|
||||
// Convert ws:// to http:// for the proxy target
|
||||
switch target.Scheme {
|
||||
case "ws":
|
||||
target.Scheme = "http"
|
||||
case "wss":
|
||||
target.Scheme = "https"
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
},
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
|
||||
|
||||
// Optionally start a companion LiveKit process.
|
||||
if cfg.Voice.LiveKitBinaryPath != "" {
|
||||
proc := ws.NewLiveKitProcess(&cfg.Voice, cfg.Server.DataDir)
|
||||
proc := ws.NewLiveKitProcess(&cfg.Voice, &cfg.TLS, cfg.Server.DataDir)
|
||||
if startErr := proc.Start(); startErr != nil {
|
||||
slog.Error("failed to start LiveKit process", "error", startErr)
|
||||
} else {
|
||||
@@ -88,6 +88,11 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
|
||||
if lkErr == nil {
|
||||
r.Post("/api/v1/livekit/webhook",
|
||||
ws.MountWebhookRoute(hub, cfg.Voice.LiveKitAPIKey, cfg.Voice.LiveKitAPISecret))
|
||||
|
||||
// Reverse proxy LiveKit signaling through OwnCord's HTTPS server.
|
||||
// This avoids mixed-content blocks (secure page → insecure WS).
|
||||
// Client connects to wss://server:8443/livekit/* → ws://localhost:7880/*
|
||||
r.Handle("/livekit/*", http.StripPrefix("/livekit", NewLiveKitProxy(cfg.Voice.LiveKitURL)))
|
||||
}
|
||||
|
||||
go hub.Run()
|
||||
|
||||
@@ -101,7 +101,7 @@ func defaults() Config {
|
||||
},
|
||||
Voice: VoiceConfig{
|
||||
LiveKitAPIKey: "devkey",
|
||||
LiveKitAPISecret: "secret",
|
||||
LiveKitAPISecret: "owncord-dev-secret-key-min-32chars",
|
||||
LiveKitURL: "ws://localhost:7880",
|
||||
Quality: "medium",
|
||||
},
|
||||
@@ -139,8 +139,8 @@ upload:
|
||||
storage_dir: "data/uploads"
|
||||
|
||||
voice:
|
||||
livekit_api_key: "devkey" # LiveKit API key
|
||||
livekit_api_secret: "secret" # LiveKit API secret
|
||||
livekit_api_key: "devkey" # LiveKit API key
|
||||
livekit_api_secret: "owncord-dev-secret-key-min-32chars" # LiveKit API secret (min 32 chars)
|
||||
livekit_url: "ws://localhost:7880" # LiveKit server WebSocket URL
|
||||
# livekit_binary: "" # path to livekit-server binary; empty = don't auto-start
|
||||
# quality: "medium" # low | medium | high
|
||||
|
||||
@@ -242,7 +242,7 @@ func TestLoadVoiceConfigDefaults(t *testing.T) {
|
||||
}{
|
||||
{"Voice.Quality", cfg.Voice.Quality, "medium"},
|
||||
{"Voice.LiveKitAPIKey", cfg.Voice.LiveKitAPIKey, "devkey"},
|
||||
{"Voice.LiveKitAPISecret", cfg.Voice.LiveKitAPISecret, "secret"},
|
||||
{"Voice.LiveKitAPISecret", cfg.Voice.LiveKitAPISecret, "owncord-dev-secret-key-min-32chars"},
|
||||
{"Voice.LiveKitURL", cfg.Voice.LiveKitURL, "ws://localhost:7880"},
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@ import (
|
||||
|
||||
// LiveKitProcess manages the companion livekit-server binary.
|
||||
type LiveKitProcess struct {
|
||||
cfg *config.VoiceConfig
|
||||
dataDir string
|
||||
cfg *config.VoiceConfig
|
||||
tlsCfg *config.TLSConfig
|
||||
dataDir string
|
||||
|
||||
mu sync.Mutex
|
||||
cmd *exec.Cmd
|
||||
@@ -31,9 +32,12 @@ type LiveKitProcess struct {
|
||||
|
||||
// NewLiveKitProcess creates a new process manager. It does not start the
|
||||
// process — call Start() to launch the LiveKit server.
|
||||
func NewLiveKitProcess(cfg *config.VoiceConfig, dataDir string) *LiveKitProcess {
|
||||
// The tlsCfg is used to configure TLS on the LiveKit server using the same
|
||||
// certs as OwnCord (avoids mixed-content blocks in WebView2).
|
||||
func NewLiveKitProcess(cfg *config.VoiceConfig, tlsCfg *config.TLSConfig, dataDir string) *LiveKitProcess {
|
||||
return &LiveKitProcess{
|
||||
cfg: cfg,
|
||||
tlsCfg: tlsCfg,
|
||||
dataDir: dataDir,
|
||||
}
|
||||
}
|
||||
@@ -42,6 +46,23 @@ func NewLiveKitProcess(cfg *config.VoiceConfig, dataDir string) *LiveKitProcess
|
||||
func (p *LiveKitProcess) generateConfig() (string, error) {
|
||||
cfgPath := filepath.Join(p.dataDir, "livekit.yaml")
|
||||
|
||||
// Build TLS section if OwnCord has TLS configured with cert files.
|
||||
tlsSection := ""
|
||||
if p.tlsCfg != nil && p.tlsCfg.CertFile != "" && p.tlsCfg.KeyFile != "" {
|
||||
// Resolve cert/key paths relative to the data directory's parent
|
||||
// (same working directory as chatserver).
|
||||
certFile := p.tlsCfg.CertFile
|
||||
keyFile := p.tlsCfg.KeyFile
|
||||
tlsSection = fmt.Sprintf(`
|
||||
turn:
|
||||
enabled: true
|
||||
tls_port: 5349
|
||||
udp_port: 3478
|
||||
cert_file: %s
|
||||
key_file: %s
|
||||
`, certFile, keyFile)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`# Auto-generated by OwnCord — do not edit manually.
|
||||
port: 7880
|
||||
rtc:
|
||||
@@ -49,10 +70,10 @@ rtc:
|
||||
port_range_end: 60000
|
||||
use_external_ip: true
|
||||
keys:
|
||||
%s: %s
|
||||
%s: %s%s
|
||||
logging:
|
||||
level: info
|
||||
`, p.cfg.LiveKitAPIKey, p.cfg.LiveKitAPISecret)
|
||||
`, p.cfg.LiveKitAPIKey, p.cfg.LiveKitAPISecret, tlsSection)
|
||||
|
||||
if err := os.MkdirAll(p.dataDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("creating data dir: %w", err)
|
||||
|
||||
@@ -103,7 +103,10 @@ func (h *Hub) handleVoiceJoin(c *Client, payload json.RawMessage) {
|
||||
slog.Error("ws handleVoiceJoin GenerateToken", "err", tokenErr, "user_id", c.userID)
|
||||
// Non-fatal: voice join still succeeds at the DB/state level.
|
||||
} else {
|
||||
c.sendMsg(buildVoiceToken(channelID, token, h.livekit.URL()))
|
||||
// Send "/livekit" as URL — client constructs the full wss:// URL
|
||||
// from its server connection. Proxied through OwnCord's HTTPS
|
||||
// to avoid mixed-content blocks in WebView2.
|
||||
c.sendMsg(buildVoiceToken(channelID, token, "/livekit"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user