Files
Warp/server/internal/setup/federation.go
T
tajniak81andClaude Opus 5.5 74f7bc8805
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
keyserver: cache remote users' device and cross-signing keys
POST /keys/query for a remote user who shares a room with a local user is
now answered from a cache, fetched whole with GET /user/devices/{userId}
on a miss. Incoming m.device_list_update EDUs are applied when their
prev_id matches the cached stream_id; anything else marks the copy stale
and it is fetched again in the background, and a stale copy is never
served. m.signing_key_update replaces the cached cross-signing keys. A
user's copy is dropped once no local user shares a room with them, by a
keyserver listener on the roomserver's emit stage.

Migration 0009 adds keyserver_remote_users, keyserver_remote_device_keys
and keyserver_remote_cross_signing_keys for sqlite and postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:36:08 +02:00

101 lines
3.0 KiB
Go

package setup
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"sync"
"github.com/dariuszjaniak/warp/server/internal/federationapi"
"github.com/dariuszjaniak/warp/server/internal/keyserver"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/roomserver"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// fanout is the roomserver's emit stage when more than one thing listens:
// the sync notifier, which wakes long polls, and the federation queue,
// which sends events to other servers.
//
// Each listener is held to the roomserver's rule for a Notifier — never
// block — so calling them in turn under the room lock is fine.
type fanout struct {
mu sync.RWMutex
listeners []roomserver.Notifier
}
func (f *fanout) add(n roomserver.Notifier) {
f.mu.Lock()
defer f.mu.Unlock()
f.listeners = append(f.listeners, n)
}
// EventPersisted implements roomserver.Notifier.
func (f *fanout) EventPersisted(ctx context.Context, n roomserver.Notification) {
f.mu.RLock()
defer f.mu.RUnlock()
for _, l := range f.listeners {
l.EventPersisted(ctx, n)
}
}
// keyMemberships is the keyserver's listener on the emit stage: a
// membership ending is when a remote user may stop sharing a room with
// anybody here, and the keyserver drops its cached copy of their keys once
// they have. MembershipLeft does not block, as the emit stage requires.
type keyMemberships struct{ keys *keyserver.API }
// EventPersisted implements roomserver.Notifier.
func (l keyMemberships) EventPersisted(_ context.Context, n roomserver.Notification) {
if n.Event == nil || n.Event.Type() != event.TypeMember || !n.StateChanged {
return
}
userID, ok := n.Event.StateKey()
if !ok {
return
}
switch membership, _ := n.Event.Membership(); membership {
case event.MembershipLeave, event.MembershipBan:
l.keys.MembershipLeft(userID)
}
}
// federationStatus adapts the module to the admin API.
type federationStatus struct {
module *federationapi.Module
}
func (s federationStatus) Running() bool { return s.module.Running() }
func (s federationStatus) Destinations(ctx context.Context) ([]storage.DestinationSummary, error) {
return s.module.Queue().Destinations(ctx)
}
func (s federationStatus) ResetBackoff(ctx context.Context, serverName string) error {
return s.module.Queue().ResetBackoff(ctx, serverName)
}
// federationTLS builds the client TLS configuration for outbound
// federation: the system roots, plus a configured CA bundle.
func federationTLS(caFile string) (*tls.Config, error) {
cfg := &tls.Config{MinVersion: tls.VersionTLS12}
if caFile == "" {
return cfg, nil
}
pool, err := x509.SystemCertPool()
if err != nil || pool == nil {
pool = x509.NewCertPool()
}
pem, err := os.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("read federation.tls_ca_file: %w", err)
}
if !pool.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("federation.tls_ca_file %s contains no PEM certificates", caFile)
}
cfg.RootCAs = pool
return cfg, nil
}