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
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
Every call in the web client now goes through MatrixRTC with a LiveKit
SFU, the model Element Call and Element X use; the legacy 1:1 m.call.*
calls are gone. In a room of two the other person is rung and can answer
or decline; in a bigger room a banner offers to join the call. Calls show
a grid of participant tiles with mute, camera, screen sharing and leave,
and media is end-to-end encrypted in encrypted rooms.
Server:
- MSC4140 delayed events (send/state with a delay, plus the
delayed_events list/restart/cancel/send API), stored and scheduled, so
a member whose client drops off is taken out of the call.
- POST /user/{userId}/openid/request_token, which lk-jwt-service uses to
hand out LiveKit tokens.
- org.matrix.msc4143.rtc_foci in /.well-known/matrix/client, configured
under client_api.rtc.
- Room presets let members send call membership state.
- Migration 0014 for both SQL dialects.
Deploy: LiveKit and lk-jwt-service in compose (calls.yml) and the helm
chart. Docs: docs/calls.md, configuration and deployment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
466 lines
19 KiB
Go
466 lines
19 KiB
Go
// Package clientapi implements the Client-Server API: the HTTP surface
|
|
// clients talk to.
|
|
//
|
|
// Phase 1 covers everything about identity — registration, login, sessions,
|
|
// devices, profiles, account data and capabilities. Rooms, messages and
|
|
// /sync arrive in Phases 3 and 4; the route table here is the place they
|
|
// will be added.
|
|
//
|
|
// Handlers are thin. They decode a request, map it onto a userapi call and
|
|
// shape the response; the rules live in userapi, and the storage in
|
|
// internal/storage. A handler that needs more than that is a sign something
|
|
// belongs further down.
|
|
package clientapi
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/appservice"
|
|
"github.com/dariuszjaniak/warp/server/internal/config"
|
|
"github.com/dariuszjaniak/warp/server/internal/delayedevents"
|
|
"github.com/dariuszjaniak/warp/server/internal/federation"
|
|
"github.com/dariuszjaniak/warp/server/internal/httputil"
|
|
"github.com/dariuszjaniak/warp/server/internal/keyserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/mediaapi"
|
|
"github.com/dariuszjaniak/warp/server/internal/pushserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/ratelimit"
|
|
"github.com/dariuszjaniak/warp/server/internal/roomserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/settings"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
"github.com/dariuszjaniak/warp/server/internal/syncapi"
|
|
"github.com/dariuszjaniak/warp/server/internal/userapi"
|
|
)
|
|
|
|
// ClientAPI serves the Client-Server API.
|
|
type ClientAPI struct {
|
|
serverName string
|
|
clientBaseURL string
|
|
users *userapi.API
|
|
rooms *roomserver.API
|
|
sync *syncapi.API
|
|
keys *keyserver.API
|
|
media *mediaapi.API
|
|
push *pushserver.API
|
|
legacyMedia bool
|
|
settings *settings.Service
|
|
federation *federation.Gate
|
|
remoteProfile RemoteProfiles
|
|
regTokens *userapi.RegistrationTokens
|
|
appservices *appservice.API
|
|
turn config.TURN
|
|
delayed *delayedevents.Service
|
|
rtcTransports []RTCTransport
|
|
openID storage.OpenIDTokenStore
|
|
log *slog.Logger
|
|
now func() time.Time
|
|
|
|
uia *uiaStore
|
|
clientIP ratelimit.KeyFunc
|
|
|
|
// One limiter per endpoint class, so a client hammering /login cannot
|
|
// spend the budget of /profile.
|
|
limitDefault *ratelimit.Limiter
|
|
limitLogin *ratelimit.Limiter
|
|
limitRegister *ratelimit.Limiter
|
|
limitRoomCreate *ratelimit.Limiter
|
|
limitJoin *ratelimit.Limiter
|
|
|
|
// limitSync is deliberately generous. A client is in /sync all the
|
|
// time, and a long poll that is refused leaves it reconnecting in a
|
|
// loop — the one place where rate limiting makes the load worse rather
|
|
// than better.
|
|
limitSync *ratelimit.Limiter
|
|
|
|
// limitMedia covers downloads and thumbnails, which a client asks for
|
|
// in bursts: every avatar in a room list at once.
|
|
limitMedia *ratelimit.Limiter
|
|
}
|
|
|
|
// Options configure the component.
|
|
type Options struct {
|
|
// ServerName is the Matrix server name.
|
|
ServerName string
|
|
|
|
// ClientBaseURL is the URL advertised to clients, the same value
|
|
// /.well-known/matrix/client carries. A successful login repeats it so
|
|
// that a client which only knew the server name does not have to fetch
|
|
// the discovery document separately.
|
|
ClientBaseURL string
|
|
|
|
// Users is the userapi component.
|
|
Users *userapi.API
|
|
|
|
// Rooms is the roomserver component. Without it the room endpoints are
|
|
// not registered at all, which is what the Phase 1 and Phase 2 tests
|
|
// relied on and what keeps a half-wired server from offering a
|
|
// createRoom it cannot honour.
|
|
Rooms *roomserver.API
|
|
|
|
// Sync is the syncapi component. Like the roomserver it is optional,
|
|
// and for the same reason: without it /sync, /messages, /context and
|
|
// the ephemeral endpoints answer M_UNRECOGNIZED rather than
|
|
// pretending there is a stream to read.
|
|
Sync *syncapi.API
|
|
|
|
// Keys is the keyserver. Without it the end-to-end encryption
|
|
// endpoints answer M_UNRECOGNIZED, which is how a client learns it
|
|
// cannot encrypt here.
|
|
Keys *keyserver.API
|
|
|
|
// Media is the media repository. Without it the media endpoints
|
|
// answer M_UNRECOGNIZED.
|
|
Media *mediaapi.API
|
|
|
|
// Push is the push server. Without it the push rule, pusher and
|
|
// notification endpoints answer M_UNRECOGNIZED.
|
|
Push *pushserver.API
|
|
|
|
// LegacyMediaDownloads serves the deprecated unauthenticated download
|
|
// and thumbnail endpoints too.
|
|
LegacyMediaDownloads bool
|
|
|
|
// Settings is the runtime-settings service; the registration switch is
|
|
// read from it on every registration attempt.
|
|
Settings *settings.Service
|
|
|
|
// Federation gates the endpoints that would need a remote server.
|
|
Federation *federation.Gate
|
|
|
|
// RemoteProfiles asks another server for one of its users' profile.
|
|
// Nil means remote profiles cannot be looked up at all.
|
|
RemoteProfiles RemoteProfiles
|
|
|
|
// Registration is the file configuration of the registration flow.
|
|
Registration config.Registration
|
|
|
|
// RegistrationTokens answers the m.login.registration_token stage.
|
|
// Nil builds one from Registration's static tokens alone.
|
|
RegistrationTokens *userapi.RegistrationTokens
|
|
|
|
// AppServices is the application service component: who may act as
|
|
// whom, the exclusive namespaces, and the queries. Nil means no
|
|
// application services are registered.
|
|
AppServices *appservice.API
|
|
|
|
// RateLimit configures the per-endpoint buckets.
|
|
RateLimit config.RateLimit
|
|
|
|
// TURN is the relay handed to clients for calls.
|
|
TURN config.TURN
|
|
|
|
// RTC is the group call backend, listed at the MSC4143 transports
|
|
// endpoint.
|
|
RTC config.RTC
|
|
|
|
// DelayedEvents schedules MSC4140 delayed events. Nil, or a service
|
|
// with delayed events switched off, leaves the endpoints unregistered
|
|
// and refuses the delay parameter.
|
|
DelayedEvents *delayedevents.Service
|
|
|
|
// OpenIDTokens keeps the OpenID tokens a user hands third parties,
|
|
// such as the LiveKit JWT service of a group call. Nil leaves the
|
|
// OpenID endpoints unregistered.
|
|
OpenIDTokens storage.OpenIDTokenStore
|
|
|
|
// TrustXForwardedFor takes the client address from the proxy header.
|
|
TrustXForwardedFor bool
|
|
|
|
Logger *slog.Logger
|
|
|
|
// Now is the clock, so tests can expire sessions without waiting.
|
|
Now func() time.Time
|
|
}
|
|
|
|
// New builds the component.
|
|
func New(opts Options) (*ClientAPI, error) {
|
|
if opts.ServerName == "" {
|
|
return nil, errors.New("clientapi: ServerName must not be empty")
|
|
}
|
|
if opts.Users == nil {
|
|
return nil, errors.New("clientapi: Users must not be nil")
|
|
}
|
|
if opts.Settings == nil {
|
|
return nil, errors.New("clientapi: Settings must not be nil")
|
|
}
|
|
if opts.Federation == nil {
|
|
opts.Federation = federation.NewGate(nil, nil)
|
|
}
|
|
if opts.Logger == nil {
|
|
opts.Logger = slog.Default()
|
|
}
|
|
if opts.Now == nil {
|
|
opts.Now = time.Now
|
|
}
|
|
if opts.RegistrationTokens == nil {
|
|
now := opts.Now
|
|
opts.RegistrationTokens = userapi.NewRegistrationTokens(userapi.RegistrationTokensOptions{
|
|
Static: opts.Registration.Tokens,
|
|
Required: opts.Registration.RequireToken,
|
|
NowMS: func() int64 { return now().UnixMilli() },
|
|
})
|
|
}
|
|
if opts.AppServices == nil {
|
|
var err error
|
|
if opts.AppServices, err = appservice.New(appservice.Options{Logger: opts.Logger}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
c := &ClientAPI{
|
|
serverName: opts.ServerName,
|
|
clientBaseURL: opts.ClientBaseURL,
|
|
users: opts.Users,
|
|
rooms: opts.Rooms,
|
|
sync: opts.Sync,
|
|
keys: opts.Keys,
|
|
media: opts.Media,
|
|
push: opts.Push,
|
|
legacyMedia: opts.LegacyMediaDownloads,
|
|
settings: opts.Settings,
|
|
federation: opts.Federation,
|
|
remoteProfile: opts.RemoteProfiles,
|
|
regTokens: opts.RegistrationTokens,
|
|
appservices: opts.AppServices,
|
|
turn: opts.TURN,
|
|
delayed: opts.DelayedEvents,
|
|
rtcTransports: RTCTransports(opts.RTC.LiveKitServiceURL),
|
|
openID: opts.OpenIDTokens,
|
|
log: opts.Logger,
|
|
now: opts.Now,
|
|
uia: newUIAStore(uiaSessionTTL, opts.Now),
|
|
clientIP: ratelimit.ClientIP(opts.TrustXForwardedFor),
|
|
}
|
|
|
|
if opts.RateLimit.Enabled {
|
|
c.limitDefault = ratelimit.New(bucket(opts.RateLimit.Default))
|
|
c.limitLogin = ratelimit.New(bucket(opts.RateLimit.Login))
|
|
c.limitRegister = ratelimit.New(bucket(opts.RateLimit.Register))
|
|
// Creating a room writes a dozen events and joining one writes a
|
|
// membership event into a room everybody in it then sees, so both
|
|
// get their own, tighter bucket: the default one is sized for
|
|
// reads.
|
|
c.limitRoomCreate = ratelimit.New(bucket(opts.RateLimit.RoomCreate))
|
|
c.limitJoin = ratelimit.New(bucket(opts.RateLimit.Join))
|
|
c.limitSync = ratelimit.New(bucket(opts.RateLimit.Sync))
|
|
c.limitMedia = ratelimit.New(bucket(opts.RateLimit.Media))
|
|
} else {
|
|
c.limitDefault, c.limitLogin, c.limitRegister =
|
|
ratelimit.Disabled(), ratelimit.Disabled(), ratelimit.Disabled()
|
|
c.limitRoomCreate, c.limitJoin = ratelimit.Disabled(), ratelimit.Disabled()
|
|
c.limitSync, c.limitMedia = ratelimit.Disabled(), ratelimit.Disabled()
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func bucket(b config.RateLimitBucket) ratelimit.Policy {
|
|
return ratelimit.Policy{Burst: b.Burst, Interval: b.Interval}
|
|
}
|
|
|
|
// Client-Server API path prefixes. Warp serves the versioned paths only:
|
|
// the r0 prefix was deprecated in Matrix 1.4, and every client that still
|
|
// sends it also understands a 404 and falls back to version negotiation
|
|
// through /_matrix/client/versions.
|
|
const (
|
|
v3 = "/_matrix/client/v3"
|
|
v1 = "/_matrix/client/v1"
|
|
)
|
|
|
|
// RegisterRoutes adds the Client-Server endpoints to the router.
|
|
func (c *ClientAPI) RegisterRoutes(r *httputil.Router) {
|
|
// Registration. The token-validity endpoint is v1 because that is the
|
|
// version the spec introduced it at.
|
|
c.public(r, "POST "+v3+"/register", c.limitRegister, c.handleRegister)
|
|
c.public(r, "GET "+v3+"/register/available", c.limitRegister, c.handleRegisterAvailable)
|
|
c.public(r, "GET "+v1+"/register/m.login.registration_token/validity",
|
|
c.limitRegister, c.handleRegistrationTokenValidity)
|
|
|
|
// Sessions.
|
|
c.public(r, "GET "+v3+"/login", c.limitDefault, c.handleLoginFlows)
|
|
c.public(r, "POST "+v3+"/login", c.limitLogin, c.handleLogin)
|
|
c.public(r, "POST "+v3+"/refresh", c.limitLogin, c.handleRefresh)
|
|
c.authed(r, "POST "+v3+"/logout", c.limitDefault, c.handleLogout)
|
|
c.authed(r, "POST "+v3+"/logout/all", c.limitDefault, c.handleLogoutAll)
|
|
|
|
// Account.
|
|
c.authed(r, "GET "+v3+"/account/whoami", c.limitDefault, c.handleWhoami)
|
|
c.authed(r, "POST "+v3+"/account/password", c.limitLogin, c.handleChangePassword)
|
|
c.authed(r, "POST "+v3+"/account/deactivate", c.limitLogin, c.handleDeactivate)
|
|
|
|
// Application services. The ping is the service checking that this
|
|
// server can reach it, so it is authenticated with the service's own
|
|
// token.
|
|
c.authed(r, "POST "+v1+"/appservice/{appserviceID}/ping", c.limitDefault, c.handleAppServicePing)
|
|
c.authed(r, "GET "+v3+"/capabilities", c.limitDefault, c.handleCapabilities)
|
|
|
|
// Calls: the relay a client sets its peer connection up with.
|
|
c.authed(r, "GET "+v3+"/voip/turnServer", c.limitDefault, c.handleTurnServer)
|
|
c.registerOpenIDRoutes(r)
|
|
c.registerDelayedEventRoutes(r)
|
|
c.registerRTCRoutes(r)
|
|
|
|
// Devices.
|
|
c.authed(r, "GET "+v3+"/devices", c.limitDefault, c.handleGetDevices)
|
|
c.authed(r, "GET "+v3+"/devices/{deviceID}", c.limitDefault, c.handleGetDevice)
|
|
c.authed(r, "PUT "+v3+"/devices/{deviceID}", c.limitDefault, c.handlePutDevice)
|
|
c.authed(r, "DELETE "+v3+"/devices/{deviceID}", c.limitDefault, c.handleDeleteDevice)
|
|
c.authed(r, "POST "+v3+"/delete_devices", c.limitDefault, c.handleDeleteDevices)
|
|
|
|
// Profiles are world-readable: a client showing a room member's name
|
|
// has no token for that member's server, and the spec makes the GETs
|
|
// unauthenticated for exactly that reason.
|
|
c.public(r, "GET "+v3+"/profile/{userID}", c.limitDefault, c.handleGetProfile)
|
|
c.public(r, "GET "+v3+"/profile/{userID}/displayname", c.limitDefault, c.handleGetDisplayName)
|
|
c.public(r, "GET "+v3+"/profile/{userID}/avatar_url", c.limitDefault, c.handleGetAvatarURL)
|
|
c.authed(r, "PUT "+v3+"/profile/{userID}/displayname", c.limitDefault, c.handlePutDisplayName)
|
|
c.authed(r, "PUT "+v3+"/profile/{userID}/avatar_url", c.limitDefault, c.handlePutAvatarURL)
|
|
|
|
// Account data.
|
|
c.authed(r, "GET "+v3+"/user/{userID}/account_data/{dataType}",
|
|
c.limitDefault, c.handleGetAccountData)
|
|
c.authed(r, "PUT "+v3+"/user/{userID}/account_data/{dataType}",
|
|
c.limitDefault, c.handlePutAccountData)
|
|
c.authed(r, "GET "+v3+"/user/{userID}/rooms/{roomID}/account_data/{dataType}",
|
|
c.limitDefault, c.handleGetRoomAccountData)
|
|
c.authed(r, "PUT "+v3+"/user/{userID}/rooms/{roomID}/account_data/{dataType}",
|
|
c.limitDefault, c.handlePutRoomAccountData)
|
|
|
|
c.registerRoomRoutes(r)
|
|
c.registerSyncRoutes(r)
|
|
c.registerKeyRoutes(r)
|
|
c.registerMediaRoutes(r)
|
|
c.registerPushRoutes(r)
|
|
}
|
|
|
|
// registerSyncRoutes adds the endpoints the syncapi serves, if there is
|
|
// one.
|
|
//
|
|
// Like the room routes they are left unregistered rather than stubbed in a
|
|
// build without the component: a client that gets M_UNRECOGNIZED from
|
|
// /sync knows the server does not have it, where an empty success would
|
|
// have it believe nothing was happening.
|
|
func (c *ClientAPI) registerSyncRoutes(r *httputil.Router) {
|
|
if c.sync == nil {
|
|
return
|
|
}
|
|
|
|
c.authed(r, "GET "+v3+"/sync", c.limitSync, c.handleSync)
|
|
c.authed(r, "POST "+slidingSyncPath, c.limitSync, c.handleSlidingSync)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/messages", c.limitDefault, c.handleMessages)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/context/{eventID}", c.limitDefault, c.handleContext)
|
|
|
|
// Relations and threads, which the spec introduced at v1.
|
|
c.authed(r, "GET "+v1+"/rooms/{roomID}/relations/{eventID}", c.limitDefault, c.handleRelations)
|
|
c.authed(r, "GET "+v1+"/rooms/{roomID}/relations/{eventID}/{relType}", c.limitDefault, c.handleRelations)
|
|
c.authed(r, "GET "+v1+"/rooms/{roomID}/relations/{eventID}/{relType}/{eventType}",
|
|
c.limitDefault, c.handleRelations)
|
|
c.authed(r, "GET "+v1+"/rooms/{roomID}/threads", c.limitDefault, c.handleThreads)
|
|
|
|
c.authed(r, "POST "+v3+"/search", c.limitDefault, c.handleSearch)
|
|
|
|
// Filters.
|
|
c.authed(r, "POST "+v3+"/user/{userID}/filter", c.limitDefault, c.handlePostFilter)
|
|
c.authed(r, "GET "+v3+"/user/{userID}/filter/{filterID}", c.limitDefault, c.handleGetFilter)
|
|
|
|
// The ephemeral streams. Typing goes in the sync limiter's class
|
|
// rather than the default one: a client sends one per keystroke-burst,
|
|
// and it is the chattiest write in the API.
|
|
c.authed(r, "PUT "+v3+"/rooms/{roomID}/typing/{userID}", c.limitSync, c.handlePutTyping)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/receipt/{receiptType}/{eventID}",
|
|
c.limitSync, c.handlePostReceipt)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/read_markers", c.limitDefault, c.handlePostReadMarkers)
|
|
|
|
// To-device messaging. A client encrypting for a busy room sends one
|
|
// of these per device, so it shares the sync budget too.
|
|
c.authed(r, "PUT "+v3+"/sendToDevice/{eventType}/{txnID}", c.limitSync, c.handleSendToDevice)
|
|
|
|
// Presence.
|
|
c.authed(r, "GET "+v3+"/presence/{userID}/status", c.limitDefault, c.handleGetPresence)
|
|
c.authed(r, "PUT "+v3+"/presence/{userID}/status", c.limitSync, c.handlePutPresence)
|
|
}
|
|
|
|
// registerRoomRoutes adds the room endpoints, if there is a roomserver.
|
|
//
|
|
// A build without one answers them with M_UNRECOGNIZED, which is the right
|
|
// answer: a server that cannot store a room should say it has no createRoom
|
|
// rather than fail every call to it.
|
|
func (c *ClientAPI) registerRoomRoutes(r *httputil.Router) {
|
|
if c.rooms == nil {
|
|
return
|
|
}
|
|
|
|
c.authed(r, "POST "+v3+"/createRoom", c.limitRoomCreate, c.handleCreateRoom)
|
|
|
|
// Membership. The two join endpoints differ only in what they are
|
|
// addressed by: /join/{roomIdOrAlias} resolves an alias first.
|
|
c.authed(r, "POST "+v3+"/join/{roomIDOrAlias}", c.limitJoin, c.handleJoinRoom)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/join", c.limitJoin, c.handleJoinRoomByID)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/leave", c.limitDefault, c.handleLeaveRoom)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/invite", c.limitDefault, c.handleInvite)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/kick", c.limitDefault, c.handleKick)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/ban", c.limitDefault, c.handleBan)
|
|
c.authed(r, "POST "+v3+"/rooms/{roomID}/unban", c.limitDefault, c.handleUnban)
|
|
|
|
// Sending. The state endpoint is registered twice, because the empty
|
|
// state key is both valid and the common case and arrives in two
|
|
// spellings: ".../state/m.room.name" and ".../state/m.room.name/".
|
|
// The trailing-slash form uses a multi-segment wildcard, which is the
|
|
// only kind Go's router matches against an empty segment — and which
|
|
// also means a state key a client did not escape arrives whole rather
|
|
// than as a 404.
|
|
c.authed(r, "PUT "+v3+"/rooms/{roomID}/send/{eventType}/{txnID}",
|
|
c.limitDefault, c.handleSendEvent)
|
|
c.authed(r, "PUT "+v3+"/rooms/{roomID}/state/{eventType}",
|
|
c.limitDefault, c.handlePutState)
|
|
c.authed(r, "PUT "+v3+"/rooms/{roomID}/state/{eventType}/{stateKey...}",
|
|
c.limitDefault, c.handlePutState)
|
|
c.authed(r, "PUT "+v3+"/rooms/{roomID}/redact/{eventID}/{txnID}",
|
|
c.limitDefault, c.handleRedact)
|
|
|
|
// Reading.
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/event/{eventID}", c.limitDefault, c.handleGetEvent)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/state", c.limitDefault, c.handleGetState)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/state/{eventType}",
|
|
c.limitDefault, c.handleGetStateEvent)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/state/{eventType}/{stateKey...}",
|
|
c.limitDefault, c.handleGetStateEvent)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/members", c.limitDefault, c.handleGetMembers)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/joined_members",
|
|
c.limitDefault, c.handleGetJoinedMembers)
|
|
c.authed(r, "GET "+v3+"/joined_rooms", c.limitDefault, c.handleGetJoinedRooms)
|
|
|
|
// Spaces, and the summary of a room the user may not be in.
|
|
c.authed(r, "GET "+v1+"/rooms/{roomID}/hierarchy", c.limitDefault, c.handleHierarchy)
|
|
c.authed(r, "GET "+v1+"/room_summary/{roomIDOrAlias}", c.limitDefault, c.handleRoomSummary)
|
|
|
|
// The directory. Resolving an alias and reading the public room list
|
|
// need no token: a client handed a link has to be able to follow it
|
|
// before it decides whether to log in.
|
|
c.public(r, "GET "+v3+"/directory/room/{roomAlias}", c.limitDefault, c.handleGetAlias)
|
|
c.authed(r, "PUT "+v3+"/directory/room/{roomAlias}", c.limitDefault, c.handlePutAlias)
|
|
c.authed(r, "DELETE "+v3+"/directory/room/{roomAlias}", c.limitDefault, c.handleDeleteAlias)
|
|
c.authed(r, "GET "+v3+"/rooms/{roomID}/aliases", c.limitDefault, c.handleGetRoomAliases)
|
|
|
|
c.public(r, "GET "+v3+"/publicRooms", c.limitDefault, c.handleGetPublicRooms)
|
|
c.authed(r, "POST "+v3+"/publicRooms", c.limitDefault, c.handlePostPublicRooms)
|
|
c.public(r, "GET "+v3+"/directory/list/room/{roomID}",
|
|
c.limitDefault, c.handleGetRoomVisibility)
|
|
c.authed(r, "PUT "+v3+"/directory/list/room/{roomID}",
|
|
c.limitDefault, c.handlePutRoomVisibility)
|
|
}
|
|
|
|
// public registers an endpoint that needs no access token.
|
|
func (c *ClientAPI) public(r *httputil.Router, pattern string, limiter *ratelimit.Limiter, h httputil.Handler) {
|
|
r.Handle(pattern, limiter.MiddlewareUnless(c.clientIP, c.rateLimitExempt)(httputil.Wrap(h)))
|
|
}
|
|
|
|
// authed registers an endpoint that needs one.
|
|
//
|
|
// The rate limiter still keys on the client address rather than the user:
|
|
// it runs before the token is resolved, and a limiter that had to hit the
|
|
// database to find its own key would be a denial of service of its own.
|
|
func (c *ClientAPI) authed(r *httputil.Router, pattern string, limiter *ratelimit.Limiter, h AuthedHandler) {
|
|
r.Handle(pattern, limiter.MiddlewareUnless(c.clientIP, c.rateLimitExempt)(httputil.Wrap(c.withAuth(h))))
|
|
}
|