From a61327eccd1ee75645e3acb435b9fa99aafbe1df Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 25 Aug 2017 17:18:36 +0100 Subject: [PATCH] Move third-party invites to a separate package and doc' it --- .../thirdpartyinvites/thirdpartyinvites.go | 337 ++++++++++++++++++ .../dendrite/clientapi/writers/membership.go | 249 +------------ 2 files changed, 345 insertions(+), 241 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/clientapi/thirdpartyinvites/thirdpartyinvites.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/thirdpartyinvites/thirdpartyinvites.go b/src/github.com/matrix-org/dendrite/clientapi/thirdpartyinvites/thirdpartyinvites.go new file mode 100644 index 000000000..371a3af6f --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/thirdpartyinvites/thirdpartyinvites.go @@ -0,0 +1,337 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package thirdpartyinvites + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/events" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/gomatrixserverlib" + + "github.com/matrix-org/util" +) + +// MembershipRequest represents the body of an incoming POST request +// on /rooms/{roomID}/(join|kick|ban|unban|leave|invite) +type MembershipRequest struct { + UserID string `json:"user_id"` + Reason string `json:"reason"` + IDServer string `json:"id_server"` + Medium string `json:"medium"` + Address string `json:"address"` +} + +// idServerResponses is a structure containing the representation of the responses +// for two requests on an identity server: one on /lookup and one on /store-invite +type idServerResponses struct { + Lookup *idServerLookupResponse + StoreInvite *idServerStoreInviteResponse +} + +// idServerLookupResponse represents the response described at https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-identity-api-v1-lookup +type idServerLookupResponse struct { + TS int64 `json:"ts"` + NotBefore int64 `json:"not_before"` + NotAfter int64 `json:"not_after"` + Medium string `json:"medium"` + Address string `json:"address"` + MXID string `json:"mxid"` + Signatures map[string]map[string]string `json:"signatures"` +} + +// idServerLookupResponse represents the response described at https://matrix.org/docs/spec/client_server/r0.2.0.html#invitation-storage +type idServerStoreInviteResponse struct { + PublicKey string `json:"public_key"` + Token string `json:"token"` + DisplayName string `json:"display_name"` + PublicKeys []common.PublicKey `json:"public_keys"` +} + +// CheckAndProcess analyses the body of an incoming membership request. +// If the fields relative to a third-party-invite are all supplied, lookups the +// matching Matrix ID from the given identity server. If no Matrix ID is +// associated to the given 3PID, asks the identity server to store the invite +// and emit a "m.room.third_party_invite" event. +// Returns a representation of the HTTP response to send to the user. +// Returns a representation of a non-200 HTTP response if something went wrong +// in the process, or if some 3PID fields aren't supplied but others are. +// If none of the 3PID-specific fields are supplied, or if a Matrix ID is +// supplied by the identity server, returns nil to indicate that the request +// must be processed as a non-3PID membership request. In the latter case, +// fills the Matrix ID in the request body so a normal invite membership event +// can be emitted. +func CheckAndProcess( + req *http.Request, device *authtypes.Device, body *MembershipRequest, + cfg config.Dendrite, queryAPI api.RoomserverQueryAPI, + producer *producers.RoomserverProducer, membership string, roomID string, +) *util.JSONResponse { + if membership != "invite" || (body.Address == "" && body.IDServer == "" && body.Medium == "") { + // If none of the 3PID-specific fields are supplied, it's a standard invite + // so return nil for it to be processed as such + return nil + } else if body.Address == "" || body.IDServer == "" || body.Medium == "" { + // If at least one of the 3PID-specific fields is supplied but not all + // of them, return an error + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON("'address', 'id_server' and 'medium' must all be supplied"), + } + } + + resp, err := queryIDServer(req, device, body, roomID) + if err != nil { + resErr := httputil.LogThenError(req, err) + return &resErr + } + + if resp.Lookup.MXID == "" { + // No Matrix ID could be found for this 3PID, meaning that a + // "m.room.third_party_invite" have to be emitted from the data in + // resp.StoreInvite. + err = emit3PIDInviteEvent(body, resp.StoreInvite, device, roomID, cfg, queryAPI, producer) + if err == events.ErrRoomNoExists { + return &util.JSONResponse{ + Code: 404, + JSON: jsonerror.NotFound(err.Error()), + } + } else if err != nil { + resErr := httputil.LogThenError(req, err) + return &resErr + } + + // If everything went well, returns with an empty response. + return &util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } + } + + // A Matrix ID have been found: set it in the body request and let the process + // continue to create a "m.room.member" event with an "invite" membership + body.UserID = resp.Lookup.MXID + + return nil +} + +// queryIDServer handles all the requests to the identity server, starting by +// looking up the given 3PID on the given identity server. +// If the lookup returned a Matrix ID, checks if the current time is within the +// time frame in which the 3PID-MXID association is known to be valid, and checks +// the response's signatures. If one of the checks fails, returns an error. +// If the lookup didn't return a Matrix ID, asks the identity server to store +// the invite and to respond with a token. +// Returns a representation of the response for both cases. +// Returns an error if a check or a request failed. +func queryIDServer( + req *http.Request, device *authtypes.Device, body *MembershipRequest, + roomID string, +) (res *idServerResponses, err error) { + res = new(idServerResponses) + // Lookup the 3PID + res.Lookup, err = queryIDServerLookup(body) + if err != nil { + return + } + + if res.Lookup.MXID == "" { + // No Matrix ID matches with the given 3PID, ask the server to store the + // invite and return a token + res.StoreInvite, err = queryIDServerStoreInvite(device, body, roomID) + return + } + + // A Matrix ID matches with the given 3PID + // Get timestamp in milliseconds to compare it with the timestamps provided + // by the identity server + now := time.Now().UnixNano() / 1000000 + if res.Lookup.NotBefore > now || now > res.Lookup.NotAfter { + // If the current timestamp isn't in the time frame in which the association + // is known to be valid, re-run the query + return queryIDServer(req, device, body, roomID) + } + + // Check the request signatures and send an error if one isn't valid + if err = checkIDServerSignatures(body, res.Lookup); err != nil { + return + } + + return +} + +// queryIDServerLookup sends a response to the identity server on /_matrix/identity/api/v1/lookup +// and returns the response as a structure. +// Returns an error if the request failed to send or if the response couldn't be parsed. +func queryIDServerLookup(body *MembershipRequest) (res *idServerLookupResponse, err error) { + address := url.QueryEscape(body.Address) + url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/lookup?medium=%s&address=%s", body.IDServer, body.Medium, address) + resp, err := http.Get(url) + if err != nil { + return + } + // TODO: Check status code + res = new(idServerLookupResponse) + err = json.NewDecoder(resp.Body).Decode(res) + return +} + +// queryIDServerStoreInvite sends a response to the identity server on /_matrix/identity/api/v1/store-invite +// and returns the response as a structure. +// Returns an error if the request failed to send or if the response couldn't be parsed. +func queryIDServerStoreInvite(device *authtypes.Device, body *MembershipRequest, roomID string) (*idServerStoreInviteResponse, error) { + client := http.Client{} + + data := url.Values{} + data.Add("medium", body.Medium) + data.Add("address", body.Address) + data.Add("room_id", roomID) + data.Add("sender", device.UserID) + + url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/store-invite", body.IDServer) + req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + // TODO: Log the error supplied with the identity server? + errMsg := fmt.Sprintf("Identity server %s responded with a %d error code", body.IDServer, resp.StatusCode) + return nil, errors.New(errMsg) + } + + idResp := new(idServerStoreInviteResponse) + err = json.NewDecoder(resp.Body).Decode(idResp) + return idResp, err +} + +// queryIDServerPubKey requests a public key identified with a given ID to the +// a given identity server and returns the matching base64-decoded public key. +// Returns an error if the request couldn't be sent, if its body couldn't be parsed +// or if the key couldn't be decoded from base64. +func queryIDServerPubKey(body *MembershipRequest, keyID string) (publicKey []byte, err error) { + url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/pubkey/%s", body.IDServer, keyID) + resp, err := http.Get(url) + if err != nil { + return + } + + var pubKeyRes struct { + PublicKey string `json:"public_key"` + } + + if resp.StatusCode != http.StatusOK { + // TODO: Log the error supplied with the identity server? + errMsg := fmt.Sprintf("Couldn't retrieve key %s from server %s", keyID, body.IDServer) + return nil, errors.New(errMsg) + } + + if err = json.NewDecoder(resp.Body).Decode(&pubKeyRes); err != nil { + return nil, err + } + + return base64.RawStdEncoding.DecodeString(pubKeyRes.PublicKey) +} + +// checkIDServerSignatures iterates over the signatures of a requests. +// If no signature can be found for the ID server's domain, returns an error, else +// iterates over the signature for the said domain, retrieves the matching public +// key, and verify it. +// Returns nil if all the verifications succeeded. +// Returns an error if something failed in the process. +func checkIDServerSignatures(body *MembershipRequest, res *idServerLookupResponse) error { + // Mashall the body so we can give it to VerifyJSON + marshalledBody, err := json.Marshal(*res) + if err != nil { + return err + } + + // TODO: Check if the domain is part of a list of trusted ID servers + signatures, ok := res.Signatures[body.IDServer] + if !ok { + return errors.New("No signature for domain " + body.IDServer) + } + + for keyID := range signatures { + pubKey, err := queryIDServerPubKey(body, keyID) + if err != nil { + return err + } + if err = gomatrixserverlib.VerifyJSON(body.IDServer, gomatrixserverlib.KeyID(keyID), pubKey, marshalledBody); err != nil { + return err + } + } + + return nil +} + +// emit3PIDInviteEvent builds and sends a "m.room.third_party_invite" event. +// Returns an error if something failed in the process. +func emit3PIDInviteEvent( + body *MembershipRequest, res *idServerStoreInviteResponse, + device *authtypes.Device, roomID string, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, producer *producers.RoomserverProducer, +) error { + builder := &gomatrixserverlib.EventBuilder{ + Sender: device.UserID, + RoomID: roomID, + Type: "m.room.third_party_invite", + StateKey: &res.Token, + } + + validityURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/pubkey/isvalid", body.IDServer) + content := common.ThirdPartyInviteContent{ + DisplayName: res.DisplayName, + KeyValidityURL: validityURL, + PublicKey: res.PublicKey, + } + + content.PublicKeys = make([]common.PublicKey, len(res.PublicKeys)) + copy(content.PublicKeys, res.PublicKeys) + + if err := builder.SetContent(content); err != nil { + return err + } + + var queryRes *api.QueryLatestEventsAndStateResponse + event, err := events.BuildEvent(builder, cfg, queryAPI, queryRes) + if err != nil { + return err + } + + if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { + return err + } + + return nil +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go index 7013d6f1e..659733299 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go @@ -15,14 +15,7 @@ package writers import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" "net/http" - "net/url" - "strings" - "time" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" @@ -30,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/clientapi/thirdpartyinvites" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/roomserver/api" @@ -38,14 +32,6 @@ import ( "github.com/matrix-org/util" ) -type membershipRequestBody struct { - UserID string `json:"user_id"` - Reason string `json:"reason"` - IDServer string `json:"id_server"` - Medium string `json:"medium"` - Address string `json:"address"` -} - // SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite) // by building a m.room.member event then sending it to the room server func SendMembership( @@ -53,12 +39,12 @@ func SendMembership( roomID string, membership string, cfg config.Dendrite, queryAPI api.RoomserverQueryAPI, producer *producers.RoomserverProducer, ) util.JSONResponse { - var body membershipRequestBody + var body thirdpartyinvites.MembershipRequest if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { return *reqErr } - if res := checkAndProcess3PIDInvite(req, device, &body, cfg, queryAPI, producer, roomID); res != nil { + if res := thirdpartyinvites.CheckAndProcess(req, device, &body, cfg, queryAPI, producer, membership, roomID); res != nil { return *res } @@ -115,6 +101,10 @@ func SendMembership( } } +// loadProfile lookups the profile of a given user from the database and returns +// it if the user is local to this server, or returns an empty profile if not. +// Returns an error if the retrieval failed or if the first parameter isn't a +// valid Matrix ID. func loadProfile(userID string, cfg config.Dendrite, accountDB *accounts.Database) (*authtypes.Profile, error) { localpart, serverName, err := gomatrixserverlib.SplitID('@', userID) if err != nil { @@ -137,7 +127,7 @@ func loadProfile(userID string, cfg config.Dendrite, accountDB *accounts.Databas // In the latter case, if there was an issue retrieving the user ID from the request body, // returns a JSONResponse with a corresponding error code and message. func getMembershipStateKey( - body membershipRequestBody, device *authtypes.Device, membership string, + body thirdpartyinvites.MembershipRequest, device *authtypes.Device, membership string, ) (stateKey string, reason string, response *util.JSONResponse) { if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" { // If we're in this case, the state key is contained in the request body, @@ -159,226 +149,3 @@ func getMembershipStateKey( return } - -func checkAndProcess3PIDInvite( - req *http.Request, device *authtypes.Device, body *membershipRequestBody, - cfg config.Dendrite, queryAPI api.RoomserverQueryAPI, - producer *producers.RoomserverProducer, roomID string, -) *util.JSONResponse { - if body.Address == "" && body.IDServer == "" && body.Medium == "" { - // If none of the 3PID-specific fields are supplied, it's a standard invite - // so return nil for it to be processed as such - return nil - } else if body.Address == "" || body.IDServer == "" || body.Medium == "" { - // If at least one of the 3PID-specific fields is supplied but not all - // of them, return an error - return &util.JSONResponse{ - Code: 400, - JSON: jsonerror.BadJSON("'address', 'id_server' and 'medium' must all be supplied"), - } - } - - resp, err := queryIDServer(req, device, body, roomID) - if err != nil { - resErr := httputil.LogThenError(req, err) - return &resErr - } - - if resp.Lookup.MXID == "" { - event, err := make3PIDInviteEvent(body, resp.StoreInvite, device, roomID, cfg, queryAPI) - if err == events.ErrRoomNoExists { - return &util.JSONResponse{ - Code: 404, - JSON: jsonerror.NotFound(err.Error()), - } - } else if err != nil { - resErr := httputil.LogThenError(req, err) - return &resErr - } - - if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { - resErr := httputil.LogThenError(req, err) - return &resErr - } - - return &util.JSONResponse{ - Code: 200, - JSON: struct{}{}, - } - } - - // Set the Matrix user ID from the body request and let the process - // continue to create a "m.room.member" event - body.UserID = resp.Lookup.MXID - - return nil -} - -type idServerResponses struct { - Lookup *idServerLookupResponse - StoreInvite *idServerStoreInviteResponse -} - -type idServerLookupResponse struct { - TS int64 `json:"ts"` - NotBefore int64 `json:"not_before"` - NotAfter int64 `json:"not_after"` - Medium string `json:"medium"` - Address string `json:"address"` - MXID string `json:"mxid"` - Signatures map[string]map[string]string `json:"signatures"` -} - -type idServerStoreInviteResponse struct { - PublicKey string `json:"public_key"` - Token string `json:"token"` - DisplayName string `json:"display_name"` - PublicKeys []common.PublicKey `json:"public_keys"` -} - -func queryIDServer( - req *http.Request, device *authtypes.Device, body *membershipRequestBody, - roomID string, -) (res *idServerResponses, err error) { - res = new(idServerResponses) - res.Lookup, err = queryIDServerLookup(body) - if err != nil { - return - } - if res.Lookup.MXID == "" { - res.StoreInvite, err = queryIDServerStoreInvite(device, body, roomID) - return - } - - // Get timestamp in milliseconds to compare it - now := time.Now().UnixNano() / 1000000 - if res.Lookup.NotBefore > now || now > res.Lookup.NotAfter { - // If the current timestamp isn't in the time frame in which the association - // is known to be valid, re-run the query - return queryIDServer(req, device, body, roomID) - } - - ok, err := checkIDServerSignatures(body, res.Lookup) - if err != nil { - return - } - if !ok { - err = errors.New("The identity server's identity could not be verified") - return - } - - return -} - -func queryIDServerLookup(body *membershipRequestBody) (res *idServerLookupResponse, err error) { - address := url.QueryEscape(body.Address) - url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/lookup?medium=%s&address=%s", body.IDServer, body.Medium, address) - resp, err := http.Get(url) - if err != nil { - return - } - // TODO: Check status code - res = new(idServerLookupResponse) - err = json.NewDecoder(resp.Body).Decode(res) - return -} - -func queryIDServerStoreInvite(device *authtypes.Device, body *membershipRequestBody, roomID string) (*idServerStoreInviteResponse, error) { - client := http.Client{} - - data := url.Values{} - data.Add("medium", body.Medium) - data.Add("address", body.Address) - data.Add("room_id", roomID) - data.Add("sender", device.UserID) - - url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/store-invite", body.IDServer) - req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode())) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - errMsg := fmt.Sprintf("Identity server %s responded with a %d error code", body.IDServer, resp.StatusCode) - return nil, errors.New(errMsg) - } - - idResp := new(idServerStoreInviteResponse) - err = json.NewDecoder(resp.Body).Decode(idResp) - return idResp, err -} - -func queryIDServerPubKey(body *membershipRequestBody, keyID string) (publicKey []byte, err error) { - url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/pubkey/%s", body.IDServer, keyID) - resp, err := http.Get(url) - if err != nil { - return - } - - var pubKeyRes struct { - PublicKey string `json:"public_key"` - } - if err = json.NewDecoder(resp.Body).Decode(&pubKeyRes); err != nil { - return nil, err - } - // TODO: Store the public key in the database and, if there's one stored, retrieve - // it and verify its validity (/isvalid) instead of fetching it - return base64.RawStdEncoding.DecodeString(pubKeyRes.PublicKey) -} - -func checkIDServerSignatures(body *membershipRequestBody, res *idServerLookupResponse) (ok bool, err error) { - marshalledBody, err := json.Marshal(*res) - if err != nil { - return - } - - for domain, signatures := range res.Signatures { - for keyID := range signatures { - pubKey, err := queryIDServerPubKey(body, keyID) - if err != nil { - return false, err - } - if err = gomatrixserverlib.VerifyJSON(domain, gomatrixserverlib.KeyID(keyID), pubKey, marshalledBody); err != nil { - return false, nil - } - } - } - - return true, nil -} - -func make3PIDInviteEvent( - body *membershipRequestBody, res *idServerStoreInviteResponse, - device *authtypes.Device, roomID string, cfg config.Dendrite, - queryAPI api.RoomserverQueryAPI, -) (*gomatrixserverlib.Event, error) { - builder := &gomatrixserverlib.EventBuilder{ - Sender: device.UserID, - RoomID: roomID, - Type: "m.room.third_party_invite", - StateKey: &res.Token, - } - - validityURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/pubkey/isvalid", body.IDServer) - content := common.ThirdPartyInviteContent{ - DisplayName: res.DisplayName, - KeyValidityURL: validityURL, - PublicKey: res.PublicKey, - } - - content.PublicKeys = make([]common.PublicKey, len(res.PublicKeys)) - copy(content.PublicKeys, res.PublicKeys) - - if err := builder.SetContent(content); err != nil { - return nil, err - } - - var queryRes *api.QueryLatestEventsAndStateResponse - return events.BuildEvent(builder, cfg, queryAPI, queryRes) -}