mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-06-02 10:29:45 +00:00
feat(jitsi): add Jitsi auth provider and engine
This commit is contained in:
94
internal/auth/jitsi/jitsi.go
Normal file
94
internal/auth/jitsi/jitsi.go
Normal 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{})
|
||||
}
|
||||
83
internal/auth/jitsi/jitsi_test.go
Normal file
83
internal/auth/jitsi/jitsi_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user