feat(jitsi): add Jitsi auth provider and engine

This commit is contained in:
zarazaex69
2026-05-15 15:37:58 +03:00
parent af87120f73
commit eceeaeba92
11 changed files with 1003 additions and 13 deletions

View File

@@ -0,0 +1,94 @@
// Package jitsi implements a pass-through auth provider for self-hosted Jitsi
// Meet instances.
//
// Public Jitsi Meet servers do not require authentication for guest access;
// the only "credentials" the engine needs are the host+room pair extracted
// from a user-supplied room URL. This provider does no HTTP at all — it just
// parses the URL and forwards host+room to the engine via auth.Credentials.
//
// Supported RoomURL forms:
//
// - "https://meet.example.com/myroom"
// - "http://meet.example.com/myroom"
// - "meet.example.com/myroom"
//
// Optional URL path prefixes (e.g. "/jitsi") are preserved as part of the
// host when present, so deployments behind a path-mounted reverse proxy work
// transparently — the j library accepts any host string the WebSocket dial
// can resolve.
package jitsi
import (
"context"
"errors"
"fmt"
"strings"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
// CredentialKeyRoom is the auth.Credentials.Extra key that carries the Jitsi
// room name (the conference identifier on the host).
const CredentialKeyRoom = "room"
// ErrInvalidRoomURL is returned when the supplied RoomURL cannot be parsed
// into a host+room pair.
var ErrInvalidRoomURL = errors.New("jitsi: invalid room URL (expected host/room or https://host/room)")
// Provider produces engine credentials for a Jitsi Meet room.
type Provider struct{}
// Engine reports which engine consumes credentials from this auth provider.
func (Provider) Engine() string { return "jitsi" }
// DefaultServiceURL returns the empty string: there is no canonical default
// Jitsi instance — every deployment is user-supplied.
func (Provider) DefaultServiceURL() string { return "" }
// Issue parses cfg.RoomURL into host+room and returns engine credentials.
//
// The URL field of the returned Credentials carries the Jitsi host (e.g.
// "meet.example.com"); the room name lives in Extra under CredentialKeyRoom.
// Token is unused — Jitsi guest access requires no token.
func (Provider) Issue(_ context.Context, cfg auth.Config) (auth.Credentials, error) {
host, room, err := parseRoomURL(cfg.RoomURL)
if err != nil {
return auth.Credentials{}, err
}
return auth.Credentials{
URL: host,
Token: "",
Extra: map[string]string{CredentialKeyRoom: room},
}, nil
}
// parseRoomURL splits a Jitsi room URL into (host, room).
//
// Accepts URLs with or without scheme. The host part is the segment before
// the first "/" after stripping the scheme; the room is everything that
// follows, with leading/trailing slashes trimmed.
func parseRoomURL(raw string) (host string, room string, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", "", auth.ErrRoomIDRequired
}
if idx := strings.Index(raw, "://"); idx >= 0 {
raw = raw[idx+3:]
}
raw = strings.TrimPrefix(raw, "//")
raw = strings.TrimPrefix(raw, "/")
slash := strings.Index(raw, "/")
if slash <= 0 {
return "", "", fmt.Errorf("%w: %q", ErrInvalidRoomURL, raw)
}
host = strings.TrimSpace(raw[:slash])
room = strings.Trim(raw[slash+1:], "/")
if host == "" || room == "" {
return "", "", fmt.Errorf("%w: %q", ErrInvalidRoomURL, raw)
}
return host, room, nil
}
func init() { //nolint:gochecknoinits // auth registration is the canonical Go pattern for plugins
auth.Register("jitsi", Provider{})
}

View File

@@ -0,0 +1,83 @@
package jitsi
import (
"context"
"errors"
"testing"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
func TestParseRoomURL(t *testing.T) {
tests := []struct {
name string
raw string
host string
room string
wantErr bool
}{
{name: "https url", raw: "https://meet.cryptopro.ru/myroom", host: "meet.cryptopro.ru", room: "myroom"},
{name: "http url", raw: "http://meet.example/myroom", host: "meet.example", room: "myroom"},
{name: "scheme-less", raw: "meet.example.com/myroom", host: "meet.example.com", room: "myroom"},
{name: "trailing slash", raw: "https://meet.example/myroom/", host: "meet.example", room: "myroom"},
{name: "double slash leader", raw: "//meet.example/myroom", host: "meet.example", room: "myroom"},
{name: "uppercase room", raw: "https://meet.example/MyRoom", host: "meet.example", room: "MyRoom"},
{name: "empty", raw: "", wantErr: true},
{name: "host only", raw: "meet.example.com", wantErr: true},
{name: "no room", raw: "https://meet.example/", wantErr: true},
{name: "scheme only", raw: "https://", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
host, room, err := parseRoomURL(tc.raw)
if tc.wantErr {
if err == nil {
t.Fatalf("parseRoomURL(%q) = (%q, %q), want error", tc.raw, host, room)
}
return
}
if err != nil {
t.Fatalf("parseRoomURL(%q) error = %v, want nil", tc.raw, err)
}
if host != tc.host || room != tc.room {
t.Fatalf("parseRoomURL(%q) = (%q, %q), want (%q, %q)",
tc.raw, host, room, tc.host, tc.room)
}
})
}
}
func TestProviderIssue(t *testing.T) {
creds, err := Provider{}.Issue(context.Background(), auth.Config{
RoomURL: "https://meet.cryptopro.ru/olcrtc",
Name: "olcrtc-test",
})
if err != nil {
t.Fatalf("Issue: %v", err)
}
if creds.URL != "meet.cryptopro.ru" {
t.Fatalf("URL = %q, want %q", creds.URL, "meet.cryptopro.ru")
}
if got := creds.Extra[CredentialKeyRoom]; got != "olcrtc" {
t.Fatalf("room = %q, want %q", got, "olcrtc")
}
if creds.Token != "" {
t.Fatalf("Token = %q, want empty", creds.Token)
}
}
func TestProviderIssueRequiresRoom(t *testing.T) {
_, err := Provider{}.Issue(context.Background(), auth.Config{RoomURL: ""})
if !errors.Is(err, auth.ErrRoomIDRequired) {
t.Fatalf("Issue() err = %v, want ErrRoomIDRequired", err)
}
}
func TestProviderEngine(t *testing.T) {
if got := (Provider{}).Engine(); got != "jitsi" {
t.Fatalf("Engine() = %q, want %q", got, "jitsi")
}
if got := (Provider{}).DefaultServiceURL(); got != "" {
t.Fatalf("DefaultServiceURL() = %q, want empty", got)
}
}