Skip to content

Commit

Permalink
Add support for v3 Client-Server API
Browse files Browse the repository at this point in the history
Related to:

- https://matrix.org/blog/2021/11/09/matrix-v-1-1-release
- matrix-org/synapse#11318
- spantaleev/matrix-docker-ansible-deploy#1404

The upcoming Synapse v1.48.0 release is likely to expose all these `r0`
APIs that we've used till now as `v3` APIs. Both the `r0` and `v3`
prefixes lead to the same APIs on the homeserver.

matrix-corporal 2.1.5 already properly handles rejecting unknown
v-prefixed versions (`v3` included), which patched a potential future
security vulnerability (when Synapse v1.48.0 ultimately gets released).

This patch adds to it and lets `v3` requests go through and get handled
the same way `r0` requests are handled.
  • Loading branch information
spantaleev committed Nov 19, 2021
1 parent 8af4278 commit 9ede33a
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 38 deletions.
4 changes: 3 additions & 1 deletion corporal/httpgateway/handler/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ func (me *loginHandler) RegisterRoutesWithRouter(router *mux.Router) {
// As of this moment (2021-11-15), Synapse (v1.46) does not like trailing-slash login requests.
// Still, we handle both trailing and non-trailing to be on the safe side.

// Requests for an `apiVersion` that we don't support (and don't match below) are rejected via a `denyUnsupportedApiVersionsMiddleware` middleware.

router.Handle(
"/_matrix/client/r0/login{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/login{optionalTrailingSlash:[/]?}`,
me.createInterceptorHandler("login", me.loginInterceptor),
).Methods("POST")
}
Expand Down
30 changes: 16 additions & 14 deletions corporal/httpgateway/handler/policy_checked_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ func (me *policyCheckedRoutesHandler) RegisterRoutesWithRouter(router *mux.Route
//
// It's important to us that policy-checked routes are matched (with and without a slash),
// so we can guarantee policy checking happens and that we potentially reject requests that need to be rejected.
// Without this, a request for `/_matrix/client/r0/rooms/{roomId}/state/m.room.encryption/` (note the trailing slash)
// does not match our `/_matrix/client/r0/rooms/{roomId}/state/m.room.encryption` policy-checked handler,
// Without this, a request for `/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/state/m.room.encryption/` (note the trailing slash)
// does not match our `/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/state/m.room.encryption` policy-checked handler,
// slips through and gets happily served by the homserver.
//
// Alternative solutions are:
// 1. Using the mux Router's `StripSlash(true)` setting (but it does weird 301 redirects for POST/PUT requests, so it's not effective)
// 2. Applying a router middleware that modifies the request (stripping trailing slashes) before matching happens.
// See: https://natedenlinger.com/dealing-with-trailing-slashes-on-requesturi-in-go-with-mux/
// The 2nd solution doesn't work as well, because some APIs (`GET /_matrix/client/r0/pushrules/`) require a trailing slash.
// The 2nd solution doesn't work as well, because some APIs (`GET /_matrix/client/{apiVersion:(?:r0|v\d+)}/pushrules/`) require a trailing slash.
// Removing the trailing slash on our side and forwarding the request to the homeserver results in `{"errcode":"M_UNRECOGNIZED","error":"Unrecognized request"}`.
//
// Instead of trying to whitelist routes that require a slash and potentially missing something,
Expand All @@ -64,56 +64,58 @@ func (me *policyCheckedRoutesHandler) RegisterRoutesWithRouter(router *mux.Route
// Most of the APIs below will not even be served by the homeserver, as a trailing slash is not tolerated at the homeserver level.
// Still, it's safer if we policy-check them all and not have to worry future homeserver versions handling things differently.

// Requests for an `apiVersion` that we don't support (and don't match below) are rejected via a `denyUnsupportedApiVersionsMiddleware` middleware.

router.HandleFunc(
"/_matrix/client/r0/groups/{communityId}/self/leave{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/groups/{communityId}/self/leave{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("community.self.leave", policycheck.CheckCommunitySelfLeave, false),
).Methods("PUT")

router.HandleFunc(
"/_matrix/client/r0/rooms/{roomId}/leave{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/leave{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.leave", policycheck.CheckRoomLeave, false),
).Methods("POST")

// Another way to leave a room is kick yourself out of it. It doesn't require any special permissions.
router.HandleFunc(
"/_matrix/client/r0/rooms/{roomId}/kick{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/kick{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.kick", policycheck.CheckRoomKick, false),
).Methods("POST")

// Another way to leave a room is to PUT a "membership=leave" into your m.room.member state.
router.HandleFunc(
"/_matrix/client/r0/rooms/{roomId}/state/m.room.member/{memberId}{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/state/m.room.member/{memberId}{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.member.state.set", policycheck.CheckRoomMembershipStateChange, false),
).Methods("PUT")

// Another way to make a room encrypted is by enabling encryption subsequently.
router.HandleFunc(
"/_matrix/client/r0/rooms/{roomId}/state/m.room.encryption{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/state/m.room.encryption{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.subsequenly_enabling_encryption", policycheck.CheckRoomEncryptionStateChange, false),
).Methods("PUT")

router.HandleFunc(
"/_matrix/client/r0/createRoom{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/createRoom{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.create", policycheck.CheckRoomCreate, false),
).Methods("POST")

router.HandleFunc(
"/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/rooms/{roomId}/send/{eventType}/{txnId}{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("room.send_event", policycheck.CheckRoomSendEvent, false),
).Methods("PUT")

router.HandleFunc(
"/_matrix/client/r0/profile/{targetUserId}/displayname{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/profile/{targetUserId}/displayname{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("user.set_display_name", policycheck.CheckProfileSetDisplayName, false),
).Methods("PUT")

router.HandleFunc(
"/_matrix/client/r0/profile/{targetUserId}/avatar_url{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/profile/{targetUserId}/avatar_url{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("user.set_avatar", policycheck.CheckProfileSetAvatarUrl, false),
).Methods("PUT")

router.HandleFunc(
"/_matrix/client/r0/account/deactivate{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/account/deactivate{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("user.deactivate", policycheck.CheckUserDeactivate, false),
).Methods("POST")

Expand All @@ -123,7 +125,7 @@ func (me *policyCheckedRoutesHandler) RegisterRoutesWithRouter(router *mux.Route
//
// We don't want to break the 2nd (access-token-less) flow in some cases (depending on the policy).
router.HandleFunc(
"/_matrix/client/r0/account/password{optionalTrailingSlash:[/]?}",
`/_matrix/client/{apiVersion:(?:r0|v\d+)}/account/password{optionalTrailingSlash:[/]?}`,
me.createPolicyCheckingHandler("user.password", policycheck.CheckUserSetPassword, true),
).Methods("POST")
}
Expand Down
3 changes: 2 additions & 1 deletion corporal/httpgateway/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func init() {

supportedApiVersions = []string{
"r0",
"v3",
}
}

Expand All @@ -33,7 +34,7 @@ func denyUnsupportedApiVersionsMiddleware(next http.Handler) http.Handler {
return
}

releaseVersion := matches[1] // Something like `r0`
releaseVersion := matches[1] // Something like `r0` or `v3`, etc.

if util.IsStringInArray(releaseVersion, supportedApiVersions) {
// We do support this version and can safely let our gateway
Expand Down
2 changes: 1 addition & 1 deletion corporal/httpgateway/policycheck/community.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/gorilla/mux"
)

// CheckCommunitySelfLeave is a policy checker for: /_matrix/client/r0/groups/{communityId}/self/leave
// CheckCommunitySelfLeave is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/groups/{communityId}/self/leave
func CheckCommunitySelfLeave(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
communityId := mux.Vars(r)["communityId"]
Expand Down
4 changes: 2 additions & 2 deletions corporal/httpgateway/policycheck/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/gorilla/mux"
)

// CheckProfileSetDisplayName is a policy checker for: /_matrix/client/r0/profile/{targetUserId}/displayname
// CheckProfileSetDisplayName is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/profile/{targetUserId}/displayname
func CheckProfileSetDisplayName(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
targetUserId := mux.Vars(r)["targetUserId"]
Expand Down Expand Up @@ -63,7 +63,7 @@ func CheckProfileSetDisplayName(r *http.Request, ctx context.Context, policy pol
}
}

// CheckProfileSetAvatarUrl is a policy checker for: /_matrix/client/r0/profile/{targetUserId}/avatar_url
// CheckProfileSetAvatarUrl is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/profile/{targetUserId}/avatar_url
func CheckProfileSetAvatarUrl(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
targetUserId := mux.Vars(r)["targetUserId"]
Expand Down
12 changes: 6 additions & 6 deletions corporal/httpgateway/policycheck/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/matrix-org/gomatrix"
)

// CheckRoomCreate is a policy checker for: /_matrix/client/r0/createRoom
// CheckRoomCreate is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/createRoom
func CheckRoomCreate(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)

Expand Down Expand Up @@ -64,7 +64,7 @@ func CheckRoomCreate(r *http.Request, ctx context.Context, policy policy.Policy,
}
}

// CheckRoomEncryptionStateChange is a policy checker for: /_matrix/client/r0/rooms/{roomId}/state/m.room.encryption
// CheckRoomEncryptionStateChange is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/rooms/{roomId}/state/m.room.encryption
func CheckRoomEncryptionStateChange(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)

Expand All @@ -81,7 +81,7 @@ func CheckRoomEncryptionStateChange(r *http.Request, ctx context.Context, policy
}
}

// CheckRoomSendEvent is a policy checker for: /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}
// CheckRoomSendEvent is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/rooms/{roomId}/send/{eventType}/{txnId}
func CheckRoomSendEvent(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
eventType := mux.Vars(r)["eventType"]
Expand All @@ -100,7 +100,7 @@ func CheckRoomSendEvent(r *http.Request, ctx context.Context, policy policy.Poli
}
}

// CheckRoomLeave is a policy checker for: /_matrix/client/r0/rooms/{roomId}/leave
// CheckRoomLeave is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/rooms/{roomId}/leave
func CheckRoomLeave(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
roomId := mux.Vars(r)["roomId"]
Expand All @@ -118,7 +118,7 @@ func CheckRoomLeave(r *http.Request, ctx context.Context, policy policy.Policy,
}
}

// CheckRoomMembershipStateChange is a policy checker for: /_matrix/client/r0/rooms/{roomId}/state/m.room.member/{memberId}
// CheckRoomMembershipStateChange is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/rooms/{roomId}/state/m.room.member/{memberId}
func CheckRoomMembershipStateChange(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
roomId := mux.Vars(r)["roomId"]
Expand Down Expand Up @@ -151,7 +151,7 @@ func CheckRoomMembershipStateChange(r *http.Request, ctx context.Context, policy
}
}

// CheckRoomKick is a policy checker for: /_matrix/client/r0/rooms/{roomId}/kick
// CheckRoomKick is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/rooms/{roomId}/kick
func CheckRoomKick(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)
roomId := mux.Vars(r)["roomId"]
Expand Down
4 changes: 2 additions & 2 deletions corporal/httpgateway/policycheck/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"net/http"
)

// CheckUserDeactivate is a policy checker for: /_matrix/client/r0/account/deactivate
// CheckUserDeactivate is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/account/deactivate
func CheckUserDeactivate(r *http.Request, ctx context.Context, policy policy.Policy, checker policy.Checker) PolicyCheckResponse {
userId := ctx.Value("userId").(string)

Expand All @@ -28,7 +28,7 @@ func CheckUserDeactivate(r *http.Request, ctx context.Context, policy policy.Pol
}
}

// CheckUserSetPassword is a policy checker for: /_matrix/client/r0/account/password
// CheckUserSetPassword is a policy checker for: /_matrix/client/{apiVersion:(r0|v3)}/account/password
func CheckUserSetPassword(r *http.Request, ctx context.Context, policyObj policy.Policy, checker policy.Checker) PolicyCheckResponse {
userIdOrNil := ctx.Value("userId")
userId, ok := userIdOrNil.(string)
Expand Down
2 changes: 1 addition & 1 deletion corporal/matrix/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
LoginTypePassword = "m.login.password"
LoginTypeToken = "m.login.token"

// See https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
// See https://spec.matrix.org/v1.1/client-server-api/#identifier-types
LoginIdentifierTypeUser = "m.id.user"
LoginIdentifierTypeThirdParty = "m.id.thirdparty"
LoginIdentifierTypePhone = "m.id.phone"
Expand Down
20 changes: 10 additions & 10 deletions corporal/matrix/payloads.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package matrix

// ApiLoginRequestPayload represents is a request payload for: POST /_matrix/client/r0/login
// ApiLoginRequestPayload represents is a request payload for: POST /_matrix/client/{apiVersion:(r0|v3)}/login
type ApiLoginRequestPayload struct {
// Type is matrix.LoginTypeToken or something else
Type string `json:"type"`
Expand Down Expand Up @@ -51,33 +51,33 @@ type ApiAdminEntityUser struct {
AvatarURL string `json:"avatar_url"`
}

// ApiWhoAmIResponse is a response as found at: GET /_matrix/client/r0/account/whoami
// ApiWhoAmIResponse is a response as found at: GET /_matrix/client/{apiVersion:(r0|v3)}/account/whoami
type ApiWhoAmIResponse struct {
UserId string `json:"user_id"`
}

// ApiUserProfileResponse is a response as found at: GET /_matrix/client/r0/profile/{userId}
// ApiUserProfileResponse is a response as found at: GET /_matrix/client/{apiVersion:(r0|v3)}/profile/{userId}
type ApiUserProfileResponse struct {
AvatarUrl string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}

// ApiUserProfileDisplayNameRequestPayload is a request payload for: POST /_matrix/client/r0/profile/{userId}/displayname
// ApiUserProfileDisplayNameRequestPayload is a request payload for: POST /_matrix/client/{apiVersion:(r0|v3)}/profile/{userId}/displayname
type ApiUserProfileDisplayNameRequestPayload struct {
DisplayName string `json:"displayname"`
}

// ApiJoinedGroupsResponse is a response as found at: GET /_matrix/client/r0/joined_groups
// ApiJoinedGroupsResponse is a response as found at: GET /_matrix/client/{apiVersion:(r0|v3)}/joined_groups
type ApiJoinedGroupsResponse struct {
GroupIds []string `json:"groups"`
}

// ApiAdminRegisterNonceResponse is a response as found at: GET /_matrix/client/r0/admin/register
// ApiAdminRegisterNonceResponse is a response as found at: GET /_matrix/client/{apiVersion:(r0|v3)}/admin/register
type ApiUserAccountRegisterNonceResponse struct {
Nonce string `json:"nonce"`
}

// ApiUserAccountRegisterRequestPayload is a request payload for: POST /_matrix/client/r0/admin/register
// ApiUserAccountRegisterRequestPayload is a request payload for: POST /_matrix/client/{apiVersion:(r0|v3)}/admin/register
type ApiUserAccountRegisterRequestPayload struct {
Nonce string `json:"nonce"`
Username string `json:"username"`
Expand All @@ -87,19 +87,19 @@ type ApiUserAccountRegisterRequestPayload struct {
Admin bool `json:"admin"`
}

// ApiUserAccountRegisterResponse is a response as found at: POST /_matrix/client/r0/admin/register
// ApiUserAccountRegisterResponse is a response as found at: POST /_matrix/client/{apiVersion:(r0|v3)}/admin/register
type ApiUserAccountRegisterResponse struct {
AccessToken string `json:"access_token"`
HomeServer string `json:"home_server"`
UserId string `json:"user_id"`
}

// ApiCommunityInviteResponse is a response as found at: POST /_matrix/client/r0/groups/{communityId}/admin/users/invite/<invitee-id>
// ApiCommunityInviteResponse is a response as found at: POST /_matrix/client/{apiVersion:(r0|v3)}/groups/{communityId}/admin/users/invite/<invitee-id>
type ApiCommunityInviteResponse struct {
State string `json:"state"`
}

// ApiCommunityInvitedUsersResponse is a response as found at: GET /_matrix/client/r0/groups/{communityId}/invited_users
// ApiCommunityInvitedUsersResponse is a response as found at: GET /_matrix/client/{apiVersion:(r0|v3)}/groups/{communityId}/invited_users
type ApiCommunityInvitedUsersResponse struct {
Chunk []ApiEntityCommunityInvitedUser `json:"chunk"`
TotalUserCountEstimate int `json:"total_user_count_estimate"`
Expand Down

0 comments on commit 9ede33a

Please sign in to comment.