Signed-off-by: Nicolas Werner <n.werner@famedly.com> Co-authored-by: Nicolas Werner <n.werner@famedly.com> Co-authored-by: Nicolas Werner <89468146+nico-famedly@users.noreply.github.com> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>tags/v1.89.0rc1
@@ -0,0 +1 @@ | |||
Implement [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814), dehydrated devices v2/shrivelled sessions and move [MSC2697](https://github.com/matrix-org/matrix-spec-proposals/pull/2697) behind a config flag. Contributed by Nico from Famedly and H-Shay. |
@@ -247,6 +247,27 @@ class ExperimentalConfig(Config): | |||
# MSC3026 (busy presence state) | |||
self.msc3026_enabled: bool = experimental.get("msc3026_enabled", False) | |||
# MSC2697 (device dehydration) | |||
# Enabled by default since this option was added after adding the feature. | |||
# It is not recommended that both MSC2697 and MSC3814 both be enabled at | |||
# once. | |||
self.msc2697_enabled: bool = experimental.get("msc2697_enabled", True) | |||
# MSC3814 (dehydrated devices with SSSS) | |||
# This is an alternative method to achieve the same goals as MSC2697. | |||
# It is not recommended that both MSC2697 and MSC3814 both be enabled at | |||
# once. | |||
self.msc3814_enabled: bool = experimental.get("msc3814_enabled", False) | |||
if self.msc2697_enabled and self.msc3814_enabled: | |||
raise ConfigError( | |||
"MSC2697 and MSC3814 should not both be enabled.", | |||
( | |||
"experimental_features", | |||
"msc3814_enabled", | |||
), | |||
) | |||
# MSC3244 (room version capabilities) | |||
self.msc3244_enabled: bool = experimental.get("msc3244_enabled", True) | |||
@@ -653,6 +653,7 @@ class DeviceHandler(DeviceWorkerHandler): | |||
async def store_dehydrated_device( | |||
self, | |||
user_id: str, | |||
device_id: Optional[str], | |||
device_data: JsonDict, | |||
initial_device_display_name: Optional[str] = None, | |||
) -> str: | |||
@@ -661,6 +662,7 @@ class DeviceHandler(DeviceWorkerHandler): | |||
Args: | |||
user_id: the user that we are storing the device for | |||
device_id: device id supplied by client | |||
device_data: the dehydrated device information | |||
initial_device_display_name: The display name to use for the device | |||
Returns: | |||
@@ -668,7 +670,7 @@ class DeviceHandler(DeviceWorkerHandler): | |||
""" | |||
device_id = await self.check_device_registered( | |||
user_id, | |||
None, | |||
device_id, | |||
initial_device_display_name, | |||
) | |||
old_device_id = await self.store.store_dehydrated_device( | |||
@@ -13,10 +13,11 @@ | |||
# limitations under the License. | |||
import logging | |||
from typing import TYPE_CHECKING, Any, Dict | |||
from http import HTTPStatus | |||
from typing import TYPE_CHECKING, Any, Dict, Optional | |||
from synapse.api.constants import EduTypes, EventContentFields, ToDeviceEventTypes | |||
from synapse.api.errors import SynapseError | |||
from synapse.api.errors import Codes, SynapseError | |||
from synapse.api.ratelimiting import Ratelimiter | |||
from synapse.logging.context import run_in_background | |||
from synapse.logging.opentracing import ( | |||
@@ -48,6 +49,9 @@ class DeviceMessageHandler: | |||
self.store = hs.get_datastores().main | |||
self.notifier = hs.get_notifier() | |||
self.is_mine = hs.is_mine | |||
if hs.config.experimental.msc3814_enabled: | |||
self.event_sources = hs.get_event_sources() | |||
self.device_handler = hs.get_device_handler() | |||
# We only need to poke the federation sender explicitly if its on the | |||
# same instance. Other federation sender instances will get notified by | |||
@@ -303,3 +307,103 @@ class DeviceMessageHandler: | |||
# Enqueue a new federation transaction to send the new | |||
# device messages to each remote destination. | |||
self.federation_sender.send_device_messages(destination) | |||
async def get_events_for_dehydrated_device( | |||
self, | |||
requester: Requester, | |||
device_id: str, | |||
since_token: Optional[str], | |||
limit: int, | |||
) -> JsonDict: | |||
"""Fetches up to `limit` events sent to `device_id` starting from `since_token` | |||
and returns the new since token. If there are no more messages, returns an empty | |||
array. | |||
Args: | |||
requester: the user requesting the messages | |||
device_id: ID of the dehydrated device | |||
since_token: stream id to start from when fetching messages | |||
limit: the number of messages to fetch | |||
Returns: | |||
A dict containing the to-device messages, as well as a token that the client | |||
can provide in the next call to fetch the next batch of messages | |||
""" | |||
user_id = requester.user.to_string() | |||
# only allow fetching messages for the dehydrated device id currently associated | |||
# with the user | |||
dehydrated_device = await self.device_handler.get_dehydrated_device(user_id) | |||
if dehydrated_device is None: | |||
raise SynapseError( | |||
HTTPStatus.FORBIDDEN, | |||
"No dehydrated device exists", | |||
Codes.FORBIDDEN, | |||
) | |||
dehydrated_device_id, _ = dehydrated_device | |||
if device_id != dehydrated_device_id: | |||
raise SynapseError( | |||
HTTPStatus.FORBIDDEN, | |||
"You may only fetch messages for your dehydrated device", | |||
Codes.FORBIDDEN, | |||
) | |||
since_stream_id = 0 | |||
if since_token: | |||
if not since_token.startswith("d"): | |||
raise SynapseError( | |||
HTTPStatus.BAD_REQUEST, | |||
"from parameter %r has an invalid format" % (since_token,), | |||
errcode=Codes.INVALID_PARAM, | |||
) | |||
try: | |||
since_stream_id = int(since_token[1:]) | |||
except Exception: | |||
raise SynapseError( | |||
HTTPStatus.BAD_REQUEST, | |||
"from parameter %r has an invalid format" % (since_token,), | |||
errcode=Codes.INVALID_PARAM, | |||
) | |||
# if we have a since token, delete any to-device messages before that token | |||
# (since we now know that the device has received them) | |||
deleted = await self.store.delete_messages_for_device( | |||
user_id, device_id, since_stream_id | |||
) | |||
logger.debug( | |||
"Deleted %d to-device messages up to %d for user_id %s device_id %s", | |||
deleted, | |||
since_stream_id, | |||
user_id, | |||
device_id, | |||
) | |||
to_token = self.event_sources.get_current_token().to_device_key | |||
messages, stream_id = await self.store.get_messages_for_device( | |||
user_id, device_id, since_stream_id, to_token, limit | |||
) | |||
for message in messages: | |||
# Remove the message id before sending to client | |||
message_id = message.pop("message_id", None) | |||
if message_id: | |||
set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id) | |||
logger.debug( | |||
"Returning %d to-device messages between %d and %d (current token: %d) for " | |||
"dehydrated device %s, user_id %s", | |||
len(messages), | |||
since_stream_id, | |||
stream_id, | |||
to_token, | |||
device_id, | |||
user_id, | |||
) | |||
return { | |||
"events": messages, | |||
"next_batch": f"d{stream_id}", | |||
} |
@@ -14,19 +14,22 @@ | |||
# limitations under the License. | |||
import logging | |||
from http import HTTPStatus | |||
from typing import TYPE_CHECKING, List, Optional, Tuple | |||
from pydantic import Extra, StrictStr | |||
from synapse.api import errors | |||
from synapse.api.errors import NotFoundError, UnrecognizedRequestError | |||
from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError | |||
from synapse.handlers.device import DeviceHandler | |||
from synapse.http.server import HttpServer | |||
from synapse.http.servlet import ( | |||
RestServlet, | |||
parse_and_validate_json_object_from_request, | |||
parse_integer, | |||
) | |||
from synapse.http.site import SynapseRequest | |||
from synapse.replication.http.devices import ReplicationUploadKeysForUserRestServlet | |||
from synapse.rest.client._base import client_patterns, interactive_auth_handler | |||
from synapse.rest.client.models import AuthenticationData | |||
from synapse.rest.models import RequestBodyModel | |||
@@ -229,6 +232,8 @@ class DehydratedDeviceDataModel(RequestBodyModel): | |||
class DehydratedDeviceServlet(RestServlet): | |||
"""Retrieve or store a dehydrated device. | |||
Implements either MSC2697 or MSC3814. | |||
GET /org.matrix.msc2697.v2/dehydrated_device | |||
HTTP/1.1 200 OK | |||
@@ -261,9 +266,7 @@ class DehydratedDeviceServlet(RestServlet): | |||
""" | |||
PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device$", releases=()) | |||
def __init__(self, hs: "HomeServer"): | |||
def __init__(self, hs: "HomeServer", msc2697: bool = True): | |||
super().__init__() | |||
self.hs = hs | |||
self.auth = hs.get_auth() | |||
@@ -271,6 +274,13 @@ class DehydratedDeviceServlet(RestServlet): | |||
assert isinstance(handler, DeviceHandler) | |||
self.device_handler = handler | |||
self.PATTERNS = client_patterns( | |||
"/org.matrix.msc2697.v2/dehydrated_device$" | |||
if msc2697 | |||
else "/org.matrix.msc3814.v1/dehydrated_device$", | |||
releases=(), | |||
) | |||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
dehydrated_device = await self.device_handler.get_dehydrated_device( | |||
@@ -293,6 +303,7 @@ class DehydratedDeviceServlet(RestServlet): | |||
device_id = await self.device_handler.store_dehydrated_device( | |||
requester.user.to_string(), | |||
None, | |||
submission.device_data.dict(), | |||
submission.initial_device_display_name, | |||
) | |||
@@ -347,6 +358,210 @@ class ClaimDehydratedDeviceServlet(RestServlet): | |||
return 200, result | |||
class DehydratedDeviceEventsServlet(RestServlet): | |||
PATTERNS = client_patterns( | |||
"/org.matrix.msc3814.v1/dehydrated_device/(?P<device_id>[^/]*)/events$", | |||
releases=(), | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.message_handler = hs.get_device_message_handler() | |||
self.auth = hs.get_auth() | |||
self.store = hs.get_datastores().main | |||
class PostBody(RequestBodyModel): | |||
next_batch: Optional[StrictStr] | |||
async def on_POST( | |||
self, request: SynapseRequest, device_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
next_batch = parse_and_validate_json_object_from_request( | |||
request, self.PostBody | |||
).next_batch | |||
limit = parse_integer(request, "limit", 100) | |||
msgs = await self.message_handler.get_events_for_dehydrated_device( | |||
requester=requester, | |||
device_id=device_id, | |||
since_token=next_batch, | |||
limit=limit, | |||
) | |||
return 200, msgs | |||
class DehydratedDeviceV2Servlet(RestServlet): | |||
"""Upload, retrieve, or delete a dehydrated device. | |||
GET /org.matrix.msc3814.v1/dehydrated_device | |||
HTTP/1.1 200 OK | |||
Content-Type: application/json | |||
{ | |||
"device_id": "dehydrated_device_id", | |||
"device_data": { | |||
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", | |||
"account": "dehydrated_device" | |||
} | |||
} | |||
PUT /org.matrix.msc3814.v1/dehydrated_device | |||
Content-Type: application/json | |||
{ | |||
"device_id": "dehydrated_device_id", | |||
"device_data": { | |||
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", | |||
"account": "dehydrated_device" | |||
}, | |||
"device_keys": { | |||
"user_id": "<user_id>", | |||
"device_id": "<device_id>", | |||
"valid_until_ts": <millisecond_timestamp>, | |||
"algorithms": [ | |||
"m.olm.curve25519-aes-sha2", | |||
] | |||
"keys": { | |||
"<algorithm>:<device_id>": "<key_base64>", | |||
}, | |||
"signatures:" { | |||
"<user_id>" { | |||
"<algorithm>:<device_id>": "<signature_base64>" | |||
} | |||
} | |||
}, | |||
"fallback_keys": { | |||
"<algorithm>:<device_id>": "<key_base64>", | |||
"signed_<algorithm>:<device_id>": { | |||
"fallback": true, | |||
"key": "<key_base64>", | |||
"signatures": { | |||
"<user_id>": { | |||
"<algorithm>:<device_id>": "<key_base64>" | |||
} | |||
} | |||
} | |||
} | |||
"one_time_keys": { | |||
"<algorithm>:<key_id>": "<key_base64>" | |||
}, | |||
} | |||
HTTP/1.1 200 OK | |||
Content-Type: application/json | |||
{ | |||
"device_id": "dehydrated_device_id" | |||
} | |||
DELETE /org.matrix.msc3814.v1/dehydrated_device | |||
HTTP/1.1 200 OK | |||
Content-Type: application/json | |||
{ | |||
"device_id": "dehydrated_device_id", | |||
} | |||
""" | |||
PATTERNS = [ | |||
*client_patterns("/org.matrix.msc3814.v1/dehydrated_device$", releases=()), | |||
] | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.hs = hs | |||
self.auth = hs.get_auth() | |||
handler = hs.get_device_handler() | |||
assert isinstance(handler, DeviceHandler) | |||
self.e2e_keys_handler = hs.get_e2e_keys_handler() | |||
self.device_handler = handler | |||
if hs.config.worker.worker_app is None: | |||
# if main process | |||
self.key_uploader = self.e2e_keys_handler.upload_keys_for_user | |||
else: | |||
# then a worker | |||
self.key_uploader = ReplicationUploadKeysForUserRestServlet.make_client(hs) | |||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
dehydrated_device = await self.device_handler.get_dehydrated_device( | |||
requester.user.to_string() | |||
) | |||
if dehydrated_device is not None: | |||
(device_id, device_data) = dehydrated_device | |||
result = {"device_id": device_id, "device_data": device_data} | |||
return 200, result | |||
else: | |||
raise errors.NotFoundError("No dehydrated device available") | |||
async def on_DELETE(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
dehydrated_device = await self.device_handler.get_dehydrated_device( | |||
requester.user.to_string() | |||
) | |||
if dehydrated_device is not None: | |||
(device_id, device_data) = dehydrated_device | |||
result = await self.device_handler.rehydrate_device( | |||
requester.user.to_string(), | |||
self.auth.get_access_token_from_request(request), | |||
device_id, | |||
) | |||
result = {"device_id": device_id} | |||
return 200, result | |||
else: | |||
raise errors.NotFoundError("No dehydrated device available") | |||
class PutBody(RequestBodyModel): | |||
device_data: DehydratedDeviceDataModel | |||
device_id: StrictStr | |||
initial_device_display_name: Optional[StrictStr] | |||
class Config: | |||
extra = Extra.allow | |||
async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
submission = parse_and_validate_json_object_from_request(request, self.PutBody) | |||
requester = await self.auth.get_user_by_req(request) | |||
user_id = requester.user.to_string() | |||
device_info = submission.dict() | |||
if "device_keys" not in device_info.keys(): | |||
raise SynapseError( | |||
HTTPStatus.BAD_REQUEST, | |||
"Device key(s) not found, these must be provided.", | |||
) | |||
# TODO: Those two operations, creating a device and storing the | |||
# device's keys should be atomic. | |||
device_id = await self.device_handler.store_dehydrated_device( | |||
requester.user.to_string(), | |||
submission.device_id, | |||
submission.device_data.dict(), | |||
submission.initial_device_display_name, | |||
) | |||
# TODO: Do we need to do something with the result here? | |||
await self.key_uploader( | |||
user_id=user_id, device_id=submission.device_id, keys=submission.dict() | |||
) | |||
return 200, {"device_id": device_id} | |||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: | |||
if ( | |||
hs.config.worker.worker_app is None | |||
@@ -354,7 +569,12 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: | |||
): | |||
DeleteDevicesRestServlet(hs).register(http_server) | |||
DevicesRestServlet(hs).register(http_server) | |||
if hs.config.worker.worker_app is None: | |||
DeviceRestServlet(hs).register(http_server) | |||
DehydratedDeviceServlet(hs).register(http_server) | |||
ClaimDehydratedDeviceServlet(hs).register(http_server) | |||
if hs.config.experimental.msc2697_enabled: | |||
DehydratedDeviceServlet(hs, msc2697=True).register(http_server) | |||
ClaimDehydratedDeviceServlet(hs).register(http_server) | |||
if hs.config.experimental.msc3814_enabled: | |||
DehydratedDeviceV2Servlet(hs).register(http_server) | |||
DehydratedDeviceEventsServlet(hs).register(http_server) |
@@ -17,15 +17,18 @@ | |||
from typing import Optional | |||
from unittest import mock | |||
from twisted.internet.defer import ensureDeferred | |||
from twisted.test.proto_helpers import MemoryReactor | |||
from synapse.api.constants import RoomEncryptionAlgorithms | |||
from synapse.api.errors import NotFoundError, SynapseError | |||
from synapse.appservice import ApplicationService | |||
from synapse.handlers.device import MAX_DEVICE_DISPLAY_NAME_LEN, DeviceHandler | |||
from synapse.rest import admin | |||
from synapse.rest.client import devices, login, register | |||
from synapse.server import HomeServer | |||
from synapse.storage.databases.main.appservice import _make_exclusive_regex | |||
from synapse.types import JsonDict | |||
from synapse.types import JsonDict, create_requester | |||
from synapse.util import Clock | |||
from tests import unittest | |||
@@ -399,11 +402,19 @@ class DeviceTestCase(unittest.HomeserverTestCase): | |||
class DehydrationTestCase(unittest.HomeserverTestCase): | |||
servlets = [ | |||
admin.register_servlets_for_client_rest_resource, | |||
login.register_servlets, | |||
register.register_servlets, | |||
devices.register_servlets, | |||
] | |||
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: | |||
hs = self.setup_test_homeserver("server") | |||
handler = hs.get_device_handler() | |||
assert isinstance(handler, DeviceHandler) | |||
self.handler = handler | |||
self.message_handler = hs.get_device_message_handler() | |||
self.registration = hs.get_registration_handler() | |||
self.auth = hs.get_auth() | |||
self.store = hs.get_datastores().main | |||
@@ -418,6 +429,7 @@ class DehydrationTestCase(unittest.HomeserverTestCase): | |||
stored_dehydrated_device_id = self.get_success( | |||
self.handler.store_dehydrated_device( | |||
user_id=user_id, | |||
device_id=None, | |||
device_data={"device_data": {"foo": "bar"}}, | |||
initial_device_display_name="dehydrated device", | |||
) | |||
@@ -481,3 +493,88 @@ class DehydrationTestCase(unittest.HomeserverTestCase): | |||
ret = self.get_success(self.handler.get_dehydrated_device(user_id=user_id)) | |||
self.assertIsNone(ret) | |||
@unittest.override_config( | |||
{"experimental_features": {"msc2697_enabled": False, "msc3814_enabled": True}} | |||
) | |||
def test_dehydrate_v2_and_fetch_events(self) -> None: | |||
user_id = "@boris:server" | |||
self.get_success(self.store.register_user(user_id, "foobar")) | |||
# First check if we can store and fetch a dehydrated device | |||
stored_dehydrated_device_id = self.get_success( | |||
self.handler.store_dehydrated_device( | |||
user_id=user_id, | |||
device_id=None, | |||
device_data={"device_data": {"foo": "bar"}}, | |||
initial_device_display_name="dehydrated device", | |||
) | |||
) | |||
device_info = self.get_success( | |||
self.handler.get_dehydrated_device(user_id=user_id) | |||
) | |||
assert device_info is not None | |||
retrieved_device_id, device_data = device_info | |||
self.assertEqual(retrieved_device_id, stored_dehydrated_device_id) | |||
self.assertEqual(device_data, {"device_data": {"foo": "bar"}}) | |||
# Create a new login for the user | |||
device_id, access_token, _expiration_time, _refresh_token = self.get_success( | |||
self.registration.register_device( | |||
user_id=user_id, | |||
device_id=None, | |||
initial_display_name="new device", | |||
) | |||
) | |||
requester = create_requester(user_id, device_id=device_id) | |||
# Fetching messages for a non-existing device should return an error | |||
self.get_failure( | |||
self.message_handler.get_events_for_dehydrated_device( | |||
requester=requester, | |||
device_id="not the right device ID", | |||
since_token=None, | |||
limit=10, | |||
), | |||
SynapseError, | |||
) | |||
# Send a message to the dehydrated device | |||
ensureDeferred( | |||
self.message_handler.send_device_message( | |||
requester=requester, | |||
message_type="test.message", | |||
messages={user_id: {stored_dehydrated_device_id: {"body": "foo"}}}, | |||
) | |||
) | |||
self.pump() | |||
# Fetch the message of the dehydrated device | |||
res = self.get_success( | |||
self.message_handler.get_events_for_dehydrated_device( | |||
requester=requester, | |||
device_id=stored_dehydrated_device_id, | |||
since_token=None, | |||
limit=10, | |||
) | |||
) | |||
self.assertTrue(len(res["next_batch"]) > 1) | |||
self.assertEqual(len(res["events"]), 1) | |||
self.assertEqual(res["events"][0]["content"]["body"], "foo") | |||
# Fetch the message of the dehydrated device again, which should return nothing | |||
# and delete the old messages | |||
res = self.get_success( | |||
self.message_handler.get_events_for_dehydrated_device( | |||
requester=requester, | |||
device_id=stored_dehydrated_device_id, | |||
since_token=res["next_batch"], | |||
limit=10, | |||
) | |||
) | |||
self.assertTrue(len(res["next_batch"]) > 1) | |||
self.assertEqual(len(res["events"]), 0) |
@@ -13,12 +13,14 @@ | |||
# limitations under the License. | |||
from http import HTTPStatus | |||
from twisted.internet.defer import ensureDeferred | |||
from twisted.test.proto_helpers import MemoryReactor | |||
from synapse.api.errors import NotFoundError | |||
from synapse.rest import admin, devices, room, sync | |||
from synapse.rest.client import account, login, register | |||
from synapse.rest.client import account, keys, login, register | |||
from synapse.server import HomeServer | |||
from synapse.types import JsonDict, create_requester | |||
from synapse.util import Clock | |||
from tests import unittest | |||
@@ -208,8 +210,13 @@ class DehydratedDeviceTestCase(unittest.HomeserverTestCase): | |||
login.register_servlets, | |||
register.register_servlets, | |||
devices.register_servlets, | |||
keys.register_servlets, | |||
] | |||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: | |||
self.registration = hs.get_registration_handler() | |||
self.message_handler = hs.get_device_message_handler() | |||
def test_PUT(self) -> None: | |||
"""Sanity-check that we can PUT a dehydrated device. | |||
@@ -226,7 +233,21 @@ class DehydratedDeviceTestCase(unittest.HomeserverTestCase): | |||
"device_data": { | |||
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", | |||
"account": "dehydrated_device", | |||
} | |||
}, | |||
"device_keys": { | |||
"user_id": "@alice:test", | |||
"device_id": "device1", | |||
"valid_until_ts": "80", | |||
"algorithms": [ | |||
"m.olm.curve25519-aes-sha2", | |||
], | |||
"keys": { | |||
"<algorithm>:<device_id>": "<key_base64>", | |||
}, | |||
"signatures": { | |||
"<user_id>": {"<algorithm>:<device_id>": "<signature_base64>"} | |||
}, | |||
}, | |||
}, | |||
access_token=token, | |||
shorthand=False, | |||
@@ -234,3 +255,128 @@ class DehydratedDeviceTestCase(unittest.HomeserverTestCase): | |||
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) | |||
device_id = channel.json_body.get("device_id") | |||
self.assertIsInstance(device_id, str) | |||
@unittest.override_config( | |||
{"experimental_features": {"msc2697_enabled": False, "msc3814_enabled": True}} | |||
) | |||
def test_dehydrate_msc3814(self) -> None: | |||
user = self.register_user("mikey", "pass") | |||
token = self.login(user, "pass", device_id="device1") | |||
content: JsonDict = { | |||
"device_data": { | |||
"algorithm": "m.dehydration.v1.olm", | |||
}, | |||
"device_id": "device1", | |||
"initial_device_display_name": "foo bar", | |||
"device_keys": { | |||
"user_id": "@mikey:test", | |||
"device_id": "device1", | |||
"valid_until_ts": "80", | |||
"algorithms": [ | |||
"m.olm.curve25519-aes-sha2", | |||
], | |||
"keys": { | |||
"<algorithm>:<device_id>": "<key_base64>", | |||
}, | |||
"signatures": { | |||
"<user_id>": {"<algorithm>:<device_id>": "<signature_base64>"} | |||
}, | |||
}, | |||
} | |||
channel = self.make_request( | |||
"PUT", | |||
"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", | |||
content=content, | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 200) | |||
device_id = channel.json_body.get("device_id") | |||
assert device_id is not None | |||
self.assertIsInstance(device_id, str) | |||
self.assertEqual("device1", device_id) | |||
# test that we can now GET the dehydrated device info | |||
channel = self.make_request( | |||
"GET", | |||
"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 200) | |||
returned_device_id = channel.json_body.get("device_id") | |||
self.assertEqual(returned_device_id, device_id) | |||
device_data = channel.json_body.get("device_data") | |||
expected_device_data = { | |||
"algorithm": "m.dehydration.v1.olm", | |||
} | |||
self.assertEqual(device_data, expected_device_data) | |||
# create another device for the user | |||
( | |||
new_device_id, | |||
_, | |||
_, | |||
_, | |||
) = self.get_success( | |||
self.registration.register_device( | |||
user_id=user, | |||
device_id=None, | |||
initial_display_name="new device", | |||
) | |||
) | |||
requester = create_requester(user, device_id=new_device_id) | |||
# Send a message to the dehydrated device | |||
ensureDeferred( | |||
self.message_handler.send_device_message( | |||
requester=requester, | |||
message_type="test.message", | |||
messages={user: {device_id: {"body": "test_message"}}}, | |||
) | |||
) | |||
self.pump() | |||
# make sure we can fetch the message with our dehydrated device id | |||
channel = self.make_request( | |||
"POST", | |||
f"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/{device_id}/events", | |||
content={}, | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 200) | |||
expected_content = {"body": "test_message"} | |||
self.assertEqual(channel.json_body["events"][0]["content"], expected_content) | |||
next_batch_token = channel.json_body.get("next_batch") | |||
# fetch messages again and make sure that the message was deleted and we are returned an | |||
# empty array | |||
content = {"next_batch": next_batch_token} | |||
channel = self.make_request( | |||
"POST", | |||
f"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/{device_id}/events", | |||
content=content, | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 200) | |||
self.assertEqual(channel.json_body["events"], []) | |||
# make sure we can delete the dehydrated device | |||
channel = self.make_request( | |||
"DELETE", | |||
"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 200) | |||
# ...and after deleting it is no longer available | |||
channel = self.make_request( | |||
"GET", | |||
"_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", | |||
access_token=token, | |||
shorthand=False, | |||
) | |||
self.assertEqual(channel.code, 404) |