mirror of
https://github.com/strukturag/nextcloud-spreed-signaling.git
synced 2025-03-14 11:32:46 +00:00
415 lines
8.5 KiB
Go
415 lines
8.5 KiB
Go
/**
|
|
* Standalone signaling server for the Nextcloud Spreed app.
|
|
* Copyright (C) 2023 struktur AG
|
|
*
|
|
* @author Joachim Bauch <bauch@struktur.de>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package signaling
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockDnsLookup struct {
|
|
sync.RWMutex
|
|
|
|
ips map[string][]net.IP
|
|
}
|
|
|
|
func newMockDnsLookupForTest(t *testing.T) *mockDnsLookup {
|
|
mock := &mockDnsLookup{
|
|
ips: make(map[string][]net.IP),
|
|
}
|
|
prev := lookupDnsMonitorIP
|
|
t.Cleanup(func() {
|
|
lookupDnsMonitorIP = prev
|
|
})
|
|
lookupDnsMonitorIP = mock.lookup
|
|
return mock
|
|
}
|
|
|
|
func (m *mockDnsLookup) Set(host string, ips []net.IP) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
m.ips[host] = ips
|
|
}
|
|
|
|
func (m *mockDnsLookup) Get(host string) []net.IP {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
return m.ips[host]
|
|
}
|
|
|
|
func (m *mockDnsLookup) lookup(host string) ([]net.IP, error) {
|
|
m.RLock()
|
|
defer m.RUnlock()
|
|
|
|
ips, found := m.ips[host]
|
|
if !found {
|
|
return nil, &net.DNSError{
|
|
Err: fmt.Sprintf("could not resolve %s", host),
|
|
Name: host,
|
|
IsNotFound: true,
|
|
}
|
|
}
|
|
|
|
return append([]net.IP{}, ips...), nil
|
|
}
|
|
|
|
func newDnsMonitorForTest(t *testing.T, interval time.Duration) *DnsMonitor {
|
|
t.Helper()
|
|
require := require.New(t)
|
|
|
|
monitor, err := NewDnsMonitor(interval)
|
|
require.NoError(err)
|
|
|
|
t.Cleanup(func() {
|
|
monitor.Stop()
|
|
})
|
|
|
|
require.NoError(monitor.Start())
|
|
return monitor
|
|
}
|
|
|
|
type dnsMonitorReceiverRecord struct {
|
|
all []net.IP
|
|
add []net.IP
|
|
keep []net.IP
|
|
remove []net.IP
|
|
}
|
|
|
|
func (r *dnsMonitorReceiverRecord) Equal(other *dnsMonitorReceiverRecord) bool {
|
|
return r == other || (reflect.DeepEqual(r.add, other.add) &&
|
|
reflect.DeepEqual(r.keep, other.keep) &&
|
|
reflect.DeepEqual(r.remove, other.remove))
|
|
}
|
|
|
|
func (r *dnsMonitorReceiverRecord) String() string {
|
|
return fmt.Sprintf("all=%v, add=%v, keep=%v, remove=%v", r.all, r.add, r.keep, r.remove)
|
|
}
|
|
|
|
var (
|
|
expectNone = &dnsMonitorReceiverRecord{}
|
|
)
|
|
|
|
type dnsMonitorReceiver struct {
|
|
sync.Mutex
|
|
|
|
t *testing.T
|
|
expected *dnsMonitorReceiverRecord
|
|
received *dnsMonitorReceiverRecord
|
|
}
|
|
|
|
func newDnsMonitorReceiverForTest(t *testing.T) *dnsMonitorReceiver {
|
|
return &dnsMonitorReceiver{
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, remove []net.IP) {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
received := &dnsMonitorReceiverRecord{
|
|
all: all,
|
|
add: add,
|
|
keep: keep,
|
|
remove: remove,
|
|
}
|
|
|
|
expected := r.expected
|
|
r.expected = nil
|
|
if expected == expectNone {
|
|
assert.Fail(r.t, "expected no event, got %v", received)
|
|
return
|
|
}
|
|
|
|
if expected == nil {
|
|
if r.received != nil && !r.received.Equal(received) {
|
|
assert.Fail(r.t, "already received %v, got %v", r.received, received)
|
|
}
|
|
return
|
|
}
|
|
|
|
assert.True(r.t, expected.Equal(received), "expected %v, got %v", expected, received)
|
|
r.received = nil
|
|
r.expected = nil
|
|
}
|
|
|
|
func (r *dnsMonitorReceiver) WaitForExpected(ctx context.Context) {
|
|
r.t.Helper()
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
ticker := time.NewTicker(time.Microsecond)
|
|
abort := false
|
|
for r.expected != nil && !abort {
|
|
r.Unlock()
|
|
select {
|
|
case <-ticker.C:
|
|
case <-ctx.Done():
|
|
assert.NoError(r.t, ctx.Err())
|
|
abort = true
|
|
}
|
|
r.Lock()
|
|
}
|
|
}
|
|
|
|
func (r *dnsMonitorReceiver) Expect(all, add, keep, remove []net.IP) {
|
|
r.t.Helper()
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
if r.expected != nil && r.expected != expectNone {
|
|
assert.Fail(r.t, "didn't get previously expected %v", r.expected)
|
|
}
|
|
|
|
expected := &dnsMonitorReceiverRecord{
|
|
all: all,
|
|
add: add,
|
|
keep: keep,
|
|
remove: remove,
|
|
}
|
|
if r.received != nil && r.received.Equal(expected) {
|
|
r.received = nil
|
|
return
|
|
}
|
|
|
|
r.expected = expected
|
|
}
|
|
|
|
func (r *dnsMonitorReceiver) ExpectNone() {
|
|
r.t.Helper()
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
if r.expected != nil && r.expected != expectNone {
|
|
assert.Fail(r.t, "didn't get previously expected %v", r.expected)
|
|
}
|
|
|
|
r.expected = expectNone
|
|
}
|
|
|
|
func TestDnsMonitor(t *testing.T) {
|
|
lookup := newMockDnsLookupForTest(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
interval := time.Millisecond
|
|
monitor := newDnsMonitorForTest(t, interval)
|
|
|
|
ip1 := net.ParseIP("192.168.0.1")
|
|
ip2 := net.ParseIP("192.168.1.1")
|
|
ip3 := net.ParseIP("10.1.2.3")
|
|
ips1 := []net.IP{
|
|
ip1,
|
|
ip2,
|
|
}
|
|
lookup.Set("foo", ips1)
|
|
|
|
rec1 := newDnsMonitorReceiverForTest(t)
|
|
rec1.Expect(ips1, ips1, nil, nil)
|
|
|
|
entry1, err := monitor.Add("https://foo:12345", rec1.OnLookup)
|
|
require.NoError(t, err)
|
|
defer monitor.Remove(entry1)
|
|
|
|
rec1.WaitForExpected(ctx)
|
|
|
|
ips2 := []net.IP{
|
|
ip1,
|
|
ip2,
|
|
ip3,
|
|
}
|
|
add2 := []net.IP{ip3}
|
|
keep2 := []net.IP{ip1, ip2}
|
|
rec1.Expect(ips2, add2, keep2, nil)
|
|
lookup.Set("foo", ips2)
|
|
rec1.WaitForExpected(ctx)
|
|
|
|
ips3 := []net.IP{
|
|
ip2,
|
|
ip3,
|
|
}
|
|
keep3 := []net.IP{ip2, ip3}
|
|
remove3 := []net.IP{ip1}
|
|
rec1.Expect(ips3, nil, keep3, remove3)
|
|
lookup.Set("foo", ips3)
|
|
rec1.WaitForExpected(ctx)
|
|
|
|
rec1.ExpectNone()
|
|
time.Sleep(5 * interval)
|
|
|
|
remove4 := []net.IP{ip2, ip3}
|
|
rec1.Expect(nil, nil, nil, remove4)
|
|
lookup.Set("foo", nil)
|
|
rec1.WaitForExpected(ctx)
|
|
|
|
rec1.ExpectNone()
|
|
time.Sleep(5 * interval)
|
|
|
|
// Removing multiple times is supported.
|
|
monitor.Remove(entry1)
|
|
monitor.Remove(entry1)
|
|
|
|
// No more events after removing.
|
|
lookup.Set("foo", ips1)
|
|
rec1.ExpectNone()
|
|
time.Sleep(5 * interval)
|
|
}
|
|
|
|
func TestDnsMonitorIP(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
interval := time.Millisecond
|
|
monitor := newDnsMonitorForTest(t, interval)
|
|
|
|
ip := "192.168.0.1"
|
|
ips := []net.IP{
|
|
net.ParseIP(ip),
|
|
}
|
|
|
|
rec1 := newDnsMonitorReceiverForTest(t)
|
|
rec1.Expect(ips, ips, nil, nil)
|
|
|
|
entry, err := monitor.Add(ip+":12345", rec1.OnLookup)
|
|
require.NoError(t, err)
|
|
defer monitor.Remove(entry)
|
|
|
|
rec1.WaitForExpected(ctx)
|
|
|
|
rec1.ExpectNone()
|
|
time.Sleep(5 * interval)
|
|
}
|
|
|
|
func TestDnsMonitorNoLookupIfEmpty(t *testing.T) {
|
|
interval := time.Millisecond
|
|
monitor := newDnsMonitorForTest(t, interval)
|
|
|
|
var checked atomic.Bool
|
|
monitor.checkHostnames = func() {
|
|
checked.Store(true)
|
|
monitor.doCheckHostnames()
|
|
}
|
|
|
|
time.Sleep(10 * interval)
|
|
assert.False(t, checked.Load(), "should not have checked hostnames")
|
|
}
|
|
|
|
type deadlockMonitorReceiver struct {
|
|
t *testing.T
|
|
monitor *DnsMonitor
|
|
|
|
mu sync.RWMutex
|
|
wg sync.WaitGroup
|
|
|
|
entry *DnsMonitorEntry
|
|
started chan struct{}
|
|
triggered bool
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func newDeadlockMonitorReceiver(t *testing.T, monitor *DnsMonitor) *deadlockMonitorReceiver {
|
|
return &deadlockMonitorReceiver{
|
|
t: t,
|
|
monitor: monitor,
|
|
started: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (r *deadlockMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
|
if !assert.False(r.t, r.closed.Load(), "received lookup after closed") {
|
|
return
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if r.triggered {
|
|
return
|
|
}
|
|
|
|
r.triggered = true
|
|
r.wg.Add(1)
|
|
go func() {
|
|
defer r.wg.Done()
|
|
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
close(r.started)
|
|
time.Sleep(50 * time.Millisecond)
|
|
}()
|
|
}
|
|
|
|
func (r *deadlockMonitorReceiver) Start() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
entry, err := r.monitor.Add("foo", r.OnLookup)
|
|
if !assert.NoError(r.t, err) {
|
|
return
|
|
}
|
|
|
|
r.entry = entry
|
|
}
|
|
|
|
func (r *deadlockMonitorReceiver) Close() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if r.entry != nil {
|
|
r.monitor.Remove(r.entry)
|
|
r.closed.Store(true)
|
|
}
|
|
r.wg.Wait()
|
|
}
|
|
|
|
func TestDnsMonitorDeadlock(t *testing.T) {
|
|
lookup := newMockDnsLookupForTest(t)
|
|
ip1 := net.ParseIP("192.168.0.1")
|
|
ip2 := net.ParseIP("192.168.0.2")
|
|
lookup.Set("foo", []net.IP{ip1})
|
|
|
|
interval := time.Millisecond
|
|
monitor := newDnsMonitorForTest(t, interval)
|
|
|
|
r := newDeadlockMonitorReceiver(t, monitor)
|
|
r.Start()
|
|
<-r.started
|
|
lookup.Set("foo", []net.IP{ip2})
|
|
r.Close()
|
|
lookup.Set("foo", []net.IP{ip1})
|
|
time.Sleep(10 * interval)
|
|
monitor.mu.Lock()
|
|
defer monitor.mu.Unlock()
|
|
assert.Empty(t, monitor.hostnames)
|
|
}
|