1# Copyright 2014-2016 OpenMarket Ltd 2# Copyright 2018 New Vector Ltd 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 16"""Contains exceptions and error codes.""" 17 18import logging 19import typing 20from http import HTTPStatus 21from typing import Any, Dict, List, Optional, Union 22 23from twisted.web import http 24 25from synapse.util import json_decoder 26 27if typing.TYPE_CHECKING: 28 from synapse.types import JsonDict 29 30logger = logging.getLogger(__name__) 31 32 33class Codes: 34 UNRECOGNIZED = "M_UNRECOGNIZED" 35 UNAUTHORIZED = "M_UNAUTHORIZED" 36 FORBIDDEN = "M_FORBIDDEN" 37 BAD_JSON = "M_BAD_JSON" 38 NOT_JSON = "M_NOT_JSON" 39 USER_IN_USE = "M_USER_IN_USE" 40 ROOM_IN_USE = "M_ROOM_IN_USE" 41 BAD_PAGINATION = "M_BAD_PAGINATION" 42 BAD_STATE = "M_BAD_STATE" 43 UNKNOWN = "M_UNKNOWN" 44 NOT_FOUND = "M_NOT_FOUND" 45 MISSING_TOKEN = "M_MISSING_TOKEN" 46 UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" 47 GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN" 48 LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" 49 CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" 50 CAPTCHA_INVALID = "M_CAPTCHA_INVALID" 51 MISSING_PARAM = "M_MISSING_PARAM" 52 INVALID_PARAM = "M_INVALID_PARAM" 53 TOO_LARGE = "M_TOO_LARGE" 54 EXCLUSIVE = "M_EXCLUSIVE" 55 THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" 56 THREEPID_IN_USE = "M_THREEPID_IN_USE" 57 THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND" 58 THREEPID_DENIED = "M_THREEPID_DENIED" 59 INVALID_USERNAME = "M_INVALID_USERNAME" 60 SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" 61 CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" 62 CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" 63 RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" 64 UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION" 65 INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION" 66 WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" 67 EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT" 68 PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT" 69 PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT" 70 PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE" 71 PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE" 72 PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL" 73 PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY" 74 WEAK_PASSWORD = "M_WEAK_PASSWORD" 75 INVALID_SIGNATURE = "M_INVALID_SIGNATURE" 76 USER_DEACTIVATED = "M_USER_DEACTIVATED" 77 BAD_ALIAS = "M_BAD_ALIAS" 78 # For restricted join rules. 79 UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN" 80 UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN" 81 82 83class CodeMessageException(RuntimeError): 84 """An exception with integer code and message string attributes. 85 86 Attributes: 87 code: HTTP error code 88 msg: string describing the error 89 """ 90 91 def __init__(self, code: Union[int, HTTPStatus], msg: str): 92 super().__init__("%d: %s" % (code, msg)) 93 94 # Some calls to this method pass instances of http.HTTPStatus for `code`. 95 # While HTTPStatus is a subclass of int, it has magic __str__ methods 96 # which emit `HTTPStatus.FORBIDDEN` when converted to a str, instead of `403`. 97 # This causes inconsistency in our log lines. 98 # 99 # To eliminate this behaviour, we convert them to their integer equivalents here. 100 self.code = int(code) 101 self.msg = msg 102 103 104class RedirectException(CodeMessageException): 105 """A pseudo-error indicating that we want to redirect the client to a different 106 location 107 108 Attributes: 109 cookies: a list of set-cookies values to add to the response. For example: 110 b"sessionId=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT" 111 """ 112 113 def __init__(self, location: bytes, http_code: int = http.FOUND): 114 """ 115 116 Args: 117 location: the URI to redirect to 118 http_code: the HTTP response code 119 """ 120 msg = "Redirect to %s" % (location.decode("utf-8"),) 121 super().__init__(code=http_code, msg=msg) 122 self.location = location 123 124 self.cookies: List[bytes] = [] 125 126 127class SynapseError(CodeMessageException): 128 """A base exception type for matrix errors which have an errcode and error 129 message (as well as an HTTP status code). 130 131 Attributes: 132 errcode: Matrix error code e.g 'M_FORBIDDEN' 133 """ 134 135 def __init__(self, code: int, msg: str, errcode: str = Codes.UNKNOWN): 136 """Constructs a synapse error. 137 138 Args: 139 code: The integer error code (an HTTP response code) 140 msg: The human-readable error message. 141 errcode: The matrix error code e.g 'M_FORBIDDEN' 142 """ 143 super().__init__(code, msg) 144 self.errcode = errcode 145 146 def error_dict(self) -> "JsonDict": 147 return cs_error(self.msg, self.errcode) 148 149 150class InvalidAPICallError(SynapseError): 151 """You called an existing API endpoint, but fed that endpoint 152 invalid or incomplete data.""" 153 154 def __init__(self, msg: str): 155 super().__init__(HTTPStatus.BAD_REQUEST, msg, Codes.BAD_JSON) 156 157 158class ProxiedRequestError(SynapseError): 159 """An error from a general matrix endpoint, eg. from a proxied Matrix API call. 160 161 Attributes: 162 errcode: Matrix error code e.g 'M_FORBIDDEN' 163 """ 164 165 def __init__( 166 self, 167 code: int, 168 msg: str, 169 errcode: str = Codes.UNKNOWN, 170 additional_fields: Optional[Dict] = None, 171 ): 172 super().__init__(code, msg, errcode) 173 if additional_fields is None: 174 self._additional_fields: Dict = {} 175 else: 176 self._additional_fields = dict(additional_fields) 177 178 def error_dict(self) -> "JsonDict": 179 return cs_error(self.msg, self.errcode, **self._additional_fields) 180 181 182class ConsentNotGivenError(SynapseError): 183 """The error returned to the client when the user has not consented to the 184 privacy policy. 185 """ 186 187 def __init__(self, msg: str, consent_uri: str): 188 """Constructs a ConsentNotGivenError 189 190 Args: 191 msg: The human-readable error message 192 consent_url: The URL where the user can give their consent 193 """ 194 super().__init__( 195 code=HTTPStatus.FORBIDDEN, msg=msg, errcode=Codes.CONSENT_NOT_GIVEN 196 ) 197 self._consent_uri = consent_uri 198 199 def error_dict(self) -> "JsonDict": 200 return cs_error(self.msg, self.errcode, consent_uri=self._consent_uri) 201 202 203class UserDeactivatedError(SynapseError): 204 """The error returned to the client when the user attempted to access an 205 authenticated endpoint, but the account has been deactivated. 206 """ 207 208 def __init__(self, msg: str): 209 """Constructs a UserDeactivatedError 210 211 Args: 212 msg: The human-readable error message 213 """ 214 super().__init__( 215 code=HTTPStatus.FORBIDDEN, msg=msg, errcode=Codes.USER_DEACTIVATED 216 ) 217 218 219class FederationDeniedError(SynapseError): 220 """An error raised when the server tries to federate with a server which 221 is not on its federation whitelist. 222 223 Attributes: 224 destination: The destination which has been denied 225 """ 226 227 def __init__(self, destination: Optional[str]): 228 """Raised by federation client or server to indicate that we are 229 are deliberately not attempting to contact a given server because it is 230 not on our federation whitelist. 231 232 Args: 233 destination: the domain in question 234 """ 235 236 self.destination = destination 237 238 super().__init__( 239 code=403, 240 msg="Federation denied with %s." % (self.destination,), 241 errcode=Codes.FORBIDDEN, 242 ) 243 244 245class InteractiveAuthIncompleteError(Exception): 246 """An error raised when UI auth is not yet complete 247 248 (This indicates we should return a 401 with 'result' as the body) 249 250 Attributes: 251 session_id: The ID of the ongoing interactive auth session. 252 result: the server response to the request, which should be 253 passed back to the client 254 """ 255 256 def __init__(self, session_id: str, result: "JsonDict"): 257 super().__init__("Interactive auth not yet complete") 258 self.session_id = session_id 259 self.result = result 260 261 262class UnrecognizedRequestError(SynapseError): 263 """An error indicating we don't understand the request you're trying to make""" 264 265 def __init__( 266 self, msg: str = "Unrecognized request", errcode: str = Codes.UNRECOGNIZED 267 ): 268 super().__init__(400, msg, errcode) 269 270 271class NotFoundError(SynapseError): 272 """An error indicating we can't find the thing you asked for""" 273 274 def __init__(self, msg: str = "Not found", errcode: str = Codes.NOT_FOUND): 275 super().__init__(404, msg, errcode=errcode) 276 277 278class AuthError(SynapseError): 279 """An error raised when there was a problem authorising an event, and at various 280 other poorly-defined times. 281 """ 282 283 def __init__(self, code: int, msg: str, errcode: str = Codes.FORBIDDEN): 284 super().__init__(code, msg, errcode) 285 286 287class InvalidClientCredentialsError(SynapseError): 288 """An error raised when there was a problem with the authorisation credentials 289 in a client request. 290 291 https://matrix.org/docs/spec/client_server/r0.5.0#using-access-tokens: 292 293 When credentials are required but missing or invalid, the HTTP call will 294 return with a status of 401 and the error code, M_MISSING_TOKEN or 295 M_UNKNOWN_TOKEN respectively. 296 """ 297 298 def __init__(self, msg: str, errcode: str): 299 super().__init__(code=401, msg=msg, errcode=errcode) 300 301 302class MissingClientTokenError(InvalidClientCredentialsError): 303 """Raised when we couldn't find the access token in a request""" 304 305 def __init__(self, msg: str = "Missing access token"): 306 super().__init__(msg=msg, errcode="M_MISSING_TOKEN") 307 308 309class InvalidClientTokenError(InvalidClientCredentialsError): 310 """Raised when we didn't understand the access token in a request""" 311 312 def __init__( 313 self, msg: str = "Unrecognised access token", soft_logout: bool = False 314 ): 315 super().__init__(msg=msg, errcode="M_UNKNOWN_TOKEN") 316 self._soft_logout = soft_logout 317 318 def error_dict(self) -> "JsonDict": 319 d = super().error_dict() 320 d["soft_logout"] = self._soft_logout 321 return d 322 323 324class ResourceLimitError(SynapseError): 325 """ 326 Any error raised when there is a problem with resource usage. 327 For instance, the monthly active user limit for the server has been exceeded 328 """ 329 330 def __init__( 331 self, 332 code: int, 333 msg: str, 334 errcode: str = Codes.RESOURCE_LIMIT_EXCEEDED, 335 admin_contact: Optional[str] = None, 336 limit_type: Optional[str] = None, 337 ): 338 self.admin_contact = admin_contact 339 self.limit_type = limit_type 340 super().__init__(code, msg, errcode=errcode) 341 342 def error_dict(self) -> "JsonDict": 343 return cs_error( 344 self.msg, 345 self.errcode, 346 admin_contact=self.admin_contact, 347 limit_type=self.limit_type, 348 ) 349 350 351class EventSizeError(SynapseError): 352 """An error raised when an event is too big.""" 353 354 def __init__(self, msg: str): 355 super().__init__(413, msg, Codes.TOO_LARGE) 356 357 358class LoginError(SynapseError): 359 """An error raised when there was a problem logging in.""" 360 361 362class StoreError(SynapseError): 363 """An error raised when there was a problem storing some data.""" 364 365 366class InvalidCaptchaError(SynapseError): 367 def __init__( 368 self, 369 code: int = 400, 370 msg: str = "Invalid captcha.", 371 error_url: Optional[str] = None, 372 errcode: str = Codes.CAPTCHA_INVALID, 373 ): 374 super().__init__(code, msg, errcode) 375 self.error_url = error_url 376 377 def error_dict(self) -> "JsonDict": 378 return cs_error(self.msg, self.errcode, error_url=self.error_url) 379 380 381class LimitExceededError(SynapseError): 382 """A client has sent too many requests and is being throttled.""" 383 384 def __init__( 385 self, 386 code: int = 429, 387 msg: str = "Too Many Requests", 388 retry_after_ms: Optional[int] = None, 389 errcode: str = Codes.LIMIT_EXCEEDED, 390 ): 391 super().__init__(code, msg, errcode) 392 self.retry_after_ms = retry_after_ms 393 394 def error_dict(self) -> "JsonDict": 395 return cs_error(self.msg, self.errcode, retry_after_ms=self.retry_after_ms) 396 397 398class RoomKeysVersionError(SynapseError): 399 """A client has tried to upload to a non-current version of the room_keys store""" 400 401 def __init__(self, current_version: str): 402 """ 403 Args: 404 current_version: the current version of the store they should have used 405 """ 406 super().__init__(403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION) 407 self.current_version = current_version 408 409 410class UnsupportedRoomVersionError(SynapseError): 411 """The client's request to create a room used a room version that the server does 412 not support.""" 413 414 def __init__(self, msg: str = "Homeserver does not support this room version"): 415 super().__init__( 416 code=400, 417 msg=msg, 418 errcode=Codes.UNSUPPORTED_ROOM_VERSION, 419 ) 420 421 422class ThreepidValidationError(SynapseError): 423 """An error raised when there was a problem authorising an event.""" 424 425 def __init__(self, msg: str, errcode: str = Codes.FORBIDDEN): 426 super().__init__(400, msg, errcode) 427 428 429class IncompatibleRoomVersionError(SynapseError): 430 """A server is trying to join a room whose version it does not support. 431 432 Unlike UnsupportedRoomVersionError, it is specific to the case of the make_join 433 failing. 434 """ 435 436 def __init__(self, room_version: str): 437 super().__init__( 438 code=400, 439 msg="Your homeserver does not support the features required to " 440 "interact with this room", 441 errcode=Codes.INCOMPATIBLE_ROOM_VERSION, 442 ) 443 444 self._room_version = room_version 445 446 def error_dict(self) -> "JsonDict": 447 return cs_error(self.msg, self.errcode, room_version=self._room_version) 448 449 450class PasswordRefusedError(SynapseError): 451 """A password has been refused, either during password reset/change or registration.""" 452 453 def __init__( 454 self, 455 msg: str = "This password doesn't comply with the server's policy", 456 errcode: str = Codes.WEAK_PASSWORD, 457 ): 458 super().__init__( 459 code=400, 460 msg=msg, 461 errcode=errcode, 462 ) 463 464 465class RequestSendFailed(RuntimeError): 466 """Sending a HTTP request over federation failed due to not being able to 467 talk to the remote server for some reason. 468 469 This exception is used to differentiate "expected" errors that arise due to 470 networking (e.g. DNS failures, connection timeouts etc), versus unexpected 471 errors (like programming errors). 472 """ 473 474 def __init__(self, inner_exception: BaseException, can_retry: bool): 475 super().__init__( 476 "Failed to send request: %s: %s" 477 % (type(inner_exception).__name__, inner_exception) 478 ) 479 self.inner_exception = inner_exception 480 self.can_retry = can_retry 481 482 483def cs_error(msg: str, code: str = Codes.UNKNOWN, **kwargs: Any) -> "JsonDict": 484 """Utility method for constructing an error response for client-server 485 interactions. 486 487 Args: 488 msg: The error message. 489 code: The error code. 490 kwargs: Additional keys to add to the response. 491 Returns: 492 A dict representing the error response JSON. 493 """ 494 err = {"error": msg, "errcode": code} 495 for key, value in kwargs.items(): 496 err[key] = value 497 return err 498 499 500class FederationError(RuntimeError): 501 """This class is used to inform remote homeservers about erroneous 502 PDUs they sent us. 503 504 FATAL: The remote server could not interpret the source event. 505 (e.g., it was missing a required field) 506 ERROR: The remote server interpreted the event, but it failed some other 507 check (e.g. auth) 508 WARN: The remote server accepted the event, but believes some part of it 509 is wrong (e.g., it referred to an invalid event) 510 """ 511 512 def __init__( 513 self, 514 level: str, 515 code: int, 516 reason: str, 517 affected: str, 518 source: Optional[str] = None, 519 ): 520 if level not in ["FATAL", "ERROR", "WARN"]: 521 raise ValueError("Level is not valid: %s" % (level,)) 522 self.level = level 523 self.code = code 524 self.reason = reason 525 self.affected = affected 526 self.source = source 527 528 msg = "%s %s: %s" % (level, code, reason) 529 super().__init__(msg) 530 531 def get_dict(self) -> "JsonDict": 532 return { 533 "level": self.level, 534 "code": self.code, 535 "reason": self.reason, 536 "affected": self.affected, 537 "source": self.source if self.source else self.affected, 538 } 539 540 541class HttpResponseException(CodeMessageException): 542 """ 543 Represents an HTTP-level failure of an outbound request 544 545 Attributes: 546 response: body of response 547 """ 548 549 def __init__(self, code: int, msg: str, response: bytes): 550 """ 551 552 Args: 553 code: HTTP status code 554 msg: reason phrase from HTTP response status line 555 response: body of response 556 """ 557 super().__init__(code, msg) 558 self.response = response 559 560 def to_synapse_error(self) -> SynapseError: 561 """Make a SynapseError based on an HTTPResponseException 562 563 This is useful when a proxied request has failed, and we need to 564 decide how to map the failure onto a matrix error to send back to the 565 client. 566 567 An attempt is made to parse the body of the http response as a matrix 568 error. If that succeeds, the errcode and error message from the body 569 are used as the errcode and error message in the new synapse error. 570 571 Otherwise, the errcode is set to M_UNKNOWN, and the error message is 572 set to the reason code from the HTTP response. 573 574 Returns: 575 SynapseError: 576 """ 577 # try to parse the body as json, to get better errcode/msg, but 578 # default to M_UNKNOWN with the HTTP status as the error text 579 try: 580 j = json_decoder.decode(self.response.decode("utf-8")) 581 except ValueError: 582 j = {} 583 584 if not isinstance(j, dict): 585 j = {} 586 587 errcode = j.pop("errcode", Codes.UNKNOWN) 588 errmsg = j.pop("error", self.msg) 589 590 return ProxiedRequestError(self.code, errmsg, errcode, j) 591 592 593class ShadowBanError(Exception): 594 """ 595 Raised when a shadow-banned user attempts to perform an action. 596 597 This should be caught and a proper "fake" success response sent to the user. 598 """ 599 600 601class ModuleFailedException(Exception): 602 """ 603 Raised when a module API callback fails, for example because it raised an 604 exception. 605 """ 606