telegabber/calls/signaling/xmppsig/extdisco_test.go
2026-05-25 06:27:20 -07:00

181 lines
5.8 KiB
Go

package xmppsig
import (
"context"
"fmt"
"testing"
"time"
"github.com/pion/webrtc/v4"
"gosrc.io/xmpp/stanza"
)
func TestExtractServices_ProsodyResponse(t *testing.T) {
// verbatim shape from prosody 0.12 mod_external_services
raw := []byte(`<iq xmlns="jabber:client" type="result" from="localhost" to="probe@localhost/abc" id="x">` +
`<services xmlns="urn:xmpp:extdisco:2">` +
`<service transport="udp" port="3478" host="172.18.0.1" type="stun"/>` +
`<service restricted="1" port="3478" transport="udp" username="1779481470" password="F5aXytaIwbEJWt6FJ66OqeEQtvw=" host="172.18.0.1" type="turn"/>` +
`</services>` +
`</iq>`)
got, err := extractServices(raw)
if err != nil {
t.Fatalf("extractServices: %v", err)
}
if len(got) != 2 {
t.Fatalf("want 2 services, got %d: %+v", len(got), got)
}
if got[0] != (Service{Type: "stun", Host: "172.18.0.1", Port: 3478, Transport: "udp"}) {
t.Errorf("STUN entry mismatch: %+v", got[0])
}
if got[1] != (Service{
Type: "turn", Host: "172.18.0.1", Port: 3478, Transport: "udp",
Username: "1779481470", Password: "F5aXytaIwbEJWt6FJ66OqeEQtvw=",
}) {
t.Errorf("TURN entry mismatch: %+v", got[1])
}
}
func TestToICEServers_StunAndTurn(t *testing.T) {
in := []Service{
{Type: "stun", Host: "stun.example.com", Port: 3478, Transport: "udp"},
{Type: "turn", Host: "turn.example.com", Port: 3478, Transport: "udp", Username: "u", Password: "p"},
{Type: "turns", Host: "turn.example.com", Port: 5349, Transport: "tcp", Username: "u", Password: "p"},
}
out := ToICEServers(in)
if len(out) != 3 {
t.Fatalf("want 3 servers, got %d", len(out))
}
if out[0].URLs[0] != "stun:stun.example.com:3478" {
t.Errorf("STUN URL mismatch: %q", out[0].URLs[0])
}
if out[1].URLs[0] != "turn:turn.example.com:3478?transport=udp" {
t.Errorf("TURN URL mismatch: %q", out[1].URLs[0])
}
if out[1].Username != "u" || out[1].Credential != "p" {
t.Errorf("TURN creds mismatch: user=%q cred=%v", out[1].Username, out[1].Credential)
}
if out[1].CredentialType != webrtc.ICECredentialTypePassword {
t.Errorf("TURN cred type: want Password, got %v", out[1].CredentialType)
}
if out[2].URLs[0] != "turns:turn.example.com:5349?transport=tcp" {
t.Errorf("TURNS URL mismatch: %q", out[2].URLs[0])
}
}
// pion rejects the whole PC on any creds-less turn URL, so a single
// misconfigured prosody entry must not poison every call
func TestToICEServers_SkipsBrokenAndUnknown(t *testing.T) {
in := []Service{
{Type: "stun", Host: "h", Port: 3478, Transport: "udp"},
{Type: "turn", Host: "h", Port: 3478, Transport: "udp", Username: "u", Password: "p"},
// turns with creds: kept
{Type: "turns", Host: "h", Port: 5349, Transport: "tcp", Username: "u", Password: "p"},
// turn with no creds: dropped
{Type: "turn", Host: "broken", Port: 3478, Transport: "udp"},
// turns with no creds (prosody algorithm-omission bug): dropped
{Type: "turns", Host: "broken", Port: 5349, Transport: "tcp"},
// turn with username but no password: dropped (defensive)
{Type: "turn", Host: "half", Port: 3478, Transport: "udp", Username: "u"},
// unknown type: dropped
{Type: "ftp", Host: "h", Port: 21, Transport: "tcp"},
}
out := ToICEServers(in)
if len(out) != 3 {
t.Fatalf("want 3 servers (stun + turn-with-creds + turns-with-creds), got %d: %+v", len(out), out)
}
for _, srv := range out {
for _, u := range srv.URLs {
if (containsScheme(u, "turn:") || containsScheme(u, "turns:")) && (srv.Username == "" || srv.Credential == nil || srv.Credential == "") {
t.Errorf("TURN entry kept without creds: %+v", srv)
}
}
}
}
func containsScheme(url, scheme string) bool {
return len(url) >= len(scheme) && url[:len(scheme)] == scheme
}
// minimal Sender for QueryServices E2E against a hand-crafted response
type fakeSender struct {
sent []stanza.Packet
reply func(*stanza.IQ) stanza.IQ
failSend bool
}
func (f *fakeSender) Send(p stanza.Packet) error {
if f.failSend {
return fmt.Errorf("simulated send failure")
}
f.sent = append(f.sent, p)
return nil
}
func (f *fakeSender) SendRaw(string) error { return nil }
func (f *fakeSender) SendIQ(_ context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
ch := make(chan stanza.IQ, 1)
if err := f.Send(iq); err != nil {
return nil, err
}
if f.reply != nil {
ch <- f.reply(iq)
}
return ch, nil
}
func TestQueryServices_ParsesReply(t *testing.T) {
fs := &fakeSender{
reply: func(req *stanza.IQ) stanza.IQ {
respPtr, _ := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: req.To,
To: req.From,
Id: req.Id,
})
respPtr.Payload = &servicesQuery{Services: []serviceElem{
{Type: "stun", Host: "1.2.3.4", Port: "3478", Transport: "udp"},
{Type: "turn", Host: "1.2.3.4", Port: "3478", Transport: "udp", Username: "u", Password: "p"},
}}
return *respPtr
},
}
got, err := QueryServices(fs, "tlgrm.example.com", "example.com", time.Second)
if err != nil {
t.Fatalf("QueryServices: %v", err)
}
if len(got) != 2 {
t.Fatalf("want 2, got %d: %+v", len(got), got)
}
if got[0].Type != "stun" || got[1].Type != "turn" {
t.Errorf("unexpected service types: %+v", got)
}
if got[1].Username != "u" || got[1].Password != "p" {
t.Errorf("TURN creds lost: %+v", got[1])
}
}
func TestQueryServices_Timeout(t *testing.T) {
// no reply hook -> never delivers, must hit context timeout
fs := &fakeSender{}
_, err := QueryServices(fs, "tlgrm.example.com", "example.com", 50*time.Millisecond)
if err == nil {
t.Error("expected timeout error")
}
}
func TestQueryServices_ErrorIQ(t *testing.T) {
fs := &fakeSender{
reply: func(req *stanza.IQ) stanza.IQ {
respPtr, _ := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeError,
From: req.To,
To: req.From,
Id: req.Id,
})
return *respPtr
},
}
if _, err := QueryServices(fs, "tlgrm.example.com", "example.com", time.Second); err == nil {
t.Error("expected error on <iq type='error'>")
}
}