You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

576 lines
18 KiB

  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. # Copyright 2020 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from http import HTTPStatus
  17. from typing import TYPE_CHECKING, List, Optional, Tuple
  18. from synapse._pydantic_compat import HAS_PYDANTIC_V2
  19. if TYPE_CHECKING or HAS_PYDANTIC_V2:
  20. from pydantic.v1 import Extra, StrictStr
  21. else:
  22. from pydantic import Extra, StrictStr
  23. from synapse.api import errors
  24. from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError
  25. from synapse.handlers.device import DeviceHandler
  26. from synapse.http.server import HttpServer
  27. from synapse.http.servlet import (
  28. RestServlet,
  29. parse_and_validate_json_object_from_request,
  30. parse_integer,
  31. )
  32. from synapse.http.site import SynapseRequest
  33. from synapse.rest.client._base import client_patterns, interactive_auth_handler
  34. from synapse.rest.client.models import AuthenticationData
  35. from synapse.rest.models import RequestBodyModel
  36. from synapse.types import JsonDict
  37. if TYPE_CHECKING:
  38. from synapse.server import HomeServer
  39. logger = logging.getLogger(__name__)
  40. class DevicesRestServlet(RestServlet):
  41. PATTERNS = client_patterns("/devices$")
  42. CATEGORY = "Client API requests"
  43. def __init__(self, hs: "HomeServer"):
  44. super().__init__()
  45. self.hs = hs
  46. self.auth = hs.get_auth()
  47. self.device_handler = hs.get_device_handler()
  48. self._msc3852_enabled = hs.config.experimental.msc3852_enabled
  49. async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  50. requester = await self.auth.get_user_by_req(request, allow_guest=True)
  51. devices = await self.device_handler.get_devices_by_user(
  52. requester.user.to_string()
  53. )
  54. # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
  55. # removed from each device. If it is enabled, then the field name will
  56. # be replaced by the unstable identifier.
  57. #
  58. # When MSC3852 is accepted, this block of code can just be removed to
  59. # expose "last_seen_user_agent" to clients.
  60. for device in devices:
  61. last_seen_user_agent = device["last_seen_user_agent"]
  62. del device["last_seen_user_agent"]
  63. if self._msc3852_enabled:
  64. device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
  65. return 200, {"devices": devices}
  66. class DeleteDevicesRestServlet(RestServlet):
  67. """
  68. API for bulk deletion of devices. Accepts a JSON object with a devices
  69. key which lists the device_ids to delete. Requires user interactive auth.
  70. """
  71. PATTERNS = client_patterns("/delete_devices")
  72. def __init__(self, hs: "HomeServer"):
  73. super().__init__()
  74. self.hs = hs
  75. self.auth = hs.get_auth()
  76. handler = hs.get_device_handler()
  77. assert isinstance(handler, DeviceHandler)
  78. self.device_handler = handler
  79. self.auth_handler = hs.get_auth_handler()
  80. class PostBody(RequestBodyModel):
  81. auth: Optional[AuthenticationData]
  82. devices: List[StrictStr]
  83. @interactive_auth_handler
  84. async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  85. requester = await self.auth.get_user_by_req(request)
  86. try:
  87. body = parse_and_validate_json_object_from_request(request, self.PostBody)
  88. except errors.SynapseError as e:
  89. if e.errcode == errors.Codes.NOT_JSON:
  90. # TODO: Can/should we remove this fallback now?
  91. # deal with older clients which didn't pass a JSON dict
  92. # the same as those that pass an empty dict
  93. body = self.PostBody.parse_obj({})
  94. else:
  95. raise e
  96. await self.auth_handler.validate_user_via_ui_auth(
  97. requester,
  98. request,
  99. body.dict(exclude_unset=True),
  100. "remove device(s) from your account",
  101. # Users might call this multiple times in a row while cleaning up
  102. # devices, allow a single UI auth session to be re-used.
  103. can_skip_ui_auth=True,
  104. )
  105. await self.device_handler.delete_devices(
  106. requester.user.to_string(), body.devices
  107. )
  108. return 200, {}
  109. class DeviceRestServlet(RestServlet):
  110. PATTERNS = client_patterns("/devices/(?P<device_id>[^/]*)$")
  111. CATEGORY = "Client API requests"
  112. def __init__(self, hs: "HomeServer"):
  113. super().__init__()
  114. self.hs = hs
  115. self.auth = hs.get_auth()
  116. handler = hs.get_device_handler()
  117. assert isinstance(handler, DeviceHandler)
  118. self.device_handler = handler
  119. self.auth_handler = hs.get_auth_handler()
  120. self._msc3852_enabled = hs.config.experimental.msc3852_enabled
  121. self._msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
  122. async def on_GET(
  123. self, request: SynapseRequest, device_id: str
  124. ) -> Tuple[int, JsonDict]:
  125. requester = await self.auth.get_user_by_req(request, allow_guest=True)
  126. device = await self.device_handler.get_device(
  127. requester.user.to_string(), device_id
  128. )
  129. if device is None:
  130. raise NotFoundError("No device found")
  131. # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
  132. # removed from each device. If it is enabled, then the field name will
  133. # be replaced by the unstable identifier.
  134. #
  135. # When MSC3852 is accepted, this block of code can just be removed to
  136. # expose "last_seen_user_agent" to clients.
  137. last_seen_user_agent = device["last_seen_user_agent"]
  138. del device["last_seen_user_agent"]
  139. if self._msc3852_enabled:
  140. device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
  141. return 200, device
  142. class DeleteBody(RequestBodyModel):
  143. auth: Optional[AuthenticationData]
  144. @interactive_auth_handler
  145. async def on_DELETE(
  146. self, request: SynapseRequest, device_id: str
  147. ) -> Tuple[int, JsonDict]:
  148. if self._msc3861_oauth_delegation_enabled:
  149. raise UnrecognizedRequestError(code=404)
  150. requester = await self.auth.get_user_by_req(request)
  151. try:
  152. body = parse_and_validate_json_object_from_request(request, self.DeleteBody)
  153. except errors.SynapseError as e:
  154. if e.errcode == errors.Codes.NOT_JSON:
  155. # TODO: can/should we remove this fallback now?
  156. # deal with older clients which didn't pass a JSON dict
  157. # the same as those that pass an empty dict
  158. body = self.DeleteBody.parse_obj({})
  159. else:
  160. raise
  161. await self.auth_handler.validate_user_via_ui_auth(
  162. requester,
  163. request,
  164. body.dict(exclude_unset=True),
  165. "remove a device from your account",
  166. # Users might call this multiple times in a row while cleaning up
  167. # devices, allow a single UI auth session to be re-used.
  168. can_skip_ui_auth=True,
  169. )
  170. await self.device_handler.delete_devices(
  171. requester.user.to_string(), [device_id]
  172. )
  173. return 200, {}
  174. class PutBody(RequestBodyModel):
  175. display_name: Optional[StrictStr]
  176. async def on_PUT(
  177. self, request: SynapseRequest, device_id: str
  178. ) -> Tuple[int, JsonDict]:
  179. requester = await self.auth.get_user_by_req(request, allow_guest=True)
  180. body = parse_and_validate_json_object_from_request(request, self.PutBody)
  181. await self.device_handler.update_device(
  182. requester.user.to_string(), device_id, body.dict()
  183. )
  184. return 200, {}
  185. class DehydratedDeviceDataModel(RequestBodyModel):
  186. """JSON blob describing a dehydrated device to be stored.
  187. Expects other freeform fields. Use .dict() to access them.
  188. """
  189. class Config:
  190. extra = Extra.allow
  191. algorithm: StrictStr
  192. class DehydratedDeviceServlet(RestServlet):
  193. """Retrieve or store a dehydrated device.
  194. Implements MSC2697.
  195. GET /org.matrix.msc2697.v2/dehydrated_device
  196. HTTP/1.1 200 OK
  197. Content-Type: application/json
  198. {
  199. "device_id": "dehydrated_device_id",
  200. "device_data": {
  201. "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
  202. "account": "dehydrated_device"
  203. }
  204. }
  205. PUT /org.matrix.msc2697.v2/dehydrated_device
  206. Content-Type: application/json
  207. {
  208. "device_data": {
  209. "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
  210. "account": "dehydrated_device"
  211. }
  212. }
  213. HTTP/1.1 200 OK
  214. Content-Type: application/json
  215. {
  216. "device_id": "dehydrated_device_id"
  217. }
  218. """
  219. PATTERNS = client_patterns(
  220. "/org.matrix.msc2697.v2/dehydrated_device$",
  221. releases=(),
  222. )
  223. def __init__(self, hs: "HomeServer"):
  224. super().__init__()
  225. self.hs = hs
  226. self.auth = hs.get_auth()
  227. handler = hs.get_device_handler()
  228. assert isinstance(handler, DeviceHandler)
  229. self.device_handler = handler
  230. async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  231. requester = await self.auth.get_user_by_req(request)
  232. dehydrated_device = await self.device_handler.get_dehydrated_device(
  233. requester.user.to_string()
  234. )
  235. if dehydrated_device is not None:
  236. (device_id, device_data) = dehydrated_device
  237. result = {"device_id": device_id, "device_data": device_data}
  238. return 200, result
  239. else:
  240. raise errors.NotFoundError("No dehydrated device available")
  241. class PutBody(RequestBodyModel):
  242. device_data: DehydratedDeviceDataModel
  243. initial_device_display_name: Optional[StrictStr]
  244. async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  245. submission = parse_and_validate_json_object_from_request(request, self.PutBody)
  246. requester = await self.auth.get_user_by_req(request)
  247. device_id = await self.device_handler.store_dehydrated_device(
  248. requester.user.to_string(),
  249. None,
  250. submission.device_data.dict(),
  251. submission.initial_device_display_name,
  252. )
  253. return 200, {"device_id": device_id}
  254. class ClaimDehydratedDeviceServlet(RestServlet):
  255. """Claim a dehydrated device.
  256. POST /org.matrix.msc2697.v2/dehydrated_device/claim
  257. Content-Type: application/json
  258. {
  259. "device_id": "dehydrated_device_id"
  260. }
  261. HTTP/1.1 200 OK
  262. Content-Type: application/json
  263. {
  264. "success": true,
  265. }
  266. """
  267. PATTERNS = client_patterns(
  268. "/org.matrix.msc2697.v2/dehydrated_device/claim", releases=()
  269. )
  270. def __init__(self, hs: "HomeServer"):
  271. super().__init__()
  272. self.hs = hs
  273. self.auth = hs.get_auth()
  274. handler = hs.get_device_handler()
  275. assert isinstance(handler, DeviceHandler)
  276. self.device_handler = handler
  277. class PostBody(RequestBodyModel):
  278. device_id: StrictStr
  279. async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  280. requester = await self.auth.get_user_by_req(request)
  281. submission = parse_and_validate_json_object_from_request(request, self.PostBody)
  282. result = await self.device_handler.rehydrate_device(
  283. requester.user.to_string(),
  284. self.auth.get_access_token_from_request(request),
  285. submission.device_id,
  286. )
  287. return 200, result
  288. class DehydratedDeviceEventsServlet(RestServlet):
  289. PATTERNS = client_patterns(
  290. "/org.matrix.msc3814.v1/dehydrated_device/(?P<device_id>[^/]*)/events$",
  291. releases=(),
  292. )
  293. def __init__(self, hs: "HomeServer"):
  294. super().__init__()
  295. self.message_handler = hs.get_device_message_handler()
  296. self.auth = hs.get_auth()
  297. self.store = hs.get_datastores().main
  298. class PostBody(RequestBodyModel):
  299. next_batch: Optional[StrictStr]
  300. async def on_POST(
  301. self, request: SynapseRequest, device_id: str
  302. ) -> Tuple[int, JsonDict]:
  303. requester = await self.auth.get_user_by_req(request)
  304. next_batch = parse_and_validate_json_object_from_request(
  305. request, self.PostBody
  306. ).next_batch
  307. limit = parse_integer(request, "limit", 100)
  308. msgs = await self.message_handler.get_events_for_dehydrated_device(
  309. requester=requester,
  310. device_id=device_id,
  311. since_token=next_batch,
  312. limit=limit,
  313. )
  314. return 200, msgs
  315. class DehydratedDeviceV2Servlet(RestServlet):
  316. """Upload, retrieve, or delete a dehydrated device.
  317. GET /org.matrix.msc3814.v1/dehydrated_device
  318. HTTP/1.1 200 OK
  319. Content-Type: application/json
  320. {
  321. "device_id": "dehydrated_device_id",
  322. "device_data": {
  323. "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
  324. "account": "dehydrated_device"
  325. }
  326. }
  327. PUT /org.matrix.msc3814.v1/dehydrated_device
  328. Content-Type: application/json
  329. {
  330. "device_id": "dehydrated_device_id",
  331. "device_data": {
  332. "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
  333. "account": "dehydrated_device"
  334. },
  335. "device_keys": {
  336. "user_id": "<user_id>",
  337. "device_id": "<device_id>",
  338. "valid_until_ts": <millisecond_timestamp>,
  339. "algorithms": [
  340. "m.olm.curve25519-aes-sha2",
  341. ]
  342. "keys": {
  343. "<algorithm>:<device_id>": "<key_base64>",
  344. },
  345. "signatures:" {
  346. "<user_id>" {
  347. "<algorithm>:<device_id>": "<signature_base64>"
  348. }
  349. }
  350. },
  351. "fallback_keys": {
  352. "<algorithm>:<device_id>": "<key_base64>",
  353. "signed_<algorithm>:<device_id>": {
  354. "fallback": true,
  355. "key": "<key_base64>",
  356. "signatures": {
  357. "<user_id>": {
  358. "<algorithm>:<device_id>": "<key_base64>"
  359. }
  360. }
  361. }
  362. }
  363. "one_time_keys": {
  364. "<algorithm>:<key_id>": "<key_base64>"
  365. },
  366. }
  367. HTTP/1.1 200 OK
  368. Content-Type: application/json
  369. {
  370. "device_id": "dehydrated_device_id"
  371. }
  372. DELETE /org.matrix.msc3814.v1/dehydrated_device
  373. HTTP/1.1 200 OK
  374. Content-Type: application/json
  375. {
  376. "device_id": "dehydrated_device_id",
  377. }
  378. """
  379. PATTERNS = [
  380. *client_patterns("/org.matrix.msc3814.v1/dehydrated_device$", releases=()),
  381. ]
  382. def __init__(self, hs: "HomeServer"):
  383. super().__init__()
  384. self.hs = hs
  385. self.auth = hs.get_auth()
  386. handler = hs.get_device_handler()
  387. assert isinstance(handler, DeviceHandler)
  388. self.e2e_keys_handler = hs.get_e2e_keys_handler()
  389. self.device_handler = handler
  390. async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  391. requester = await self.auth.get_user_by_req(request)
  392. dehydrated_device = await self.device_handler.get_dehydrated_device(
  393. requester.user.to_string()
  394. )
  395. if dehydrated_device is not None:
  396. (device_id, device_data) = dehydrated_device
  397. result = {"device_id": device_id, "device_data": device_data}
  398. return 200, result
  399. else:
  400. raise errors.NotFoundError("No dehydrated device available")
  401. async def on_DELETE(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  402. requester = await self.auth.get_user_by_req(request)
  403. dehydrated_device = await self.device_handler.get_dehydrated_device(
  404. requester.user.to_string()
  405. )
  406. if dehydrated_device is not None:
  407. (device_id, device_data) = dehydrated_device
  408. await self.device_handler.delete_dehydrated_device(
  409. requester.user.to_string(), device_id
  410. )
  411. result = {"device_id": device_id}
  412. return 200, result
  413. else:
  414. raise errors.NotFoundError("No dehydrated device available")
  415. class PutBody(RequestBodyModel):
  416. device_data: DehydratedDeviceDataModel
  417. device_id: StrictStr
  418. initial_device_display_name: Optional[StrictStr]
  419. class Config:
  420. extra = Extra.allow
  421. async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  422. submission = parse_and_validate_json_object_from_request(request, self.PutBody)
  423. requester = await self.auth.get_user_by_req(request)
  424. user_id = requester.user.to_string()
  425. old_dehydrated_device = await self.device_handler.get_dehydrated_device(user_id)
  426. # if an old device exists, delete it before creating a new one
  427. if old_dehydrated_device:
  428. await self.device_handler.delete_dehydrated_device(
  429. user_id, old_dehydrated_device[0]
  430. )
  431. device_info = submission.dict()
  432. if "device_keys" not in device_info.keys():
  433. raise SynapseError(
  434. HTTPStatus.BAD_REQUEST,
  435. "Device key(s) not found, these must be provided.",
  436. )
  437. device_id = await self.device_handler.store_dehydrated_device(
  438. requester.user.to_string(),
  439. submission.device_id,
  440. submission.device_data.dict(),
  441. submission.initial_device_display_name,
  442. device_info,
  443. )
  444. return 200, {"device_id": device_id}
  445. def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
  446. if (
  447. hs.config.worker.worker_app is None
  448. and not hs.config.experimental.msc3861.enabled
  449. ):
  450. DeleteDevicesRestServlet(hs).register(http_server)
  451. DevicesRestServlet(hs).register(http_server)
  452. if hs.config.worker.worker_app is None:
  453. DeviceRestServlet(hs).register(http_server)
  454. if hs.config.experimental.msc2697_enabled:
  455. DehydratedDeviceServlet(hs).register(http_server)
  456. ClaimDehydratedDeviceServlet(hs).register(http_server)
  457. if hs.config.experimental.msc3814_enabled:
  458. DehydratedDeviceV2Servlet(hs).register(http_server)
  459. DehydratedDeviceEventsServlet(hs).register(http_server)