2
2
# This file is licensed under the Affero General Public License (AGPL) version 3.
3
3
#
4
4
# Copyright 2022 The Matrix.org Foundation C.I.C.
5
- # Copyright (C) 2023 New Vector, Ltd
5
+ # Copyright (C) 2023-2024 New Vector, Ltd
6
6
#
7
7
# This program is free software: you can redistribute it and/or modify
8
8
# it under the terms of the GNU Affero General Public License as
19
19
#
20
20
#
21
21
22
+ from typing import Dict
23
+ from urllib .parse import urlparse
24
+
22
25
from twisted .test .proto_helpers import MemoryReactor
26
+ from twisted .web .resource import Resource
23
27
24
28
from synapse .rest .client import rendezvous
29
+ from synapse .rest .synapse .client .rendezvous import MSC4108RendezvousSessionResource
25
30
from synapse .server import HomeServer
26
31
from synapse .util import Clock
27
32
@@ -41,6 +46,12 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
41
46
self .hs = self .setup_test_homeserver ()
42
47
return self .hs
43
48
49
+ def create_resource_dict (self ) -> Dict [str , Resource ]:
50
+ return {
51
+ ** super ().create_resource_dict (),
52
+ "/_synapse/client/rendezvous" : MSC4108RendezvousSessionResource (self .hs ),
53
+ }
54
+
44
55
def test_disabled (self ) -> None :
45
56
channel = self .make_request ("POST" , msc3886_endpoint , {}, access_token = None )
46
57
self .assertEqual (channel .code , 404 )
@@ -73,3 +84,327 @@ def test_msc4108_delegation(self) -> None:
73
84
channel = self .make_request ("POST" , msc4108_endpoint , {}, access_token = None )
74
85
self .assertEqual (channel .code , 307 )
75
86
self .assertEqual (channel .headers .getRawHeaders ("Location" ), ["https://asd" ])
87
+
88
+ @override_config (
89
+ {
90
+ "disable_registration" : True ,
91
+ "experimental_features" : {
92
+ "msc4108_enabled" : True ,
93
+ "msc3861" : {
94
+ "enabled" : True ,
95
+ "issuer" : "https://issuer" ,
96
+ "client_id" : "client_id" ,
97
+ "client_auth_method" : "client_secret_post" ,
98
+ "client_secret" : "client_secret" ,
99
+ "admin_token" : "admin_token_value" ,
100
+ },
101
+ },
102
+ }
103
+ )
104
+ def test_msc4108 (self ) -> None :
105
+ """
106
+ Test the MSC4108 rendezvous endpoint, including:
107
+ - Creating a session
108
+ - Getting the data back
109
+ - Updating the data
110
+ - Deleting the data
111
+ - ETag handling
112
+ """
113
+ # We can post arbitrary data to the endpoint
114
+ channel = self .make_request (
115
+ "POST" ,
116
+ msc4108_endpoint ,
117
+ "foo=bar" ,
118
+ # This sets the content type to application/x-www-form-urlencoded
119
+ content_is_form = True ,
120
+ access_token = None ,
121
+ )
122
+ self .assertEqual (channel .code , 201 )
123
+ self .assertSubstring ("/_synapse/client/rendezvous/" , channel .json_body ["url" ])
124
+ headers = dict (channel .headers .getAllRawHeaders ())
125
+ self .assertIn (b"ETag" , headers )
126
+ self .assertIn (b"Expires" , headers )
127
+ self .assertEqual (headers [b"Content-Type" ], [b"application/json" ])
128
+ self .assertEqual (headers [b"Access-Control-Allow-Origin" ], [b"*" ])
129
+ self .assertEqual (headers [b"Access-Control-Expose-Headers" ], [b"etag" ])
130
+ self .assertEqual (headers [b"Cache-Control" ], [b"no-store" ])
131
+ self .assertEqual (headers [b"Pragma" ], [b"no-cache" ])
132
+ self .assertIn ("url" , channel .json_body )
133
+ self .assertTrue (channel .json_body ["url" ].startswith ("https://" ))
134
+
135
+ url = urlparse (channel .json_body ["url" ])
136
+ session_endpoint = url .path
137
+ etag = headers [b"ETag" ][0 ]
138
+
139
+ # We can get the data back
140
+ channel = self .make_request (
141
+ "GET" ,
142
+ session_endpoint ,
143
+ access_token = None ,
144
+ )
145
+
146
+ self .assertEqual (channel .code , 200 )
147
+ headers = dict (channel .headers .getAllRawHeaders ())
148
+ self .assertEqual (headers [b"ETag" ], [etag ])
149
+ self .assertIn (b"Expires" , headers )
150
+ self .assertEqual (
151
+ headers [b"Content-Type" ], [b"application/x-www-form-urlencoded" ]
152
+ )
153
+ self .assertEqual (headers [b"Access-Control-Allow-Origin" ], [b"*" ])
154
+ self .assertEqual (headers [b"Access-Control-Expose-Headers" ], [b"etag" ])
155
+ self .assertEqual (headers [b"Cache-Control" ], [b"no-store" ])
156
+ self .assertEqual (headers [b"Pragma" ], [b"no-cache" ])
157
+ self .assertEqual (channel .text_body , "foo=bar" )
158
+
159
+ # We can make sure the data hasn't changed
160
+ channel = self .make_request (
161
+ "GET" ,
162
+ session_endpoint ,
163
+ access_token = None ,
164
+ custom_headers = [("If-None-Match" , etag )],
165
+ )
166
+
167
+ self .assertEqual (channel .code , 304 )
168
+
169
+ # We can update the data
170
+ channel = self .make_request (
171
+ "PUT" ,
172
+ session_endpoint ,
173
+ "foo=baz" ,
174
+ content_is_form = True ,
175
+ access_token = None ,
176
+ custom_headers = [("If-Match" , etag )],
177
+ )
178
+
179
+ self .assertEqual (channel .code , 202 )
180
+ headers = dict (channel .headers .getAllRawHeaders ())
181
+ old_etag = etag
182
+ new_etag = headers [b"ETag" ][0 ]
183
+
184
+ # If we try to update it again with the old etag, it should fail
185
+ channel = self .make_request (
186
+ "PUT" ,
187
+ session_endpoint ,
188
+ "bar=baz" ,
189
+ content_is_form = True ,
190
+ access_token = None ,
191
+ custom_headers = [("If-Match" , old_etag )],
192
+ )
193
+
194
+ self .assertEqual (channel .code , 412 )
195
+ self .assertEqual (channel .json_body ["errcode" ], "M_CONCURRENT_WRITE" )
196
+
197
+ # If we try to get with the old etag, we should get the updated data
198
+ channel = self .make_request (
199
+ "GET" ,
200
+ session_endpoint ,
201
+ access_token = None ,
202
+ custom_headers = [("If-None-Match" , old_etag )],
203
+ )
204
+
205
+ self .assertEqual (channel .code , 200 )
206
+ headers = dict (channel .headers .getAllRawHeaders ())
207
+ self .assertEqual (headers [b"ETag" ], [new_etag ])
208
+ self .assertEqual (channel .text_body , "foo=baz" )
209
+
210
+ # We can delete the data
211
+ channel = self .make_request (
212
+ "DELETE" ,
213
+ session_endpoint ,
214
+ access_token = None ,
215
+ )
216
+
217
+ self .assertEqual (channel .code , 204 )
218
+
219
+ # If we try to get the data again, it should fail
220
+ channel = self .make_request (
221
+ "GET" ,
222
+ session_endpoint ,
223
+ access_token = None ,
224
+ )
225
+
226
+ self .assertEqual (channel .code , 404 )
227
+ self .assertEqual (channel .json_body ["errcode" ], "M_NOT_FOUND" )
228
+
229
+ @override_config (
230
+ {
231
+ "disable_registration" : True ,
232
+ "experimental_features" : {
233
+ "msc4108_enabled" : True ,
234
+ "msc3861" : {
235
+ "enabled" : True ,
236
+ "issuer" : "https://issuer" ,
237
+ "client_id" : "client_id" ,
238
+ "client_auth_method" : "client_secret_post" ,
239
+ "client_secret" : "client_secret" ,
240
+ "admin_token" : "admin_token_value" ,
241
+ },
242
+ },
243
+ }
244
+ )
245
+ def test_msc4108_expiration (self ) -> None :
246
+ """
247
+ Test that entries are evicted after a TTL.
248
+ """
249
+ # Start a new session
250
+ channel = self .make_request (
251
+ "POST" ,
252
+ msc4108_endpoint ,
253
+ "foo=bar" ,
254
+ content_is_form = True ,
255
+ access_token = None ,
256
+ )
257
+ self .assertEqual (channel .code , 201 )
258
+ session_endpoint = urlparse (channel .json_body ["url" ]).path
259
+
260
+ # Sanity check that we can get the data back
261
+ channel = self .make_request (
262
+ "GET" ,
263
+ session_endpoint ,
264
+ access_token = None ,
265
+ )
266
+ self .assertEqual (channel .code , 200 )
267
+ self .assertEqual (channel .text_body , "foo=bar" )
268
+
269
+ # Advance the clock, TTL of entries is 5 minutes
270
+ self .reactor .advance (300 )
271
+
272
+ # Get the data back, it should be gone
273
+ channel = self .make_request (
274
+ "GET" ,
275
+ session_endpoint ,
276
+ access_token = None ,
277
+ )
278
+ self .assertEqual (channel .code , 404 )
279
+
280
+ @override_config (
281
+ {
282
+ "disable_registration" : True ,
283
+ "experimental_features" : {
284
+ "msc4108_enabled" : True ,
285
+ "msc3861" : {
286
+ "enabled" : True ,
287
+ "issuer" : "https://issuer" ,
288
+ "client_id" : "client_id" ,
289
+ "client_auth_method" : "client_secret_post" ,
290
+ "client_secret" : "client_secret" ,
291
+ "admin_token" : "admin_token_value" ,
292
+ },
293
+ },
294
+ }
295
+ )
296
+ def test_msc4108_capacity (self ) -> None :
297
+ """
298
+ Test that a capacity limit is enforced on the rendezvous sessions, as old
299
+ entries are evicted at an interval when the limit is reached.
300
+ """
301
+ # Start a new session
302
+ channel = self .make_request (
303
+ "POST" ,
304
+ msc4108_endpoint ,
305
+ "foo=bar" ,
306
+ content_is_form = True ,
307
+ access_token = None ,
308
+ )
309
+ self .assertEqual (channel .code , 201 )
310
+ session_endpoint = urlparse (channel .json_body ["url" ]).path
311
+
312
+ # Sanity check that we can get the data back
313
+ channel = self .make_request (
314
+ "GET" ,
315
+ session_endpoint ,
316
+ access_token = None ,
317
+ )
318
+ self .assertEqual (channel .code , 200 )
319
+ self .assertEqual (channel .text_body , "foo=bar" )
320
+
321
+ # Start a lot of new sessions
322
+ for _ in range (100 ):
323
+ channel = self .make_request (
324
+ "POST" ,
325
+ msc4108_endpoint ,
326
+ "foo=bar" ,
327
+ content_is_form = True ,
328
+ access_token = None ,
329
+ )
330
+ self .assertEqual (channel .code , 201 )
331
+
332
+ # Get the data back, it should still be there, as the eviction hasn't run yet
333
+ channel = self .make_request (
334
+ "GET" ,
335
+ session_endpoint ,
336
+ access_token = None ,
337
+ )
338
+
339
+ self .assertEqual (channel .code , 200 )
340
+
341
+ # Advance the clock, as it will trigger the eviction
342
+ self .reactor .advance (1 )
343
+
344
+ # Get the data back, it should be gone
345
+ channel = self .make_request (
346
+ "GET" ,
347
+ session_endpoint ,
348
+ access_token = None ,
349
+ )
350
+
351
+ @override_config (
352
+ {
353
+ "disable_registration" : True ,
354
+ "experimental_features" : {
355
+ "msc4108_enabled" : True ,
356
+ "msc3861" : {
357
+ "enabled" : True ,
358
+ "issuer" : "https://issuer" ,
359
+ "client_id" : "client_id" ,
360
+ "client_auth_method" : "client_secret_post" ,
361
+ "client_secret" : "client_secret" ,
362
+ "admin_token" : "admin_token_value" ,
363
+ },
364
+ },
365
+ }
366
+ )
367
+ def test_msc4108_hard_capacity (self ) -> None :
368
+ """
369
+ Test that a hard capacity limit is enforced on the rendezvous sessions, as old
370
+ entries are evicted immediately when the limit is reached.
371
+ """
372
+ # Start a new session
373
+ channel = self .make_request (
374
+ "POST" ,
375
+ msc4108_endpoint ,
376
+ "foo=bar" ,
377
+ content_is_form = True ,
378
+ access_token = None ,
379
+ )
380
+ self .assertEqual (channel .code , 201 )
381
+ session_endpoint = urlparse (channel .json_body ["url" ]).path
382
+
383
+ # Sanity check that we can get the data back
384
+ channel = self .make_request (
385
+ "GET" ,
386
+ session_endpoint ,
387
+ access_token = None ,
388
+ )
389
+ self .assertEqual (channel .code , 200 )
390
+ self .assertEqual (channel .text_body , "foo=bar" )
391
+
392
+ # Start a lot of new sessions
393
+ for _ in range (200 ):
394
+ channel = self .make_request (
395
+ "POST" ,
396
+ msc4108_endpoint ,
397
+ "foo=bar" ,
398
+ content_is_form = True ,
399
+ access_token = None ,
400
+ )
401
+ self .assertEqual (channel .code , 201 )
402
+
403
+ # Get the data back, it should already be gone as we hit the hard limit
404
+ channel = self .make_request (
405
+ "GET" ,
406
+ session_endpoint ,
407
+ access_token = None ,
408
+ )
409
+
410
+ self .assertEqual (channel .code , 404 )
0 commit comments