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
Implements PLAN.md Phase 10. Simplified Sliding Sync (MSC4186) is served at the unstable org.matrix.simplified_msc3575 endpoint and advertised in /versions. Lists are sorted by activity and filtered by DM, invite, encryption, room type, space and tag; room subscriptions, required_state with wildcards, $ME and $LAZY, and the to_device, e2ee, account_data, receipts and typing extensions are supported. Per-connection state is kept in memory and an unknown pos answers M_UNKNOWN_POS. Threads and relations: m.relates_to is indexed on roomserver_events (migration 0012), /relations (with recursion) and /threads are served, and every event a timeline read returns carries its m.thread, m.replace and m.reference summaries in unsigned.m.relations. Notifications carry a thread ID (migration 0013): threaded receipts clear only their thread, and sync reports unread_thread_notifications when the filter asks. Spaces: /hierarchy walks a space breadth first, asking other servers about rooms this one is not in through the new federation hierarchy endpoint; room_summary describes one room. /search runs on FTS5 (sqlite, pocketbase) or tsvector (postgres), bounded to what each user may see, with context, profiles, state, grouping and paging. The PocketBase driver is real: the sqlite schema and store on an embedded PocketBase app's database, with media in its filesystem, local or S3 (database.pocketbase.s3). It runs the full storage test suite. Ops: OpenTelemetry tracing over OTLP/HTTP (tracing.*), with server spans per route, component spans, and trace propagation on federation, push and application service calls. New metrics for persisted events, waiting syncs, sliding sync connections and the database pool, with a Grafana dashboard. deploy/ adds a Compose setup (PostgreSQL, Prometheus, Grafana, Jaeger) and a Helm chart; cmd/warp-loadtest loads a server and reports request and delivery latencies. The load tester found a long-standing bug, fixed here: on a server where no account data had been written, every incremental /sync re-sent the push rules and returned at once, and an all-zero token was taken for an initial sync, so long polls became busy loops. The PostgreSQL migrations and search dialect compile but were not run against a live PostgreSQL in this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
433 lines
18 KiB
Go
433 lines
18 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/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/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
|
|
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
|
|
|
|
// 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,
|
|
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)
|
|
|
|
// 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))))
|
|
}
|