2017-08-29 09:17:26 -05:00
// 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.
2017-09-01 04:13:10 -05:00
package threepid
2017-08-29 09:17:26 -05:00
import (
2017-09-13 07:37:50 -05:00
"context"
2017-08-29 09:17:26 -05:00
"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"
"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"
)
// 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" `
}
// 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" `
}
2017-09-11 13:18:19 -05:00
var (
// ErrMissingParameter is the error raised if a request for 3PID invite has
// an incomplete body
ErrMissingParameter = errors . New ( "'address', 'id_server' and 'medium' must all be supplied" )
// ErrNotTrusted is the error raised if an identity server isn't in the list
// of trusted servers in the configuration file.
ErrNotTrusted = errors . New ( "untrusted server" )
)
2017-09-01 04:13:10 -05:00
// CheckAndProcessInvite analyses the body of an incoming membership request.
2017-08-29 09:17:26 -05:00
// 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.
2017-09-01 04:13:10 -05:00
func CheckAndProcessInvite (
2018-08-06 08:09:25 -05:00
ctx context . Context ,
2017-09-11 13:18:19 -05:00
device * authtypes . Device , body * MembershipRequest , cfg config . Dendrite ,
queryAPI api . RoomserverQueryAPI , db * accounts . Database ,
2017-08-29 09:17:26 -05:00
producer * producers . RoomserverProducer , membership string , roomID string ,
2018-08-06 08:09:25 -05:00
evTime time . Time ,
2017-09-11 13:18:19 -05:00
) ( inviteStoredOnIDServer bool , err error ) {
2017-08-29 09:17:26 -05:00
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
2017-09-11 13:18:19 -05:00
return
2017-08-29 09:17:26 -05:00
} 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
2017-09-11 13:18:19 -05:00
err = ErrMissingParameter
return
2017-08-29 09:17:26 -05:00
}
2018-08-06 08:09:25 -05:00
lookupRes , storeInviteRes , err := queryIDServer ( ctx , db , cfg , device , body , roomID )
2017-08-29 09:17:26 -05:00
if err != nil {
2017-09-11 13:18:19 -05:00
return
2017-08-29 09:17:26 -05:00
}
if lookupRes . 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
// storeInviteRes.
2018-08-06 08:09:25 -05:00
err = emit3PIDInviteEvent (
ctx , body , storeInviteRes , device , roomID , cfg , queryAPI , producer , evTime ,
)
2017-09-11 13:18:19 -05:00
inviteStoredOnIDServer = err == nil
2017-08-29 09:17:26 -05:00
2017-09-11 13:18:19 -05:00
return
2017-08-29 09:17:26 -05:00
}
// 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 = lookupRes . MXID
2017-09-11 13:18:19 -05:00
return
2017-08-29 09:17:26 -05:00
}
// 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 (
2017-09-18 08:15:27 -05:00
ctx context . Context ,
2017-09-11 13:18:19 -05:00
db * accounts . Database , cfg config . Dendrite , device * authtypes . Device ,
body * MembershipRequest , roomID string ,
2017-08-29 09:17:26 -05:00
) ( lookupRes * idServerLookupResponse , storeInviteRes * idServerStoreInviteResponse , err error ) {
2017-09-11 13:18:19 -05:00
if err = isTrusted ( body . IDServer , cfg ) ; err != nil {
return
}
2017-08-29 09:17:26 -05:00
// Lookup the 3PID
2017-09-18 08:15:27 -05:00
lookupRes , err = queryIDServerLookup ( ctx , body )
2017-08-29 09:17:26 -05:00
if err != nil {
return
}
if lookupRes . MXID == "" {
// No Matrix ID matches with the given 3PID, ask the server to store the
// invite and return a token
2017-09-18 08:15:27 -05:00
storeInviteRes , err = queryIDServerStoreInvite ( ctx , db , cfg , device , body , roomID )
2017-08-29 09:17:26 -05:00
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 lookupRes . NotBefore > now || now > lookupRes . NotAfter {
// If the current timestamp isn't in the time frame in which the association
// is known to be valid, re-run the query
2017-09-18 08:15:27 -05:00
return queryIDServer ( ctx , db , cfg , device , body , roomID )
2017-08-29 09:17:26 -05:00
}
// Check the request signatures and send an error if one isn't valid
2017-09-18 08:15:27 -05:00
if err = checkIDServerSignatures ( ctx , body , lookupRes ) ; err != nil {
2017-08-29 09:17:26 -05:00
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.
2017-09-18 08:15:27 -05:00
func queryIDServerLookup ( ctx context . Context , body * MembershipRequest ) ( * idServerLookupResponse , error ) {
2017-08-29 09:17:26 -05:00
address := url . QueryEscape ( body . Address )
2018-04-20 09:50:44 -05:00
requestURL := fmt . Sprintf ( "https://%s/_matrix/identity/api/v1/lookup?medium=%s&address=%s" , body . IDServer , body . Medium , address )
req , err := http . NewRequest ( http . MethodGet , requestURL , nil )
2017-09-18 08:15:27 -05:00
if err != nil {
return nil , err
}
resp , err := http . DefaultClient . Do ( req . WithContext ( ctx ) )
2017-08-29 09:17:26 -05:00
if err != nil {
return nil , err
}
if resp . StatusCode != http . StatusOK {
// TODO: Log the error supplied with the identity server?
errMgs := fmt . Sprintf ( "Failed to ask %s to store an invite for %s" , body . IDServer , body . Address )
return nil , errors . New ( errMgs )
}
var res idServerLookupResponse
err = json . NewDecoder ( resp . Body ) . Decode ( & res )
return & res , err
}
// 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 (
2017-09-18 08:15:27 -05:00
ctx context . Context ,
2017-08-29 09:17:26 -05:00
db * accounts . Database , cfg config . Dendrite , device * authtypes . Device ,
body * MembershipRequest , roomID string ,
) ( * idServerStoreInviteResponse , error ) {
// Retrieve the sender's profile to get their display name
localpart , serverName , err := gomatrixserverlib . SplitID ( '@' , device . UserID )
if err != nil {
return nil , err
}
var profile * authtypes . Profile
if serverName == cfg . Matrix . ServerName {
2017-09-18 08:15:27 -05:00
profile , err = db . GetProfileByLocalpart ( ctx , localpart )
2017-08-29 09:17:26 -05:00
if err != nil {
return nil , err
}
} else {
profile = & authtypes . Profile { }
}
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 )
data . Add ( "sender_display_name" , profile . DisplayName )
// TODO: Also send:
// - The room name (room_name)
// - The room's avatar url (room_avatar_url)
// See https://github.com/matrix-org/sydent/blob/master/sydent/http/servlets/store_invite_servlet.py#L82-L91
// These can be easily retrieved by requesting the public rooms API
// server's database.
2018-04-20 09:50:44 -05:00
requestURL := fmt . Sprintf ( "https://%s/_matrix/identity/api/v1/store-invite" , body . IDServer )
req , err := http . NewRequest ( http . MethodPost , requestURL , strings . NewReader ( data . Encode ( ) ) )
2017-08-29 09:17:26 -05:00
if err != nil {
return nil , err
}
req . Header . Add ( "Content-Type" , "application/x-www-form-urlencoded" )
2017-09-18 08:15:27 -05:00
resp , err := client . Do ( req . WithContext ( ctx ) )
2017-08-29 09:17:26 -05:00
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 )
}
var idResp 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.
2017-09-12 11:15:13 -05:00
// We assume that the ID server is trusted at this point.
2017-08-29 09:17:26 -05:00
// 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.
2017-09-18 08:15:27 -05:00
func queryIDServerPubKey ( ctx context . Context , idServerName string , keyID string ) ( [ ] byte , error ) {
2018-04-20 09:50:44 -05:00
requestURL := fmt . Sprintf ( "https://%s/_matrix/identity/api/v1/pubkey/%s" , idServerName , keyID )
req , err := http . NewRequest ( http . MethodGet , requestURL , nil )
2017-09-18 08:15:27 -05:00
if err != nil {
return nil , err
}
resp , err := http . DefaultClient . Do ( req . WithContext ( ctx ) )
2017-08-29 09:17:26 -05:00
if err != nil {
return nil , err
}
var pubKeyRes struct {
PublicKey gomatrixserverlib . Base64String ` json:"public_key" `
}
if resp . StatusCode != http . StatusOK {
errMsg := fmt . Sprintf ( "Couldn't retrieve key %s from server %s" , keyID , idServerName )
return nil , errors . New ( errMsg )
}
err = json . NewDecoder ( resp . Body ) . Decode ( & pubKeyRes )
return pubKeyRes . PublicKey , err
}
// 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.
2017-09-12 11:15:13 -05:00
// We assume that the ID server is trusted at this point.
2017-08-29 09:17:26 -05:00
// Returns nil if all the verifications succeeded.
// Returns an error if something failed in the process.
2017-09-18 08:15:27 -05:00
func checkIDServerSignatures (
ctx context . Context , body * MembershipRequest , res * idServerLookupResponse ,
) error {
2017-08-29 09:17:26 -05:00
// Mashall the body so we can give it to VerifyJSON
marshalledBody , err := json . Marshal ( * res )
if err != nil {
return err
}
signatures , ok := res . Signatures [ body . IDServer ]
if ! ok {
return errors . New ( "No signature for domain " + body . IDServer )
}
for keyID := range signatures {
2017-09-18 08:15:27 -05:00
pubKey , err := queryIDServerPubKey ( ctx , body . IDServer , keyID )
2017-08-29 09:17:26 -05:00
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 (
2018-08-06 08:09:25 -05:00
ctx context . Context ,
2017-08-29 09:17:26 -05:00
body * MembershipRequest , res * idServerStoreInviteResponse ,
device * authtypes . Device , roomID string , cfg config . Dendrite ,
queryAPI api . RoomserverQueryAPI , producer * producers . RoomserverProducer ,
2018-08-06 08:09:25 -05:00
evTime time . Time ,
2017-08-29 09:17:26 -05:00
) 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 ,
PublicKeys : res . PublicKeys ,
}
if err := builder . SetContent ( content ) ; err != nil {
return err
}
var queryRes * api . QueryLatestEventsAndStateResponse
2018-08-06 08:09:25 -05:00
event , err := common . BuildEvent ( ctx , builder , cfg , evTime , queryAPI , queryRes )
2017-08-29 09:17:26 -05:00
if err != nil {
return err
}
2018-08-06 08:09:25 -05:00
_ , err = producer . SendEvents ( ctx , [ ] gomatrixserverlib . Event { * event } , cfg . Matrix . ServerName , nil )
2018-05-26 06:03:35 -05:00
return err
2017-08-29 09:17:26 -05:00
}