Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 5b24839

Browse files
committed
Merge remote-tracking branch 'synapse/develop' into delete_room_bg
2 parents a37b09c + 46d0937 commit 5b24839

19 files changed

+154
-47
lines changed

.github/PULL_REQUEST_TEMPLATE.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
### Pull Request Checklist
22

3-
<!-- Please read CONTRIBUTING.md before submitting your pull request -->
3+
<!-- Please read https://matrix-org.github.io/synapse/latest/development/contributing_guide.html before submitting your pull request -->
44

55
* [ ] Pull request is based on the develop branch
6-
* [ ] Pull request includes a [changelog file](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#changelog). The entry should:
6+
* [ ] Pull request includes a [changelog file](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should:
77
- Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
88
- Use markdown where necessary, mostly for `code blocks`.
99
- End with either a period (.) or an exclamation mark (!).
1010
- Start with a capital letter.
11-
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off)
12-
* [ ] Code style is correct (run the [linters](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#code-style))
11+
* [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
12+
* [ ] [Code style](https://matrix-org.github.io/synapse/latest/code_style.html) is correct
13+
(run the [linters](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

changelog.d/11033.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Do not accept events if a third-party rule module API callback raises an exception.

changelog.d/11200.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug wherein a missing `Content-Type` header when downloading remote media would cause Synapse to throw an error.

changelog.d/11217.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug introduced in 1.35.0 which made it impossible to join rooms that return a `send_join` response containing floats.

changelog.d/11225.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace outdated links in the pull request checklist with links to the rendered documentation.

changelog.d/11226.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug in unit test `test_block_room_and_not_purge`.

changelog.d/11229.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`ObservableDeferred`: run registered observers in order.

docs/modules/third_party_rules_callbacks.md

+8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ event with new data by returning the new event's data as a dictionary. In order
4343
that, it is recommended the module calls `event.get_dict()` to get the current event as a
4444
dictionary, and modify the returned dictionary accordingly.
4545

46+
If `check_event_allowed` raises an exception, the module is assumed to have failed.
47+
The event will not be accepted but is not treated as explicitly rejected, either.
48+
An HTTP request causing the module check will likely result in a 500 Internal
49+
Server Error.
50+
51+
When the boolean returned by the module is `False`, the event is rejected.
52+
(Module developers should not use exceptions for rejection.)
53+
4654
Note that replacing the event only works for events sent by local users, not for events
4755
received over federation.
4856

synapse/api/errors.py

+7
Original file line numberDiff line numberDiff line change
@@ -596,3 +596,10 @@ class ShadowBanError(Exception):
596596
597597
This should be caught and a proper "fake" success response sent to the user.
598598
"""
599+
600+
601+
class ModuleFailedException(Exception):
602+
"""
603+
Raised when a module API callback fails, for example because it raised an
604+
exception.
605+
"""

synapse/events/third_party_rules.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import logging
1515
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple
1616

17-
from synapse.api.errors import SynapseError
17+
from synapse.api.errors import ModuleFailedException, SynapseError
1818
from synapse.events import EventBase
1919
from synapse.events.snapshot import EventContext
2020
from synapse.types import Requester, StateMap
@@ -233,9 +233,10 @@ async def check_event_allowed(
233233
# This module callback needs a rework so that hacks such as
234234
# this one are not necessary.
235235
raise e
236-
except Exception as e:
237-
logger.warning("Failed to run module API callback %s: %s", callback, e)
238-
continue
236+
except Exception:
237+
raise ModuleFailedException(
238+
"Failed to run `check_event_allowed` module API callback"
239+
)
239240

240241
# Return if the event shouldn't be allowed or if the module came up with a
241242
# replacement dict for the event.

synapse/federation/transport/client.py

+3
Original file line numberDiff line numberDiff line change
@@ -1310,14 +1310,17 @@ def __init__(self, room_version: RoomVersion, v1_api: bool):
13101310
self._coro_state = ijson.items_coro(
13111311
_event_list_parser(room_version, self._response.state),
13121312
prefix + "state.item",
1313+
use_float=True,
13131314
)
13141315
self._coro_auth = ijson.items_coro(
13151316
_event_list_parser(room_version, self._response.auth_events),
13161317
prefix + "auth_chain.item",
1318+
use_float=True,
13171319
)
13181320
self._coro_event = ijson.kvitems_coro(
13191321
_event_parser(self._response.event_dict),
13201322
prefix + "org.matrix.msc3083.v2.event",
1323+
use_float=True,
13211324
)
13221325

13231326
def write(self, data: bytes) -> int:

synapse/rest/media/v1/media_repository.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ async def get_local_media(
215215
self.mark_recently_accessed(None, media_id)
216216

217217
media_type = media_info["media_type"]
218+
if not media_type:
219+
media_type = "application/octet-stream"
218220
media_length = media_info["media_length"]
219221
upload_name = name if name else media_info["upload_name"]
220222
url_cache = media_info["url_cache"]
@@ -333,6 +335,9 @@ async def _get_remote_media_impl(
333335
logger.info("Media is quarantined")
334336
raise NotFoundError()
335337

338+
if not media_info["media_type"]:
339+
media_info["media_type"] = "application/octet-stream"
340+
336341
responder = await self.media_storage.fetch_media(file_info)
337342
if responder:
338343
return responder, media_info
@@ -354,6 +359,8 @@ async def _get_remote_media_impl(
354359
raise e
355360

356361
file_id = media_info["filesystem_id"]
362+
if not media_info["media_type"]:
363+
media_info["media_type"] = "application/octet-stream"
357364
file_info = FileInfo(server_name, file_id)
358365

359366
# We generate thumbnails even if another process downloaded the media
@@ -445,7 +452,10 @@ async def _download_remote_file(
445452

446453
await finish()
447454

448-
media_type = headers[b"Content-Type"][0].decode("ascii")
455+
if b"Content-Type" in headers:
456+
media_type = headers[b"Content-Type"][0].decode("ascii")
457+
else:
458+
media_type = "application/octet-stream"
449459
upload_name = get_filename_from_headers(headers)
450460
time_now_ms = self.clock.time_msec()
451461

synapse/rest/media/v1/upload_resource.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ async def _async_render_POST(self, request: SynapseRequest) -> None:
8080
assert content_type_headers # for mypy
8181
media_type = content_type_headers[0].decode("ascii")
8282
else:
83-
raise SynapseError(msg="Upload request missing 'Content-Type'", code=400)
83+
media_type = "application/octet-stream"
8484

8585
# if headers.hasHeader(b"Content-Disposition"):
8686
# disposition = headers.getRawHeaders(b"Content-Disposition")[0]

synapse/util/async_helpers.py

+18-16
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
Any,
2323
Awaitable,
2424
Callable,
25+
Collection,
2526
Dict,
2627
Generic,
2728
Hashable,
2829
Iterable,
29-
List,
3030
Optional,
3131
Set,
3232
TypeVar,
@@ -76,12 +76,17 @@ class ObservableDeferred(Generic[_T]):
7676
def __init__(self, deferred: "defer.Deferred[_T]", consumeErrors: bool = False):
7777
object.__setattr__(self, "_deferred", deferred)
7878
object.__setattr__(self, "_result", None)
79-
object.__setattr__(self, "_observers", set())
79+
object.__setattr__(self, "_observers", [])
8080

8181
def callback(r):
8282
object.__setattr__(self, "_result", (True, r))
83-
while self._observers:
84-
observer = self._observers.pop()
83+
84+
# once we have set _result, no more entries will be added to _observers,
85+
# so it's safe to replace it with the empty tuple.
86+
observers = self._observers
87+
object.__setattr__(self, "_observers", ())
88+
89+
for observer in observers:
8590
try:
8691
observer.callback(r)
8792
except Exception as e:
@@ -95,12 +100,16 @@ def callback(r):
95100

96101
def errback(f):
97102
object.__setattr__(self, "_result", (False, f))
98-
while self._observers:
103+
104+
# once we have set _result, no more entries will be added to _observers,
105+
# so it's safe to replace it with the empty tuple.
106+
observers = self._observers
107+
object.__setattr__(self, "_observers", ())
108+
109+
for observer in observers:
99110
# This is a little bit of magic to correctly propagate stack
100111
# traces when we `await` on one of the observer deferreds.
101112
f.value.__failure__ = f
102-
103-
observer = self._observers.pop()
104113
try:
105114
observer.errback(f)
106115
except Exception as e:
@@ -127,20 +136,13 @@ def observe(self) -> "defer.Deferred[_T]":
127136
"""
128137
if not self._result:
129138
d: "defer.Deferred[_T]" = defer.Deferred()
130-
131-
def remove(r):
132-
self._observers.discard(d)
133-
return r
134-
135-
d.addBoth(remove)
136-
137-
self._observers.add(d)
139+
self._observers.append(d)
138140
return d
139141
else:
140142
success, res = self._result
141143
return defer.succeed(res) if success else defer.fail(res)
142144

143-
def observers(self) -> "List[defer.Deferred[_T]]":
145+
def observers(self) -> "Collection[defer.Deferred[_T]]":
144146
return self._observers
145147

146148
def has_called(self) -> bool:

tests/rest/admin/test_room.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def test_block_room_and_not_purge(self):
263263
# Assert one user in room
264264
self._is_member(room_id=self.room_id, user_id=self.other_user)
265265

266-
body = json.dumps({"block": False, "purge": False})
266+
body = json.dumps({"block": True, "purge": False})
267267

268268
channel = self.make_request(
269269
"DELETE",
@@ -280,7 +280,7 @@ def test_block_room_and_not_purge(self):
280280

281281
with self.assertRaises(AssertionError):
282282
self._is_purged(self.room_id)
283-
self._is_blocked(self.room_id, expect=False)
283+
self._is_blocked(self.room_id, expect=True)
284284
self._has_no_members(self.room_id)
285285

286286
def test_shutdown_room_consent(self):

tests/rest/client/test_third_party_rules.py

+3-13
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,9 @@ async def check(ev: EventBase, state):
216216
{"x": "x"},
217217
access_token=self.tok,
218218
)
219-
# check_event_allowed has some error handling, so it shouldn't 500 just because a
220-
# module did something bad.
221-
self.assertEqual(channel.code, 200, channel.result)
222-
event_id = channel.json_body["event_id"]
223-
224-
channel = self.make_request(
225-
"GET",
226-
"/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id),
227-
access_token=self.tok,
228-
)
229-
self.assertEqual(channel.code, 200, channel.result)
230-
ev = channel.json_body
231-
self.assertEqual(ev["content"]["x"], "x")
219+
# Because check_event_allowed raises an exception, it leads to a
220+
# 500 Internal Server Error
221+
self.assertEqual(channel.code, 500, channel.result)
232222

233223
def test_modify_event(self):
234224
"""The module can return a modified version of the event"""

tests/rest/media/v1/test_media_storage.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def prepare(self, reactor, clock, hs):
248248

249249
self.media_id = "example.com/12345"
250250

251-
def _req(self, content_disposition):
251+
def _req(self, content_disposition, include_content_type=True):
252252

253253
channel = make_request(
254254
self.reactor,
@@ -271,8 +271,11 @@ def _req(self, content_disposition):
271271

272272
headers = {
273273
b"Content-Length": [b"%d" % (len(self.test_image.data))],
274-
b"Content-Type": [self.test_image.content_type],
275274
}
275+
276+
if include_content_type:
277+
headers[b"Content-Type"] = [self.test_image.content_type]
278+
276279
if content_disposition:
277280
headers[b"Content-Disposition"] = [content_disposition]
278281

@@ -285,6 +288,17 @@ def _req(self, content_disposition):
285288

286289
return channel
287290

291+
def test_handle_missing_content_type(self):
292+
channel = self._req(
293+
b"inline; filename=out" + self.test_image.extension,
294+
include_content_type=False,
295+
)
296+
headers = channel.headers
297+
self.assertEqual(channel.code, 200)
298+
self.assertEqual(
299+
headers.getRawHeaders(b"Content-Type"), [b"application/octet-stream"]
300+
)
301+
288302
def test_disposition_filename_ascii(self):
289303
"""
290304
If the filename is filename=<ascii> then Synapse will decode it as an

tests/util/caches/test_deferred_cache.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ def check1(r):
4747
self.assertTrue(set_d.called)
4848
return r
4949

50-
# TODO: Actually ObservableDeferred *doesn't* run its tests in order on py3.8.
51-
# maybe we should fix that?
52-
# get_d.addCallback(check1)
50+
get_d.addCallback(check1)
5351

5452
# now fire off all the deferreds
5553
origin_d.callback(99)

tests/util/test_async_utils.py tests/util/test_async_helpers.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,78 @@
2121
PreserveLoggingContext,
2222
current_context,
2323
)
24-
from synapse.util.async_helpers import timeout_deferred
24+
from synapse.util.async_helpers import ObservableDeferred, timeout_deferred
2525

2626
from tests.unittest import TestCase
2727

2828

29+
class ObservableDeferredTest(TestCase):
30+
def test_succeed(self):
31+
origin_d = Deferred()
32+
observable = ObservableDeferred(origin_d)
33+
34+
observer1 = observable.observe()
35+
observer2 = observable.observe()
36+
37+
self.assertFalse(observer1.called)
38+
self.assertFalse(observer2.called)
39+
40+
# check the first observer is called first
41+
def check_called_first(res):
42+
self.assertFalse(observer2.called)
43+
return res
44+
45+
observer1.addBoth(check_called_first)
46+
47+
# store the results
48+
results = [None, None]
49+
50+
def check_val(res, idx):
51+
results[idx] = res
52+
return res
53+
54+
observer1.addCallback(check_val, 0)
55+
observer2.addCallback(check_val, 1)
56+
57+
origin_d.callback(123)
58+
self.assertEqual(results[0], 123, "observer 1 callback result")
59+
self.assertEqual(results[1], 123, "observer 2 callback result")
60+
61+
def test_failure(self):
62+
origin_d = Deferred()
63+
observable = ObservableDeferred(origin_d, consumeErrors=True)
64+
65+
observer1 = observable.observe()
66+
observer2 = observable.observe()
67+
68+
self.assertFalse(observer1.called)
69+
self.assertFalse(observer2.called)
70+
71+
# check the first observer is called first
72+
def check_called_first(res):
73+
self.assertFalse(observer2.called)
74+
return res
75+
76+
observer1.addBoth(check_called_first)
77+
78+
# store the results
79+
results = [None, None]
80+
81+
def check_val(res, idx):
82+
results[idx] = res
83+
return None
84+
85+
observer1.addErrback(check_val, 0)
86+
observer2.addErrback(check_val, 1)
87+
88+
try:
89+
raise Exception("gah!")
90+
except Exception as e:
91+
origin_d.errback(e)
92+
self.assertEqual(str(results[0].value), "gah!", "observer 1 errback result")
93+
self.assertEqual(str(results[1].value), "gah!", "observer 2 errback result")
94+
95+
2996
class TimeoutDeferredTest(TestCase):
3097
def setUp(self):
3198
self.clock = Clock()

0 commit comments

Comments
 (0)