mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Phase 5 — Voice: - migrations/002_voice_states.sql: voice_states table with FK + index - db/voice_queries: JoinVoiceChannel, LeaveVoiceChannel, GetVoiceState, GetChannelVoiceStates, UpdateVoiceMute, UpdateVoiceDeafen, ClearVoiceState - ws/voice_handlers: handleVoiceJoin (perm check, DB, broadcast existing states), handleVoiceLeave, handleVoiceMute, handleVoiceDeafen, handleVoiceSignal (rate-limited relay, SDP never logged), handleSoundboard (rate-limited, USE_SOUNDBOARD perm check) - ws/handlers: dispatch voice_join/leave/mute/deafen/offer/answer/ice/soundboard - ws/serve: call handleVoiceLeave on disconnect; include voice states in ready payload - ws/messages: buildVoiceState, buildVoiceLeave, buildVoiceSignalRelay - api/voice_handler: GET /api/v1/voice/credentials — HMAC-SHA1 TURN creds - config: VoiceConfig (TURNSecret, STUNPort, TURNPort, TURNEnabled) Phase 6 — Admin Panel: - migrations/003_audit_log.sql: audit_log table with indexes - db/admin_queries: GetServerStats, ListAllUsers, UpdateUserRole, ForceLogoutUser, AdminCreate/Update/DeleteChannel, LogAudit, GetAuditLog, GetSetting, SetSetting, GetAllSettings, BackupTo - admin/api: full REST API — stats, users, channels, audit log, settings, backup; adminAuthMiddleware (ADMINISTRATOR bit), ownerOnlyMiddleware - admin/static/index.html: single-page admin panel (dark theme, vanilla JS, no CDN) — dashboard, users, channels, audit log, settings sections - admin/admin.go: NewHandler wiring go:embed static files + API Fixes: Channel struct json tags (was serializing as "ID" not "id"), duplicate getWithToken helper renamed in voice_handler_test.go Test coverage: admin 59.1%, api 78.2%, auth 90.9%, db 82.0%, ws 37.9%
122 lines
3.4 KiB
Go
122 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/owncord/server/config"
|
|
"github.com/owncord/server/db"
|
|
)
|
|
|
|
const voiceCredentialTTL = 24 * time.Hour
|
|
|
|
// iceServer describes a single ICE server entry for WebRTC peer connections.
|
|
type iceServer struct {
|
|
URLs string `json:"urls"`
|
|
Username string `json:"username,omitempty"`
|
|
Credential string `json:"credential,omitempty"`
|
|
}
|
|
|
|
// voiceCredentialsResponse is the JSON body for GET /api/v1/voice/credentials.
|
|
type voiceCredentialsResponse struct {
|
|
ICEServers []iceServer `json:"ice_servers"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
|
|
// turnCredentials holds the generated TURN username and HMAC credential.
|
|
type turnCredentials struct {
|
|
Username string
|
|
Credential string
|
|
}
|
|
|
|
// MountVoiceRoutes registers the voice REST endpoints on r.
|
|
func MountVoiceRoutes(r chi.Router, cfg *config.Config, database *db.DB) {
|
|
r.Route("/api/v1/voice", func(r chi.Router) {
|
|
r.Use(AuthMiddleware(database))
|
|
r.Get("/credentials", handleVoiceCredentials(cfg, database))
|
|
})
|
|
}
|
|
|
|
// handleVoiceCredentials returns ICE server credentials for WebRTC.
|
|
// Requires a valid session (AuthMiddleware). Generates time-limited TURN
|
|
// credentials using HMAC-SHA1 as per the coturn REST API spec.
|
|
func handleVoiceCredentials(cfg *config.Config, _ *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := r.Context().Value(UserKey).(*db.User)
|
|
if !ok || user == nil {
|
|
writeJSON(w, http.StatusUnauthorized, errorResponse{
|
|
Error: "UNAUTHORIZED",
|
|
Message: "authentication required",
|
|
})
|
|
return
|
|
}
|
|
|
|
host := serverHost(r)
|
|
servers := buildICEServers(user.ID, cfg, host)
|
|
|
|
writeJSON(w, http.StatusOK, voiceCredentialsResponse{
|
|
ICEServers: servers,
|
|
ExpiresIn: int(voiceCredentialTTL.Seconds()),
|
|
})
|
|
}
|
|
}
|
|
|
|
// buildICEServers constructs the ICE server list for the given user.
|
|
func buildICEServers(userID int64, cfg *config.Config, host string) []iceServer {
|
|
servers := []iceServer{
|
|
{URLs: fmt.Sprintf("stun:%s:%d", host, cfg.Voice.STUNPort)},
|
|
}
|
|
|
|
if cfg.Voice.TURNEnabled && cfg.Voice.TURNSecret != "" {
|
|
creds := generateTURNCredentials(userID, cfg.Voice.TURNSecret)
|
|
servers = append(servers, iceServer{
|
|
URLs: fmt.Sprintf("turn:%s:%d", host, cfg.Voice.TURNPort),
|
|
Username: creds.Username,
|
|
Credential: creds.Credential,
|
|
})
|
|
}
|
|
|
|
return servers
|
|
}
|
|
|
|
// generateTURNCredentials produces time-limited TURN credentials using HMAC-SHA1.
|
|
// Username format: "<expiry_unix_timestamp>:<userID>"
|
|
// Credential: base64(HMAC-SHA1(secret, username))
|
|
func generateTURNCredentials(userID int64, secret string) turnCredentials {
|
|
expiry := time.Now().Add(voiceCredentialTTL).Unix()
|
|
username := fmt.Sprintf("%d:%d", expiry, userID)
|
|
|
|
mac := hmac.New(sha1.New, []byte(secret))
|
|
mac.Write([]byte(username))
|
|
credential := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
|
|
return turnCredentials{
|
|
Username: username,
|
|
Credential: credential,
|
|
}
|
|
}
|
|
|
|
// serverHost extracts the host for ICE server URLs from the request or falls
|
|
// back to "localhost".
|
|
func serverHost(r *http.Request) string {
|
|
if host := r.Host; host != "" {
|
|
// Strip port if present.
|
|
for i := len(host) - 1; i >= 0; i-- {
|
|
if host[i] == ':' {
|
|
return host[:i]
|
|
}
|
|
if host[i] == ']' {
|
|
// IPv6 with no port.
|
|
return host
|
|
}
|
|
}
|
|
return host
|
|
}
|
|
return "localhost"
|
|
}
|