mautrix-go/federation/client.go
timedout 132bfbcef4
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
federation/client: Add key query and claim endpoints too
2026-02-22 19:40:13 +00:00

634 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2024 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package federation
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"go.mau.fi/util/exslices"
"go.mau.fi/util/jsontime"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/federation/signutil"
"maunium.net/go/mautrix/id"
)
type Client struct {
HTTP *http.Client
ServerName string
UserAgent string
Key *SigningKey
ResponseSizeLimit int64
}
func NewClient(serverName string, key *SigningKey, cache ResolutionCache) *Client {
return &Client{
HTTP: &http.Client{
Transport: NewServerResolvingTransport(cache),
Timeout: 120 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Federation requests do not allow redirects.
return http.ErrUseLastResponse
},
},
UserAgent: mautrix.DefaultUserAgent,
ServerName: serverName,
Key: key,
ResponseSizeLimit: mautrix.DefaultResponseSizeLimit,
}
}
func (c *Client) Version(ctx context.Context, serverName string) (resp *RespServerVersion, err error) {
err = c.MakeRequest(ctx, serverName, false, http.MethodGet, URLPath{"v1", "version"}, nil, &resp)
return
}
func (c *Client) ServerKeys(ctx context.Context, serverName string) (resp *ServerKeyResponse, err error) {
err = c.MakeRequest(ctx, serverName, false, http.MethodGet, KeyURLPath{"v2", "server"}, nil, &resp)
return
}
func (c *Client) QueryKeys(ctx context.Context, serverName string, req *ReqQueryKeys) (resp *QueryKeysResponse, err error) {
err = c.MakeRequest(ctx, serverName, false, http.MethodPost, KeyURLPath{"v2", "query"}, req, &resp)
return
}
type PDU = json.RawMessage
type EDU = json.RawMessage
type ReqSendTransaction struct {
Destination string `json:"destination"`
TxnID string `json:"-"`
Origin string `json:"origin"`
OriginServerTS jsontime.UnixMilli `json:"origin_server_ts"`
PDUs []PDU `json:"pdus"`
EDUs []EDU `json:"edus,omitempty"`
}
type PDUProcessingResult struct {
Error string `json:"error,omitempty"`
}
type RespSendTransaction struct {
PDUs map[id.EventID]PDUProcessingResult `json:"pdus"`
}
func (c *Client) SendTransaction(ctx context.Context, req *ReqSendTransaction) (resp *RespSendTransaction, err error) {
err = c.MakeRequest(ctx, req.Destination, true, http.MethodPut, URLPath{"v1", "send", req.TxnID}, req, &resp)
return
}
type RespGetEventAuthChain struct {
AuthChain []PDU `json:"auth_chain"`
}
func (c *Client) GetEventAuthChain(ctx context.Context, serverName string, roomID id.RoomID, eventID id.EventID) (resp *RespGetEventAuthChain, err error) {
err = c.MakeRequest(ctx, serverName, true, http.MethodGet, URLPath{"v1", "event_auth", roomID, eventID}, nil, &resp)
return
}
type ReqBackfill struct {
ServerName string
RoomID id.RoomID
Limit int
BackfillFrom []id.EventID
}
type RespBackfill struct {
Origin string `json:"origin"`
OriginServerTS jsontime.UnixMilli `json:"origin_server_ts"`
PDUs []PDU `json:"pdus"`
}
func (c *Client) Backfill(ctx context.Context, req *ReqBackfill) (resp *RespBackfill, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.ServerName,
Method: http.MethodGet,
Path: URLPath{"v1", "backfill", req.RoomID},
Query: url.Values{
"limit": {strconv.Itoa(req.Limit)},
"v": exslices.CastToString[string](req.BackfillFrom),
},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
type ReqGetMissingEvents struct {
ServerName string `json:"-"`
RoomID id.RoomID `json:"-"`
EarliestEvents []id.EventID `json:"earliest_events"`
LatestEvents []id.EventID `json:"latest_events"`
Limit int `json:"limit,omitempty"`
MinDepth int `json:"min_depth,omitempty"`
}
type RespGetMissingEvents struct {
Events []PDU `json:"events"`
}
func (c *Client) GetMissingEvents(ctx context.Context, req *ReqGetMissingEvents) (resp *RespGetMissingEvents, err error) {
err = c.MakeRequest(ctx, req.ServerName, true, http.MethodPost, URLPath{"v1", "get_missing_events", req.RoomID}, req, &resp)
return
}
func (c *Client) GetEvent(ctx context.Context, serverName string, eventID id.EventID) (resp *RespBackfill, err error) {
err = c.MakeRequest(ctx, serverName, true, http.MethodGet, URLPath{"v1", "event", eventID}, nil, &resp)
return
}
type RespGetState struct {
AuthChain []PDU `json:"auth_chain"`
PDUs []PDU `json:"pdus"`
}
func (c *Client) GetState(ctx context.Context, serverName string, roomID id.RoomID, eventID id.EventID) (resp *RespGetState, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "state", roomID},
Query: url.Values{
"event_id": {string(eventID)},
},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
type RespGetStateIDs struct {
AuthChain []id.EventID `json:"auth_chain_ids"`
PDUs []id.EventID `json:"pdu_ids"`
}
func (c *Client) GetStateIDs(ctx context.Context, serverName string, roomID id.RoomID, eventID id.EventID) (resp *RespGetStateIDs, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "state_ids", roomID},
Query: url.Values{
"event_id": {string(eventID)},
},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
func (c *Client) TimestampToEvent(ctx context.Context, serverName string, roomID id.RoomID, timestamp time.Time, dir mautrix.Direction) (resp *mautrix.RespTimestampToEvent, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "timestamp_to_event", roomID},
Query: url.Values{
"dir": {string(dir)},
"ts": {strconv.FormatInt(timestamp.UnixMilli(), 10)},
},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
func (c *Client) QueryProfile(ctx context.Context, serverName string, userID id.UserID) (resp *mautrix.RespUserProfile, err error) {
err = c.Query(ctx, serverName, "profile", url.Values{"user_id": {userID.String()}}, &resp)
return
}
func (c *Client) QueryDirectory(ctx context.Context, serverName string, roomAlias id.RoomAlias) (resp *mautrix.RespAliasResolve, err error) {
err = c.Query(ctx, serverName, "directory", url.Values{"room_alias": {roomAlias.String()}}, &resp)
return
}
func (c *Client) Query(ctx context.Context, serverName, queryType string, queryParams url.Values, respStruct any) (err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "query", queryType},
Query: queryParams,
Authenticate: true,
ResponseJSON: respStruct,
})
return
}
func queryToValues(query map[string]string) url.Values {
values := make(url.Values, len(query))
for k, v := range query {
values[k] = []string{v}
}
return values
}
func (c *Client) PublicRooms(ctx context.Context, serverName string, req *mautrix.ReqPublicRooms) (resp *mautrix.RespPublicRooms, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "publicRooms"},
Query: queryToValues(req.Query()),
Authenticate: true,
ResponseJSON: &resp,
})
return
}
type RespOpenIDUserInfo struct {
Sub id.UserID `json:"sub"`
}
func (c *Client) GetOpenIDUserInfo(ctx context.Context, serverName, accessToken string) (resp *RespOpenIDUserInfo, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: http.MethodGet,
Path: URLPath{"v1", "openid", "userinfo"},
Query: url.Values{"access_token": {accessToken}},
ResponseJSON: &resp,
})
return
}
type ReqMakeJoin struct {
RoomID id.RoomID
UserID id.UserID
Via string
SupportedVersions []id.RoomVersion
}
type RespMakeJoin struct {
RoomVersion id.RoomVersion `json:"room_version"`
Event PDU `json:"event"`
}
type ReqSendJoin struct {
RoomID id.RoomID
EventID id.EventID
OmitMembers bool
Event PDU
Via string
}
type ReqSendKnock struct {
RoomID id.RoomID
EventID id.EventID
Event PDU
Via string
}
type RespSendJoin struct {
AuthChain []PDU `json:"auth_chain"`
Event PDU `json:"event"`
MembersOmitted bool `json:"members_omitted"`
ServersInRoom []string `json:"servers_in_room"`
State []PDU `json:"state"`
}
type RespSendKnock struct {
KnockRoomState []PDU `json:"knock_room_state"`
}
type ReqSendInvite struct {
RoomID id.RoomID `json:"-"`
UserID id.UserID `json:"-"`
Event PDU `json:"event"`
InviteRoomState []PDU `json:"invite_room_state"`
RoomVersion id.RoomVersion `json:"room_version"`
}
type RespSendInvite struct {
Event PDU `json:"event"`
}
type ReqMakeLeave struct {
RoomID id.RoomID
UserID id.UserID
Via string
}
type ReqSendLeave struct {
RoomID id.RoomID
EventID id.EventID
Event PDU
Via string
}
type (
ReqMakeKnock = ReqMakeJoin
RespMakeKnock = RespMakeJoin
RespMakeLeave = RespMakeJoin
)
func (c *Client) MakeJoin(ctx context.Context, req *ReqMakeJoin) (resp *RespMakeJoin, err error) {
versions := make([]string, len(req.SupportedVersions))
for i, v := range req.SupportedVersions {
versions[i] = string(v)
}
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodGet,
Path: URLPath{"v1", "make_join", req.RoomID, req.UserID},
Query: url.Values{"ver": versions},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
func (c *Client) MakeKnock(ctx context.Context, req *ReqMakeKnock) (resp *RespMakeKnock, err error) {
versions := make([]string, len(req.SupportedVersions))
for i, v := range req.SupportedVersions {
versions[i] = string(v)
}
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodGet,
Path: URLPath{"v1", "make_knock", req.RoomID, req.UserID},
Query: url.Values{"ver": versions},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
func (c *Client) SendJoin(ctx context.Context, req *ReqSendJoin) (resp *RespSendJoin, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodPut,
Path: URLPath{"v2", "send_join", req.RoomID, req.EventID},
Query: url.Values{
"omit_members": {strconv.FormatBool(req.OmitMembers)},
},
Authenticate: true,
RequestJSON: req.Event,
ResponseJSON: &resp,
})
return
}
func (c *Client) SendKnock(ctx context.Context, req *ReqSendKnock) (resp *RespSendKnock, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodPut,
Path: URLPath{"v1", "send_knock", req.RoomID, req.EventID},
Authenticate: true,
RequestJSON: req.Event,
ResponseJSON: &resp,
})
return
}
func (c *Client) SendInvite(ctx context.Context, req *ReqSendInvite) (resp *RespSendInvite, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.UserID.Homeserver(),
Method: http.MethodPut,
Path: URLPath{"v2", "invite", req.RoomID, req.UserID},
Authenticate: true,
RequestJSON: req,
ResponseJSON: &resp,
})
return
}
func (c *Client) MakeLeave(ctx context.Context, req *ReqMakeLeave) (resp *RespMakeLeave, err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodGet,
Path: URLPath{"v1", "make_leave", req.RoomID, req.UserID},
Authenticate: true,
ResponseJSON: &resp,
})
return
}
func (c *Client) SendLeave(ctx context.Context, req *ReqSendLeave) (err error) {
_, _, err = c.MakeFullRequest(ctx, RequestParams{
ServerName: req.Via,
Method: http.MethodPut,
Path: URLPath{"v2", "send_leave", req.RoomID, req.EventID},
Authenticate: true,
RequestJSON: req.Event,
})
return
}
type UserDevice struct {
DisplayName string `json:"device_display_name,omitempty"`
DeviceID id.DeviceID `json:"device_id"`
Keys mautrix.DeviceKeys `json:"keys"`
}
type RespUserDevices struct {
MasterKey *mautrix.CrossSigningKeys `json:"master_key,omitempty"`
SelfSigningKey *mautrix.CrossSigningKeys `json:"self_signing_key,omitempty"`
StreamID int64 `json:"stream_id,omitempty"`
UserID id.UserID
Devices []UserDevice `json:"devices"`
}
// GetUserDevices Gets information on all of the users devices
func (c *Client) GetUserDevices(ctx context.Context, serverName string, userID id.UserID) (resp *RespUserDevices, err error) {
return resp, c.MakeRequest(ctx, serverName, true, http.MethodGet, URLPath{"v1", "user", "devices", userID}, nil, &resp)
}
// QueryUserKeys Returns the current devices and identity keys for the given users.
func (c *Client) QueryUserKeys(ctx context.Context, serverName string, req *mautrix.ReqQueryKeys) (resp *mautrix.RespQueryKeys, err error) {
return resp, c.MakeRequest(ctx, serverName, true, http.MethodPost, URLPath{"v1", "user", "keys", "query"}, req, &resp)
}
// ClaimUserKeys Claims one-time keys for use in pre-key messages.
func (c *Client) ClaimUserKeys(ctx context.Context, serverName string, req *mautrix.ReqClaimKeys) (resp *mautrix.RespClaimKeys, err error) {
return resp, c.MakeRequest(ctx, serverName, true, http.MethodPost, URLPath{"v1", "user", "keys", "claim"}, req, &resp)
}
type URLPath []any
func (fup URLPath) FullPath() []any {
return append([]any{"_matrix", "federation"}, []any(fup)...)
}
type KeyURLPath []any
func (fkup KeyURLPath) FullPath() []any {
return append([]any{"_matrix", "key"}, []any(fkup)...)
}
type RequestParams struct {
ServerName string
Method string
Path mautrix.PrefixableURLPath
Query url.Values
Authenticate bool
RequestJSON any
ResponseJSON any
DontReadBody bool
}
func (c *Client) MakeRequest(ctx context.Context, serverName string, authenticate bool, method string, path mautrix.PrefixableURLPath, reqJSON, respJSON any) error {
_, _, err := c.MakeFullRequest(ctx, RequestParams{
ServerName: serverName,
Method: method,
Path: path,
Authenticate: authenticate,
RequestJSON: reqJSON,
ResponseJSON: respJSON,
})
return err
}
func (c *Client) MakeFullRequest(ctx context.Context, params RequestParams) ([]byte, *http.Response, error) {
req, err := c.compileRequest(ctx, params)
if err != nil {
return nil, nil, err
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, mautrix.HTTPError{
Request: req,
Response: resp,
Message: "request error",
WrappedError: err,
}
}
if !params.DontReadBody {
defer resp.Body.Close()
}
var body []byte
if resp.StatusCode >= 300 {
body, err = mautrix.ParseErrorResponse(req, resp)
return body, resp, err
} else if params.ResponseJSON != nil || !params.DontReadBody {
if resp.ContentLength > c.ResponseSizeLimit {
return body, resp, mautrix.HTTPError{
Request: req,
Response: resp,
Message: "not reading response",
WrappedError: fmt.Errorf("%w (%.2f MiB)", mautrix.ErrResponseTooLong, float64(resp.ContentLength)/1024/1024),
}
}
body, err = io.ReadAll(io.LimitReader(resp.Body, c.ResponseSizeLimit+1))
if err == nil && len(body) > int(c.ResponseSizeLimit) {
err = mautrix.ErrBodyReadReachedLimit
}
if err != nil {
return body, resp, mautrix.HTTPError{
Request: req,
Response: resp,
Message: "failed to read response body",
WrappedError: err,
}
}
if params.ResponseJSON != nil {
err = json.Unmarshal(body, params.ResponseJSON)
if err != nil {
return body, resp, mautrix.HTTPError{
Request: req,
Response: resp,
Message: "failed to unmarshal response JSON",
ResponseBody: string(body),
WrappedError: err,
}
}
}
}
return body, resp, nil
}
func (c *Client) compileRequest(ctx context.Context, params RequestParams) (*http.Request, error) {
reqURL := mautrix.BuildURL(&url.URL{
Scheme: "matrix-federation",
Host: params.ServerName,
}, params.Path.FullPath()...)
reqURL.RawQuery = params.Query.Encode()
var reqJSON json.RawMessage
var reqBody io.Reader
if params.RequestJSON != nil {
var err error
reqJSON, err = json.Marshal(params.RequestJSON)
if err != nil {
return nil, mautrix.HTTPError{
Message: "failed to marshal JSON",
WrappedError: err,
}
}
reqBody = bytes.NewReader(reqJSON)
}
req, err := http.NewRequestWithContext(ctx, params.Method, reqURL.String(), reqBody)
if err != nil {
return nil, mautrix.HTTPError{
Message: "failed to create request",
WrappedError: err,
}
}
req.Header.Set("User-Agent", c.UserAgent)
if params.Authenticate {
if c.ServerName == "" || c.Key == nil {
return nil, mautrix.HTTPError{
Message: "client not configured for authentication",
}
}
auth, err := (&signableRequest{
Method: req.Method,
URI: reqURL.RequestURI(),
Origin: c.ServerName,
Destination: params.ServerName,
Content: reqJSON,
}).Sign(c.Key)
if err != nil {
return nil, mautrix.HTTPError{
Message: "failed to sign request",
WrappedError: err,
}
}
req.Header.Set("Authorization", auth)
}
return req, nil
}
type signableRequest struct {
Method string `json:"method"`
URI string `json:"uri"`
Origin string `json:"origin"`
Destination string `json:"destination"`
Content json.RawMessage `json:"content,omitempty"`
}
func (r *signableRequest) Verify(key id.SigningKey, sig string) error {
message, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
return signutil.VerifyJSONRaw(key, sig, message)
}
func (r *signableRequest) Sign(key *SigningKey) (string, error) {
sig, err := key.SignJSON(r)
if err != nil {
return "", err
}
return XMatrixAuth{
Origin: r.Origin,
Destination: r.Destination,
KeyID: key.ID,
Signature: sig,
}.String(), nil
}