aboutsummaryrefslogtreecommitdiff
path: root/clientapi/threepid/invites.go
blob: 5e7e4f2b4885e988787db750cc76126f67db2897 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// 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 threepid

import (
	"context"
	"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  []gomatrixserverlib.PublicKey `json:"public_keys"`
}

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")
)

// CheckAndProcessInvite 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 CheckAndProcessInvite(
	ctx context.Context,
	device *authtypes.Device, body *MembershipRequest, cfg *config.Dendrite,
	rsAPI api.RoomserverInternalAPI, db accounts.Database,
	producer *producers.RoomserverProducer, membership string, roomID string,
	evTime time.Time,
) (inviteStoredOnIDServer bool, err error) {
	if membership != gomatrixserverlib.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
	} 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
		err = ErrMissingParameter
		return
	}

	lookupRes, storeInviteRes, err := queryIDServer(ctx, db, cfg, device, body, roomID)
	if err != nil {
		return
	}

	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.
		err = emit3PIDInviteEvent(
			ctx, body, storeInviteRes, device, roomID, cfg, rsAPI, producer, evTime,
		)
		inviteStoredOnIDServer = err == nil

		return
	}

	// 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

	return
}

// 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(
	ctx context.Context,
	db accounts.Database, cfg *config.Dendrite, device *authtypes.Device,
	body *MembershipRequest, roomID string,
) (lookupRes *idServerLookupResponse, storeInviteRes *idServerStoreInviteResponse, err error) {
	if err = isTrusted(body.IDServer, cfg); err != nil {
		return
	}

	// Lookup the 3PID
	lookupRes, err = queryIDServerLookup(ctx, body)
	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
		storeInviteRes, err = queryIDServerStoreInvite(ctx, db, cfg, 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 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
		return queryIDServer(ctx, db, cfg, device, body, roomID)
	}

	// Check the request signatures and send an error if one isn't valid
	if err = checkIDServerSignatures(ctx, body, lookupRes); 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(ctx context.Context, body *MembershipRequest) (*idServerLookupResponse, error) {
	address := url.QueryEscape(body.Address)
	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)
	if err != nil {
		return nil, err
	}
	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
	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(
	ctx context.Context,
	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 {
		profile, err = db.GetProfileByLocalpart(ctx, localpart)
		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.

	requestURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/store-invite", body.IDServer)
	req, err := http.NewRequest(http.MethodPost, requestURL, 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.WithContext(ctx))
	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.
// We assume that the ID server is trusted at this point.
// 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(ctx context.Context, idServerName string, keyID string) ([]byte, error) {
	requestURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/pubkey/%s", idServerName, keyID)
	req, err := http.NewRequest(http.MethodGet, requestURL, nil)
	if err != nil {
		return nil, err
	}
	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
	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.
// We assume that the ID server is trusted at this point.
// Returns nil if all the verifications succeeded.
// Returns an error if something failed in the process.
func checkIDServerSignatures(
	ctx context.Context, 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
	}

	signatures, ok := res.Signatures[body.IDServer]
	if !ok {
		return errors.New("No signature for domain " + body.IDServer)
	}

	for keyID := range signatures {
		pubKey, err := queryIDServerPubKey(ctx, body.IDServer, 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(
	ctx context.Context,
	body *MembershipRequest, res *idServerStoreInviteResponse,
	device *authtypes.Device, roomID string, cfg *config.Dendrite,
	rsAPI api.RoomserverInternalAPI, producer *producers.RoomserverProducer,
	evTime time.Time,
) 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 := gomatrixserverlib.ThirdPartyInviteContent{
		DisplayName:    res.DisplayName,
		KeyValidityURL: validityURL,
		PublicKey:      res.PublicKey,
		PublicKeys:     res.PublicKeys,
	}

	if err := builder.SetContent(content); err != nil {
		return err
	}

	queryRes := api.QueryLatestEventsAndStateResponse{}
	event, err := common.BuildEvent(ctx, builder, cfg, evTime, rsAPI, &queryRes)
	if err != nil {
		return err
	}

	_, err = producer.SendEvents(
		ctx,
		[]gomatrixserverlib.HeaderedEvent{
			(*event).Headered(queryRes.RoomVersion),
		},
		cfg.Matrix.ServerName,
		nil,
	)
	return err
}