Files
Warp/server/internal/federationapi/server.go
T
tajniak81andClaude Opus 5.5 fc49f56339 Phase 7: E2EE support
Implements PLAN.md Phase 7: the server half of end-to-end encryption.
Devices upload identity, one-time and fallback keys; other devices query
and claim them, across servers too, and every one-time key is handed out
exactly once. Cross-signing, signature uploads, device list tracking and
server-side key backup complete the set.

internal/keyserver is the component. Keys are stored in canonical JSON
so that re-uploading the same key is a byte comparison, and signatures
uploaded later are kept apart from the key they sign and merged in on
read, so a key's own JSON stays what its owner uploaded. Device keys must
be for the caller's own device; cross-signing keys must be the
uploader's, carry the right usage and, for self- and user-signing keys,
be signed by the master key. Signatures are verified against the key as
stored. Replacing a master key asks for the password; a first upload or
an upload of the keys already stored does not, as the spec allows. A
signature of another user's master key is shown only to the user who
made it and never federated.

Every write that changes what /keys/query answers moves the device list
stream in the same transaction, one position per device, and wakes the
syncs of everyone sharing a room. The stream is the sixth field of a sync
token; older tokens still parse. The syncapi works out device_lists
changed and left from the stream and from membership moves in the
window, skipping invites and knocks, and reports
device_one_time_keys_count and device_unused_fallback_key_types, the
latter as [] when empty. GET /keys/changes uses the same logic.

Over federation: /user/keys/query, /user/keys/claim and
/user/devices/{userId}, and m.device_list_update and m.signing_key_update
both ways. Remote keys are not cached; an update about a remote user
sharing a room moves them in the stream so local clients re-query.
Device display names are not sent to other servers. With federation off,
remote users are reported in failures at once.

Deleting a device deletes its keys, through a new userapi
DeviceObserver. Key backup keeps the better copy of each session by the
spec's rule, never reuses a deleted version's number, and answers writes
to a stale version with M_WRONG_ROOM_KEYS_VERSION.

Storage is migration 0008 for sqlite and postgres, with a PocketBase
stub. The Complement target sets gain the key, device list, fallback,
cross-signing and backup tests.

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

682 lines
23 KiB
Go

package federationapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/dariuszjaniak/warp/server/internal/buildinfo"
"github.com/dariuszjaniak/warp/server/internal/federation"
"github.com/dariuszjaniak/warp/server/internal/httputil"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/mediaapi"
"github.com/dariuszjaniak/warp/server/internal/roomserver"
"github.com/dariuszjaniak/warp/server/internal/storage"
"github.com/dariuszjaniak/warp/server/internal/syncapi"
)
// maxBody caps a federation request body. A transaction is up to 50 PDUs
// of up to 64 KiB each, plus EDUs; anything past this is not a
// transaction.
const maxBody = 8 << 20
// Error codes the federation API uses beyond the Client-Server set.
const (
errCodeIncompatibleRoomVersion = "M_INCOMPATIBLE_ROOM_VERSION"
errCodeUnableToAuthoriseJoin = "M_UNABLE_TO_AUTHORISE_JOIN"
)
// EphemeralSink takes in EDUs from other servers. *syncapi.API satisfies
// it.
type EphemeralSink interface {
SetTyping(ctx context.Context, req syncapi.TypingRequest) error
SetReceipt(ctx context.Context, req syncapi.ReceiptRequest) error
SendToDevice(ctx context.Context, req syncapi.SendToDeviceRequest) error
}
// Profiles answers profile queries for local users. *userapi.API
// satisfies it.
type Profiles interface {
Account(ctx context.Context, localpart string) (storage.Account, error)
}
// ServerOptions configure the inbound API.
type ServerOptions struct {
ServerName string
Gate *federation.Gate
Keys *federation.KeyRing
Rooms *roomserver.API
Ephemeral EphemeralSink
Profiles Profiles
Store storage.FederationStore
// Media serves this server's media to others. Nil leaves the media
// endpoints unregistered.
Media *mediaapi.API
// UserKeys answers queries and claims of this server's users'
// end-to-end keys, and takes in key update EDUs. Nil leaves the key
// endpoints unregistered.
UserKeys KeyService
Logger *slog.Logger
Now func() time.Time
}
// Server is the inbound Server-Server API.
type Server struct {
serverName string
gate *federation.Gate
keys *federation.KeyRing
rooms *roomserver.API
ephemeral EphemeralSink
profiles Profiles
store storage.FederationStore
media *mediaapi.API
keyService KeyService
log *slog.Logger
now func() time.Time
}
// NewServer builds the inbound API.
func NewServer(opts ServerOptions) (*Server, error) {
switch {
case opts.ServerName == "":
return nil, errors.New("federationapi: ServerName must not be empty")
case opts.Gate == nil, opts.Keys == nil, opts.Rooms == nil, opts.Store == nil:
return nil, errors.New("federationapi: Gate, Keys, Rooms and Store are required")
}
if opts.Logger == nil {
opts.Logger = slog.Default()
}
if opts.Now == nil {
opts.Now = time.Now
}
return &Server{
serverName: opts.ServerName,
gate: opts.Gate,
keys: opts.Keys,
rooms: opts.Rooms,
ephemeral: opts.Ephemeral,
profiles: opts.Profiles,
store: opts.Store,
media: opts.Media,
keyService: opts.UserKeys,
log: opts.Logger,
now: opts.Now,
}, nil
}
// authedHandler is a federation handler that runs after the request has
// been authenticated, with the origin and the body it was signed over.
type authedHandler func(w http.ResponseWriter, r *http.Request, origin string, body []byte) error
// RegisterRoutes adds every federation endpoint, each behind the gate: with
// the switch off the whole surface answers M_UNRECOGNIZED, as a server
// with no federation API would.
func (s *Server) RegisterRoutes(r *httputil.Router) {
public := func(pattern string, h httputil.Handler) {
r.Handle(pattern, s.gate.ProtectFunc(h))
}
authed := func(pattern string, h authedHandler) {
r.Handle(pattern, s.gate.ProtectFunc(s.authenticated(h)))
}
// Keys and version need no authentication: a server has to be able to
// fetch our key before it can check anything we sign.
public("GET /_matrix/key/v2/server", s.handleServerKey)
public("GET /_matrix/key/v2/server/{keyID}", s.handleServerKey)
public("POST /_matrix/key/v2/query", s.handleKeyQuery)
public("GET /_matrix/key/v2/query/{serverName}", s.handleKeyQueryOne)
public("GET /_matrix/key/v2/query/{serverName}/{keyID}", s.handleKeyQueryOne)
public("GET /_matrix/federation/v1/version", s.handleVersion)
const v1, v2 = "/_matrix/federation/v1", "/_matrix/federation/v2"
authed("PUT "+v1+"/send/{txnID}", s.handleSend)
authed("GET "+v1+"/make_join/{roomID}/{userID}", s.handleMakeJoin)
authed("PUT "+v1+"/send_join/{roomID}/{eventID}", s.handleSendJoinV1)
authed("PUT "+v2+"/send_join/{roomID}/{eventID}", s.handleSendJoinV2)
authed("GET "+v1+"/make_leave/{roomID}/{userID}", s.handleMakeLeave)
authed("PUT "+v1+"/send_leave/{roomID}/{eventID}", s.handleSendLeaveV1)
authed("PUT "+v2+"/send_leave/{roomID}/{eventID}", s.handleSendLeaveV2)
authed("PUT "+v2+"/invite/{roomID}/{eventID}", s.handleInvite)
authed("GET "+v1+"/event/{eventID}", s.handleEvent)
authed("GET "+v1+"/state/{roomID}", s.handleState)
authed("GET "+v1+"/state_ids/{roomID}", s.handleStateIDs)
authed("GET "+v1+"/event_auth/{roomID}/{eventID}", s.handleEventAuth)
authed("GET "+v1+"/backfill/{roomID}", s.handleBackfill)
authed("POST "+v1+"/get_missing_events/{roomID}", s.handleMissingEvents)
authed("GET "+v1+"/query/directory", s.handleQueryDirectory)
authed("GET "+v1+"/query/profile", s.handleQueryProfile)
authed("GET "+v1+"/publicRooms", s.handlePublicRoomsGet)
authed("POST "+v1+"/publicRooms", s.handlePublicRoomsPost)
s.registerMediaRoutes(authed)
s.registerKeyRoutes(authed)
}
// authenticated checks X-Matrix and the allow/deny lists before the
// handler runs.
func (s *Server) authenticated(h authedHandler) httputil.Handler {
return func(w http.ResponseWriter, r *http.Request) error {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBody))
if err != nil {
return httputil.NewError(http.StatusRequestEntityTooLarge, httputil.ErrCodeTooLarge, "Request body too large")
}
origin, err := s.keys.AuthenticateRequest(r.Context(), r, body)
if err != nil {
httputil.LoggerFrom(r.Context()).Info("federation request failed authentication", "error", err)
return httputil.NewError(http.StatusUnauthorized, httputil.ErrCodeUnauthorized, "%s", err.Error())
}
if !s.gate.Policy().Allowed(origin) {
return httputil.Forbidden("Federation with %s is not allowed by this server", origin)
}
return h(w, r, origin, body)
}
}
// fedError maps a roomserver error onto the Matrix error a remote server
// is sent.
func fedError(err error) error {
var matrixErr *httputil.MatrixError
switch {
case err == nil:
return nil
case errors.As(err, &matrixErr):
return err
case errors.Is(err, roomserver.ErrIncompatibleRoomVersion):
return httputil.NewError(http.StatusBadRequest, errCodeIncompatibleRoomVersion, "%s", err.Error())
case errors.Is(err, roomserver.ErrNoSuchRoom), errors.Is(err, roomserver.ErrNoSuchEvent),
errors.Is(err, roomserver.ErrNoSuchAlias):
return httputil.NotFound("%s", err.Error())
case errors.Is(err, roomserver.ErrForbidden), errors.Is(err, roomserver.ErrRejected),
errors.Is(err, roomserver.ErrNotInRoom):
return httputil.Forbidden("%s", err.Error())
case errors.Is(err, roomserver.ErrBadEvent), errors.Is(err, roomserver.ErrInvalidContent):
return httputil.BadJSON("%s", err.Error())
case errors.Is(err, roomserver.ErrFederationDisabled):
return httputil.Unrecognized()
case errors.Is(err, roomserver.ErrRemote):
return httputil.NewError(http.StatusBadGateway, httputil.ErrCodeUnknown, "%s", err.Error())
}
return err
}
// decode reads an already-read body.
func decode(body []byte, v any) error {
if len(body) == 0 {
return httputil.NotJSON()
}
if err := json.Unmarshal(body, v); err != nil {
return httputil.BadJSON("Invalid request body: %v", err)
}
return nil
}
// --- keys and version -------------------------------------------------------
func (s *Server) handleServerKey(w http.ResponseWriter, _ *http.Request) error {
doc, err := s.keys.OwnKeyResponse()
if err != nil {
return err
}
httputil.WriteRawJSON(w, http.StatusOK, doc)
return nil
}
// keyQueryResponse is the notary answer.
type keyQueryResponse struct {
ServerKeys []json.RawMessage `json:"server_keys"`
}
func (s *Server) handleKeyQuery(w http.ResponseWriter, r *http.Request) error {
var req struct {
ServerKeys map[string]json.RawMessage `json:"server_keys"`
}
if err := httputil.DecodeJSON(w, r, &req); err != nil {
return err
}
resp := keyQueryResponse{ServerKeys: []json.RawMessage{}}
for serverName := range req.ServerKeys {
docs, err := s.keys.NotaryResponse(serverName)
if err != nil {
return err
}
resp.ServerKeys = append(resp.ServerKeys, docs...)
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
func (s *Server) handleKeyQueryOne(w http.ResponseWriter, r *http.Request) error {
docs, err := s.keys.NotaryResponse(r.PathValue("serverName"))
if err != nil {
return err
}
if docs == nil {
docs = []json.RawMessage{}
}
httputil.WriteJSON(w, http.StatusOK, keyQueryResponse{ServerKeys: docs})
return nil
}
func (s *Server) handleVersion(w http.ResponseWriter, _ *http.Request) error {
var resp federation.VersionResponse
resp.Server.Name = "Warp"
resp.Server.Version = buildinfo.Version
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// --- transactions -------------------------------------------------------------
func (s *Server) handleSend(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
txnID := r.PathValue("txnID")
if cached, ok, err := s.store.InboundTransaction(r.Context(), origin, txnID); err != nil {
return err
} else if ok {
httputil.WriteRawJSON(w, http.StatusOK, cached)
return nil
}
var txn federation.Transaction
if err := decode(body, &txn); err != nil {
return err
}
if txn.Origin != "" && txn.Origin != origin {
return httputil.Forbidden("The transaction's origin is %s, but it was signed by %s", txn.Origin, origin)
}
if len(txn.PDUs) > maxPDUsPerTxn || len(txn.EDUs) > maxEDUsPerTxn {
return httputil.BadJSON("A transaction carries at most %d PDUs and %d EDUs", maxPDUsPerTxn, maxEDUsPerTxn)
}
resp := federation.SendTransactionResponse{PDUs: map[string]federation.PDUResult{}}
for _, raw := range txn.PDUs {
eventID, err := s.rooms.ReceivePDU(r.Context(), origin, raw)
if eventID == "" {
// Unparseable: there is no ID to report a result under.
s.log.Info("dropping an unreadable PDU", "origin", origin, "error", err)
continue
}
result := federation.PDUResult{}
if err != nil {
result.Error = err.Error()
s.log.Info("refused a PDU", "origin", origin, "event_id", eventID, "error", err)
}
resp.PDUs[eventID] = result
}
for _, edu := range txn.EDUs {
s.receiveEDU(r.Context(), origin, edu)
}
encoded, err := json.Marshal(resp)
if err != nil {
return err
}
if err := s.store.RecordInboundTransaction(r.Context(), origin, txnID, encoded, s.now().UnixMilli()); err != nil {
s.log.Warn("could not record an inbound transaction", "origin", origin, "txn_id", txnID, "error", err)
}
httputil.WriteRawJSON(w, http.StatusOK, encoded)
return nil
}
// sameServer reports whether a user belongs to the server that sent the
// EDU about them. Nothing in an EDU is signed, so this is the only check
// there is, and without it any server could make anybody appear to type.
func sameServer(userID, origin string) bool {
server, err := id.ServerNameOf(userID)
return err == nil && server == origin
}
// receiveEDU takes one EDU in. Failures are logged and otherwise ignored:
// an EDU is ephemeral, and the transaction it came in succeeds whatever
// becomes of it.
func (s *Server) receiveEDU(ctx context.Context, origin string, edu federation.EDU) {
if s.ephemeral == nil {
return
}
switch edu.Type {
case federation.EDUTyping:
var c federation.TypingContent
if json.Unmarshal(edu.Content, &c) != nil || !sameServer(c.UserID, origin) || !s.allowed(ctx, c.RoomID, origin) {
return
}
if err := s.ephemeral.SetTyping(ctx, syncapi.TypingRequest{RoomID: c.RoomID, User: c.UserID, Typing: c.Typing}); err != nil {
s.log.Debug("ignored a typing EDU", "origin", origin, "room_id", c.RoomID, "error", err)
}
case federation.EDUReceipt:
var c federation.ReceiptContent
if json.Unmarshal(edu.Content, &c) != nil {
return
}
for roomID, byType := range c {
if !s.allowed(ctx, roomID, origin) {
continue
}
for receiptType, byUser := range byType {
if receiptType != syncapi.ReceiptRead {
continue
}
for userID, data := range byUser {
if !sameServer(userID, origin) || len(data.EventIDs) == 0 {
continue
}
if err := s.ephemeral.SetReceipt(ctx, syncapi.ReceiptRequest{
RoomID: roomID, User: userID, ReceiptType: receiptType,
EventID: data.EventIDs[len(data.EventIDs)-1], ThreadID: data.Data.ThreadID,
}); err != nil {
s.log.Debug("ignored a receipt EDU", "origin", origin, "room_id", roomID, "error", err)
}
}
}
}
case federation.EDUDirectToDev:
var c federation.DirectToDeviceContent
if json.Unmarshal(edu.Content, &c) != nil || !sameServer(c.Sender, origin) {
return
}
if err := s.ephemeral.SendToDevice(ctx, syncapi.SendToDeviceRequest{
Sender: c.Sender, EventType: c.Type, Messages: c.Messages,
}); err != nil {
s.log.Debug("ignored a to-device EDU", "origin", origin, "error", err)
}
case federation.EDUDeviceListUp, federation.EDUSigningKeys:
s.receiveKeyEDU(ctx, origin, edu)
default:
// Presence is off by default and not federated yet.
s.log.Debug("ignored an EDU", "origin", origin, "type", edu.Type)
}
}
// --- membership ---------------------------------------------------------------
func (s *Server) handleMakeJoin(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
version, tmpl, err := s.rooms.MakeJoin(r.Context(), origin, r.PathValue("roomID"), r.PathValue("userID"),
r.URL.Query()["ver"])
if err != nil {
if errors.Is(err, roomserver.ErrForbidden) && strings.Contains(err.Error(), "authorise a restricted join") {
return httputil.NewError(http.StatusBadRequest, errCodeUnableToAuthoriseJoin, "%s", err.Error())
}
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.MakeJoinResponse{RoomVersion: version, Event: tmpl})
return nil
}
func (s *Server) sendJoin(r *http.Request, origin string, body []byte) (federation.SendJoinResponse, error) {
res, err := s.rooms.SendJoin(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body)
if err != nil {
return federation.SendJoinResponse{}, fedError(err)
}
return federation.SendJoinResponse{
Origin: s.serverName,
State: res.State,
AuthChain: res.AuthChain,
Event: res.Event,
ServersInRoom: res.ServersInRoom,
}, nil
}
func (s *Server) handleSendJoinV2(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
resp, err := s.sendJoin(r, origin, body)
if err != nil {
return err
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// handleSendJoinV1 is the deprecated form, which wraps the same answer in
// a [200, body] pair.
func (s *Server) handleSendJoinV1(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
resp, err := s.sendJoin(r, origin, body)
if err != nil {
return err
}
httputil.WriteJSON(w, http.StatusOK, []any{http.StatusOK, resp})
return nil
}
func (s *Server) handleMakeLeave(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
version, tmpl, err := s.rooms.MakeLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("userID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.MakeJoinResponse{RoomVersion: version, Event: tmpl})
return nil
}
func (s *Server) handleSendLeaveV2(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
if err := s.rooms.SendLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body); err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, struct{}{})
return nil
}
func (s *Server) handleSendLeaveV1(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
if err := s.rooms.SendLeave(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"), body); err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, []any{http.StatusOK, struct{}{}})
return nil
}
func (s *Server) handleInvite(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
var req federation.InviteRequest
if err := decode(body, &req); err != nil {
return err
}
signed, err := s.rooms.ReceiveInvite(r.Context(), origin, req.RoomVersion, req.Event, req.InviteRoomState)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.InviteResponse{Event: signed})
return nil
}
// --- reads --------------------------------------------------------------------
func (s *Server) transaction(pdus []json.RawMessage) federation.Transaction {
if pdus == nil {
pdus = []json.RawMessage{}
}
return federation.Transaction{Origin: s.serverName, OriginServerTS: s.now().UnixMilli(), PDUs: pdus}
}
func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
raw, err := s.rooms.EventForServer(r.Context(), origin, r.PathValue("eventID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, s.transaction([]json.RawMessage{raw}))
return nil
}
func (s *Server) handleState(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
eventID := r.URL.Query().Get("event_id")
if eventID == "" {
return httputil.MissingParam("event_id is required")
}
state, chain, err := s.rooms.StateForServer(r.Context(), origin, r.PathValue("roomID"), eventID)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.StateResponse{PDUs: state, AuthChain: chain})
return nil
}
func (s *Server) handleStateIDs(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
eventID := r.URL.Query().Get("event_id")
if eventID == "" {
return httputil.MissingParam("event_id is required")
}
state, chain, err := s.rooms.StateIDsForServer(r.Context(), origin, r.PathValue("roomID"), eventID)
if err != nil {
return fedError(err)
}
if state == nil {
state = []string{}
}
if chain == nil {
chain = []string{}
}
httputil.WriteJSON(w, http.StatusOK, federation.StateIDsResponse{PDUIDs: state, AuthChainIDs: chain})
return nil
}
func (s *Server) handleEventAuth(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
chain, err := s.rooms.EventAuthForServer(r.Context(), origin, r.PathValue("roomID"), r.PathValue("eventID"))
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.EventAuthResponse{AuthChain: chain})
return nil
}
func (s *Server) handleBackfill(w http.ResponseWriter, r *http.Request, origin string, _ []byte) error {
query := r.URL.Query()
from := query["v"]
if len(from) == 0 {
return httputil.MissingParam("v is required")
}
limit, err := strconv.Atoi(query.Get("limit"))
if err != nil || limit <= 0 {
return httputil.InvalidParam("limit must be a positive integer")
}
events, err := s.rooms.BackfillForServer(r.Context(), origin, r.PathValue("roomID"), from, limit)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, s.transaction(events))
return nil
}
func (s *Server) handleMissingEvents(w http.ResponseWriter, r *http.Request, origin string, body []byte) error {
var req federation.MissingEventsRequest
if err := decode(body, &req); err != nil {
return err
}
events, err := s.rooms.MissingEventsForServer(r.Context(), origin, r.PathValue("roomID"),
req.EarliestEvents, req.LatestEvents, req.Limit, req.MinDepth)
if err != nil {
return fedError(err)
}
if events == nil {
events = []json.RawMessage{}
}
httputil.WriteJSON(w, http.StatusOK, federation.MissingEventsResponse{Events: events})
return nil
}
// --- queries ------------------------------------------------------------------
func (s *Server) handleQueryDirectory(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
alias := r.URL.Query().Get("room_alias")
if alias == "" {
return httputil.MissingParam("room_alias is required")
}
resolved, err := s.rooms.QueryAliasForServer(r.Context(), alias)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, federation.DirectoryResponse{RoomID: resolved.RoomID, Servers: resolved.Servers})
return nil
}
func (s *Server) handleQueryProfile(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
query := r.URL.Query()
userID := query.Get("user_id")
localpart, server, err := id.SplitUserID(userID)
if err != nil {
return httputil.InvalidParam("user_id is not a user ID")
}
if server != s.serverName || s.profiles == nil {
return httputil.NotFound("%s is not a user of this server", userID)
}
account, err := s.profiles.Account(r.Context(), localpart)
if errors.Is(err, storage.ErrNoSuchUser) {
return httputil.NotFound("Profile not found")
} else if err != nil {
return err
}
resp := map[string]string{}
switch field := query.Get("field"); field {
case "":
if account.DisplayName != "" {
resp["displayname"] = account.DisplayName
}
if account.AvatarURL != "" {
resp["avatar_url"] = account.AvatarURL
}
case "displayname":
if account.DisplayName != "" {
resp["displayname"] = account.DisplayName
}
case "avatar_url":
if account.AvatarURL != "" {
resp["avatar_url"] = account.AvatarURL
}
default:
return httputil.InvalidParam("field must be displayname or avatar_url")
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
func (s *Server) handlePublicRoomsGet(w http.ResponseWriter, r *http.Request, _ string, _ []byte) error {
query := r.URL.Query()
limit, _ := strconv.Atoi(query.Get("limit"))
return s.publicRooms(w, r, roomserver.PublicRoomsRequest{Limit: limit, Since: query.Get("since")})
}
func (s *Server) handlePublicRoomsPost(w http.ResponseWriter, r *http.Request, _ string, body []byte) error {
var req struct {
Limit int `json:"limit"`
Since string `json:"since"`
Filter struct {
GenericSearchTerm string `json:"generic_search_term"`
} `json:"filter"`
}
if len(body) > 0 {
if err := decode(body, &req); err != nil {
return err
}
}
return s.publicRooms(w, r, roomserver.PublicRoomsRequest{
Limit: req.Limit, Since: req.Since, Search: req.Filter.GenericSearchTerm,
})
}
func (s *Server) publicRooms(w http.ResponseWriter, r *http.Request, req roomserver.PublicRoomsRequest) error {
resp, err := s.rooms.PublicRooms(r.Context(), req)
if err != nil {
return fedError(err)
}
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// String is for logs.
func (s *Server) String() string { return fmt.Sprintf("federation API for %s", s.serverName) }
// allowed applies a room's server ACL to an EDU about it. An EDU is not
// a room event, but it is still a server speaking in the room, and a
// server the room has banned does not get to make its users appear to
// type.
func (s *Server) allowed(ctx context.Context, roomID, origin string) bool {
ok, err := s.rooms.ServerAllowed(ctx, roomID, origin)
return err == nil && ok
}