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