Beispiel #1
0
    def render_GET(self, request: Request) -> str:
        send_cors(request)

        args = get_args(request, ("token", "sid", "client_secret"))
        resp = self.do_validate_request(request)
        if "success" in resp and resp["success"]:
            msg = "Verification successful! Please return to your Matrix client to continue."
            if "next_link" in args:
                next_link = args["next_link"]
                request.setResponseCode(302)
                request.setHeader("Location", next_link)
        else:
            request.setResponseCode(400)
            msg = (
                "Verification failed: you may need to request another verification text"
            )

        brand = self.sydent.brand_from_request(request)

        # self.sydent.config.http.verify_response_template is deprecated
        if self.sydent.config.http.verify_response_template is None:
            templateFile = self.sydent.get_branded_template(
                brand,
                "verify_response_template.html",
            )
        else:
            templateFile = self.sydent.config.http.verify_response_template

        request.setHeader("Content-Type", "text/html")
        return open(templateFile).read() % {"message": msg}
Beispiel #2
0
    def render_OPTIONS(self, request: Request) -> bytes:
        request.setResponseCode(204)
        request.setHeader(b"Content-Length", b"0")

        set_cors_headers(request)

        return b""
Beispiel #3
0
def respond_with_json_bytes(
    request: Request,
    code: int,
    json_bytes: bytes,
    send_cors: bool = False,
) -> Optional[int]:
    """Sends encoded JSON in response to the given request.

    Args:
        request: The http request to respond to.
        code: The HTTP response code.
        json_bytes: The json bytes to use as the response body.
        send_cors: Whether to send Cross-Origin Resource Sharing headers
            https://fetch.spec.whatwg.org/#http-cors-protocol

    Returns:
        twisted.web.server.NOT_DONE_YET if the request is still active.
    """
    if request._disconnected:
        logger.warning(
            "Not sending response to request %s, already disconnected.", request
        )
        return None

    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"application/json")
    request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
    request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")

    if send_cors:
        set_cors_headers(request)

    _write_bytes_to_request(request, json_bytes)
    return NOT_DONE_YET
Beispiel #4
0
    def render_GET(self, request: Request) -> bytes:
        args = get_args(request, ("nextLink", ), required=False)

        resp = None
        try:
            resp = self.do_validate_request(request)
        except Exception:
            pass
        if resp and "success" in resp and resp["success"]:
            msg = "Verification successful! Please return to your Matrix client to continue."
            if "nextLink" in args:
                next_link = args["nextLink"]
                if not next_link.startswith("file:///"):
                    request.setResponseCode(302)
                    request.setHeader("Location", next_link)
        else:
            msg = "Verification failed: you may need to request another verification email"

        brand = self.sydent.brand_from_request(request)

        # self.sydent.config.http.verify_response_template is deprecated
        if self.sydent.config.http.verify_response_template is None:
            templateFile = self.sydent.get_branded_template(
                brand,
                "verify_response_template.html",
            )
        else:
            templateFile = self.sydent.config.http.verify_response_template

        request.setHeader("Content-Type", "text/html")
        res = open(templateFile).read() % {"message": msg}

        return res.encode("UTF-8")
Beispiel #5
0
    def renderDirectory(self, request: TwistedRequest, zipFile: ZipFile,
                        node: 'ZipTreeNode') -> object:
        """Serve a directory from a ZIP file.
        """

        # URLs for directory entries should end with a slash.
        if not request.path.endswith(b'/'):
            path = (request.postpath or request.prepath)[-1]
            return redirectTo(path + b'/', request)

        # Serve index.html at directory URL.
        entries = node.children
        index = entries.get('index.html')
        if isinstance(index, ZipInfo):
            return self.renderFile(request, zipFile, index)

        # If a ZIP contains a single file or single top-level directory,
        # redirect to that.
        if len(request.postpath) == 1:
            if len(entries) == 1:
                (name, entry), = entries.items()
                path = name.encode()
                if isinstance(entry, ZipTreeNode):
                    path += b'/'
                return redirectTo(path, request)

        request.setResponseCode(500)
        request.setHeader(b'Content-Type', b'text/plain; charset=UTF-8')
        return b'ZIP directory listing not yet implemented\n'
Beispiel #6
0
    def inner(self: Res, request: Request) -> bytes:
        """
        Runs a web handler function with the given request and parameters, then
        converts its result into JSON and returns it. If an error happens, also sets
        the HTTP response code.

        :param self: The current object.
        :param request: The request to process.

        :return: The JSON payload to send as a response to the request.
        """
        try:
            request.setHeader("Content-Type", "application/json")
            return dict_to_json_bytes(f(self, request))
        except MatrixRestError as e:
            request.setResponseCode(e.httpStatus)
            return dict_to_json_bytes({"errcode": e.errcode, "error": e.error})
        except Exception:
            logger.exception("Exception processing request")
            request.setHeader("Content-Type", "application/json")
            request.setResponseCode(500)
            return dict_to_json_bytes({
                "errcode": "M_UNKNOWN",
                "error": "Internal Server Error",
            })
Beispiel #7
0
def respond_with_html_bytes(request: Request, code: int, html_bytes: bytes) -> None:
    """
    Sends HTML (encoded as UTF-8 bytes) as the response to the given request.

    Note that this adds clickjacking protection headers and finishes the request.

    Args:
        request: The http request to respond to.
        code: The HTTP response code.
        html_bytes: The HTML bytes to use as the response body.
    """
    # could alternatively use request.notifyFinish() and flip a flag when
    # the Deferred fires, but since the flag is RIGHT THERE it seems like
    # a waste.
    if request._disconnected:
        logger.warning(
            "Not sending response to request %s, already disconnected.", request
        )
        return None

    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
    request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

    # Ensure this content cannot be embedded.
    set_clickjacking_protection_headers(request)

    request.write(html_bytes)
    finish_request(request)
Beispiel #8
0
def respond_with_json_bytes(
    request: Request, code: int, json_bytes: bytes, send_cors: bool = False,
):
    """Sends encoded JSON in response to the given request.

    Args:
        request: The http request to respond to.
        code: The HTTP response code.
        json_bytes: The json bytes to use as the response body.
        send_cors: Whether to send Cross-Origin Resource Sharing headers
            https://fetch.spec.whatwg.org/#http-cors-protocol

    Returns:
        twisted.web.server.NOT_DONE_YET if the request is still active.
    """

    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"application/json")
    request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
    request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")

    if send_cors:
        set_cors_headers(request)

    # note that this is zero-copy (the bytesio shares a copy-on-write buffer with
    # the original `bytes`).
    bytes_io = BytesIO(json_bytes)

    producer = NoRangeStaticProducer(request, bytes_io)
    producer.start()
    return NOT_DONE_YET
Beispiel #9
0
    def render_GET(self, request: Request) -> bytes:
        if not self._serve_server_wellknown:
            request.setResponseCode(404)
            request.setHeader(b"Content-Type", b"text/plain")
            return b"404. Is anything ever truly *well* known?\n"

        request.setHeader(b"Content-Type", b"application/json")
        return self._response
Beispiel #10
0
 def putDone(
         cls,
         result: None,  # pylint: disable=unused-argument
         request: TwistedRequest) -> None:
     request.setResponseCode(201)
     request.setHeader(b'Content-Type', b'text/plain; charset=UTF-8')
     request.write(b'Artifact stored\n')
     request.finish()
Beispiel #11
0
    def do_validate_request(self, request: Request) -> JsonDict:
        """
        Extracts information about a validation session from the request and
        attempts to validate that session.

        :param request: The request to extract information about the session from.

        :return: A dict with a "success" key which value indicates whether the
            validation succeeded. If the validation failed, this dict also includes
            a "errcode" and a "error" keys which include information about the failure.
        """

        args = get_args(request, ("token", "sid", "client_secret"))

        sid = args["sid"]
        tokenString = args["token"]
        clientSecret = args["client_secret"]

        if not is_valid_client_secret(clientSecret):
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PARAM",
                "error": "Invalid client_secret provided",
            }

        try:
            return self.sydent.validators.msisdn.validateSessionWithToken(
                sid, clientSecret, tokenString
            )
        except IncorrectClientSecretException:
            request.setResponseCode(400)
            return {
                "success": False,
                "errcode": "M_INVALID_PARAM",
                "error": "Client secret does not match the one given when requesting the token",
            }
        except SessionExpiredException:
            request.setResponseCode(400)
            return {
                "success": False,
                "errcode": "M_SESSION_EXPIRED",
                "error": "This validation session has expired: call requestToken again",
            }
        except InvalidSessionIdException:
            request.setResponseCode(400)
            return {
                "success": False,
                "errcode": "M_INVALID_PARAM",
                "error": "The token doesn't match",
            }
        except IncorrectSessionTokenException:
            request.setResponseCode(404)
            return {
                "success": False,
                "errcode": "M_NO_VALID_SESSION",
                "error": "No session could be found with this sid",
            }
Beispiel #12
0
    def render_GET(self, request: server.Request):  # noqa: N802
        """Render the database status in plain text."""
        if not self.is_client_permitted(request):
            request.setResponseCode(403)
            return b'Access denied'

        result_txt = DatabaseStatusRequest().generate_status()
        request.setHeader(b'Content-Type', b'text/plain; charset=utf-8')
        return result_txt.encode('utf-8')
Beispiel #13
0
 def putFailed(cls, fail: Failure, request: TwistedRequest) -> None:
     ex = fail.value
     if isinstance(ex, ValueError):
         request.setResponseCode(415)
         request.setHeader(b'Content-Type', b'text/plain; charset=UTF-8')
         request.write((f'{ex}\n').encode())
         request.finish()
     else:
         request.processingFailed(fail)
 def test_handle_error(self):
     import json
     request = Request(DummyChannel(), 1)
     request.gotLength(0)
     self.api_resource.write_request = mock.Mock()
     result = self.api_resource._handle_error(request,
         500, 'Error', 'Big mistake')
     request.setResponseCode(500)
     body = json.dumps({'error': 'Error', 'message': 'Big mistake'})
     self.api_resource.write_request.assert_called_with((request, body))
Beispiel #15
0
    def render_GET(self, request: Request) -> bytes:
        set_cors_headers(request)
        r = self._well_known_builder.get_well_known()
        if not r:
            request.setResponseCode(404)
            request.setHeader(b"Content-Type", b"text/plain")
            return b".well-known not available"

        logger.debug("returning: %s", r)
        request.setHeader(b"Content-Type", b"application/json")
        return json_encoder.encode(r).encode("utf-8")
 def test_error_in_read_function(self):
     req = Request(DummyChannel(), 1)
     req.setResponseCode(200)
     req.method = 'GET'
     def error_function(a, b):
         raise Exception("boom")
     self.api.read_GET = error_function
     self.api._handle_error = mock.Mock()
     result, body = self.api.resource_renderer('res', req)
     self.api._handle_error.assert_called_with(req, 500, "ReadError",
         "Error %r in resource reading function." % Exception('boom'))
Beispiel #17
0
def return_html_error(
    f: failure.Failure,
    request: Request,
    error_template: Union[str, jinja2.Template],
) -> None:
    """Sends an HTML error page corresponding to the given failure.

    Handles RedirectException and other CodeMessageExceptions (such as SynapseError)

    Args:
        f: the error to report
        request: the failing request
        error_template: the HTML template. Can be either a string (with `{code}`,
            `{msg}` placeholders), or a jinja2 template
    """
    if f.check(CodeMessageException):
        cme = f.value
        code = cme.code
        msg = cme.msg

        if isinstance(cme, RedirectException):
            logger.info("%s redirect to %s", request, cme.location)
            request.setHeader(b"location", cme.location)
            request.cookies.extend(cme.cookies)
        elif isinstance(cme, SynapseError):
            logger.info("%s SynapseError: %s - %s", request, code, msg)
        else:
            logger.error(
                "Failed handle request %r",
                request,
                exc_info=(f.type, f.value, f.getTracebackObject()),
            )
    else:
        code = http.HTTPStatus.INTERNAL_SERVER_ERROR
        msg = "Internal server error"

        logger.error(
            "Failed handle request %r",
            request,
            exc_info=(f.type, f.value, f.getTracebackObject()),
        )

    if isinstance(error_template, str):
        body = error_template.format(code=code, msg=html.escape(msg))
    else:
        body = error_template.render(code=code, msg=msg)

    body_bytes = body.encode("utf-8")
    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
    request.setHeader(b"Content-Length", b"%i" % (len(body_bytes), ))
    request.write(body_bytes)
    finish_request(request)
def _return_html_error(code: int, msg: str, request: Request):
    """Sends an HTML error page"""
    body = HTML_ERROR_TEMPLATE.format(code=code,
                                      msg=html.escape(msg)).encode("utf-8")
    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
    request.setHeader(b"Content-Length", b"%i" % (len(body), ))
    request.write(body)
    try:
        request.finish()
    except RuntimeError as e:
        logger.info("Connection disconnected before response was written: %r",
                    e)
Beispiel #19
0
def respond_with_json(
    request: Request,
    code: int,
    json_object: Any,
    send_cors: bool = False,
    pretty_print: bool = False,
    canonical_json: bool = True,
):
    """Sends encoded JSON in response to the given request.

    Args:
        request: The http request to respond to.
        code: The HTTP response code.
        json_object: The object to serialize to JSON.
        send_cors: Whether to send Cross-Origin Resource Sharing headers
            https://fetch.spec.whatwg.org/#http-cors-protocol
        pretty_print: Whether to include indentation and line-breaks in the
            resulting JSON bytes.
        canonical_json: Whether to use the canonicaljson algorithm when encoding
            the JSON bytes.

    Returns:
        twisted.web.server.NOT_DONE_YET if the request is still active.
    """
    # could alternatively use request.notifyFinish() and flip a flag when
    # the Deferred fires, but since the flag is RIGHT THERE it seems like
    # a waste.
    if request._disconnected:
        logger.warning(
            "Not sending response to request %s, already disconnected.",
            request)
        return None

    if pretty_print:
        encoder = iterencode_pretty_printed_json
    else:
        if canonical_json or synapse.events.USE_FROZEN_DICTS:
            encoder = iterencode_canonical_json
        else:
            encoder = _encode_json_bytes

    request.setResponseCode(code)
    request.setHeader(b"Content-Type", b"application/json")
    request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")

    if send_cors:
        set_cors_headers(request)

    _ByteProducer(request, encoder(json_object))
    return NOT_DONE_YET
Beispiel #20
0
    async def on_GET(self, request: Request) -> None:
        if self.config.email.threepid_behaviour_email == ThreepidBehaviour.OFF:
            if self.config.email.local_threepid_handling_disabled_due_to_email_config:
                logger.warning(
                    "Adding emails have been disabled due to lack of an email config"
                )
            raise SynapseError(
                400, "Adding an email to your account is disabled on this server"
            )
        elif self.config.email.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
            raise SynapseError(
                400,
                "This homeserver is not validating threepids. Use an identity server "
                "instead.",
            )

        sid = parse_string(request, "sid", required=True)
        token = parse_string(request, "token", required=True)
        client_secret = parse_string(request, "client_secret", required=True)
        assert_valid_client_secret(client_secret)

        # Attempt to validate a 3PID session
        try:
            # Mark the session as valid
            next_link = await self.store.validate_threepid_session(
                sid, client_secret, token, self.clock.time_msec()
            )

            # Perform a 302 redirect if next_link is set
            if next_link:
                request.setResponseCode(302)
                request.setHeader("Location", next_link)
                finish_request(request)
                return None

            # Otherwise show the success template
            html = self.config.email.email_add_threepid_template_success_html_content
            status_code = 200
        except ThreepidValidationError as e:
            status_code = e.code

            # Show a failure page with a reason
            template_vars = {"failure_reason": e.msg}
            html = self._failure_email_template.render(**template_vars)

        respond_with_html(request, status_code, html)
Beispiel #21
0
    async def on_GET(self, request: Request, medium: str) -> None:
        if medium != "email":
            raise SynapseError(
                400, "This medium is currently not supported for registration")
        if self.config.email.threepid_behaviour_email == ThreepidBehaviour.OFF:
            if self.config.email.local_threepid_handling_disabled_due_to_email_config:
                logger.warning(
                    "User registration via email has been disabled due to lack of email config"
                )
            raise SynapseError(
                400, "Email-based registration is disabled on this server")

        sid = parse_string(request, "sid", required=True)
        client_secret = parse_string(request, "client_secret", required=True)
        assert_valid_client_secret(client_secret)
        token = parse_string(request, "token", required=True)

        # Attempt to validate a 3PID session
        try:
            # Mark the session as valid
            next_link = await self.store.validate_threepid_session(
                sid, client_secret, token, self.clock.time_msec())

            # Perform a 302 redirect if next_link is set
            if next_link:
                if next_link.startswith("file:///"):
                    logger.warning(
                        "Not redirecting to next_link as it is a local file: address"
                    )
                else:
                    request.setResponseCode(302)
                    request.setHeader("Location", next_link)
                    finish_request(request)
                    return None

            # Otherwise show the success template
            html = self.config.email.email_registration_template_success_html_content
            status_code = 200
        except ThreepidValidationError as e:
            status_code = e.code

            # Show a failure page with a reason
            template_vars = {"failure_reason": e.msg}
            html = self._failure_email_template.render(**template_vars)

        respond_with_html(request, status_code, html)
    def render_GET(self, request: Request) -> JsonDict:
        send_cors(request)
        if self.require_auth:
            authV2(self.sydent, request)

        args = get_args(request, ("sid", "client_secret"))

        sid = args["sid"]
        clientSecret = args["client_secret"]

        if not is_valid_client_secret(clientSecret):
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PARAM",
                "error": "Invalid client_secret provided",
            }

        valSessionStore = ThreePidValSessionStore(self.sydent)

        noMatchError = {
            "errcode":
            "M_NO_VALID_SESSION",
            "error":
            "No valid session was found matching that sid and client secret",
        }

        try:
            s = valSessionStore.getValidatedSession(sid, clientSecret)
        except (IncorrectClientSecretException, InvalidSessionIdException):
            request.setResponseCode(404)
            return noMatchError
        except SessionExpiredException:
            request.setResponseCode(400)
            return {
                "errcode":
                "M_SESSION_EXPIRED",
                "error":
                "This validation session has expired: call requestToken again",
            }
        except SessionNotValidatedException:
            request.setResponseCode(400)
            return {
                "errcode": "M_SESSION_NOT_VALIDATED",
                "error": "This validation session has not yet been completed",
            }

        return {
            "medium": s.medium,
            "address": s.address,
            "validated_at": s.mtime
        }
Beispiel #23
0
 async def render(f: AsyncRenderer[Res], self: Res,
                  request: Request) -> None:
     request.setHeader("Content-Type", "application/json")
     try:
         result = await f(self, request)
         request.write(dict_to_json_bytes(result))
     except MatrixRestError as e:
         request.setResponseCode(e.httpStatus)
         request.write(
             dict_to_json_bytes({
                 "errcode": e.errcode,
                 "error": e.error
             }))
     except Exception:
         logger.exception("Request processing failed")
         request.setResponseCode(500)
         request.write(
             dict_to_json_bytes({
                 "errcode": "M_UNKNOWN",
                 "error": "Internal Server Error"
             }))
     request.finish()
Beispiel #24
0
    def handlePush(self, request: TwistedRequest, repoId: str,
                   parsed: Any) -> bytes:
        # Find branches.
        try:
            branches = set(self.findBranches(parsed))
        except KeyError as ex:
            request.setResponseCode(400)
            return b'Missing key in JSON: %s\n' % str(ex).encode()

        # Trigger schedules.
        tagValues = {f'{repoId}/{branch}' for branch in branches}
        scheduleIds = []
        for scheduleId, schedule in self.scheduleDB.items():
            if tagValues & schedule.tags.getTagValues('sf.trigger'):
                schedule.setTrigger()
                scheduleIds.append(scheduleId)

        logging.info(
            'Got update on "%s" webhook for branch: %s; '
            'triggered schedule: %s', self.name, ', '.join(branches),
            ', '.join(scheduleIds))
        return b'Received\n'
Beispiel #25
0
    def render_PUT(self, request: TwistedRequest) -> object:
        path = self.path

        if path.isfile():
            request.setResponseCode(409)
            request.setHeader(b'Content-Type', b'text/plain; charset=UTF-8')
            return b'Artifacts cannot be overwritten\n'

        # Note: Twisted buffers the entire upload into 'request.content'
        #       prior to calling our render method.
        #       There doesn't seem to be a clean way to handle streaming
        #       uploads in Twisted; we'd have to set site.requestFactory
        #       to a request implementation that overrides gotLength() or
        #       handleContentChunk(), both of which are documented as
        #       "not intended for users".

        # Do the actual store in a separate thread, so we don't have to worry
        # about slow operations hogging the reactor thread.
        deferToThread(self._storeArtifact, request.content, path) \
            .addCallback(self.putDone, request) \
            .addErrback(self.putFailed, request)
        return NOT_DONE_YET
Beispiel #26
0
    def render_OPTIONS(self, request: TwistedRequest) -> bytes:
        # Generic HTTP options.
        request.setHeader(b'Allow', b'GET, HEAD, OPTIONS')

        # CORS options.
        origin = request.getHeader(b'Origin')
        if origin is not None:
            # Grant all requested headers.
            requestedHeaders = request.getHeader(
                b'Access-Control-Request-Headers') or b''
            request.setHeader(b'Access-Control-Allow-Headers',
                              requestedHeaders)

            # The information returned does not expire, but the sanboxed URL
            # to which it applies does, so caching it beyond the sanbox key
            # timeout is pointless.
            request.setHeader(b'Access-Control-Max-Age',
                              b'%d' % ArtifactSandbox.keyTimeout)

        # Reply without content.
        request.setResponseCode(204)
        request.setHeader(b'Content-Length', b'0')
        return b''
Beispiel #27
0
    def render_POST(self, request: Request) -> bytes:
        try:
            command = self.read_required_string_parameter('command', request)

            if command not in self.commands:
                raise InvalidCommandException(command)

            response = self.commands[command](request)

            if response is not None:
                requests.post(self.read_required_string_parameter(
                    'response_url', request),
                              timeout=15,
                              data=json.dumps(response),
                              headers={'Content-Type': 'application/json'})

            request.setResponseCode(200)
            return self.to_bytes('')

        except MissingParameterException as e:
            return BadRequestErrorPage(str(e)).render(request)

        except InvalidCommandException as e:
            return BadRequestErrorPage(str(e)).render(request)
Beispiel #28
0
    def render_POST(self, request: Request) -> JsonDict:
        # Cast safety: This request has an ISSLTransport because this servlet
        # is a resource under the ReplicationHttpsServer and nowhere else.
        request.transport = cast(ISSLTransport, request.transport)
        peerCert = cast(X509, request.transport.getPeerCertificate())
        peerCertCn = peerCert.get_subject().commonName

        peerStore = PeerStore(self.sydent)

        peer = peerStore.getPeerByName(peerCertCn)

        if not peer:
            logger.warning(
                "Got connection from %s but no peer found by that name",
                peerCertCn)
            raise MatrixRestError(403, "M_UNKNOWN_PEER",
                                  "This peer is not known to this server")

        logger.info("Push connection made from peer %s", peer.servername)

        if (not request.requestHeaders.hasHeader("Content-Type")
                # Type safety: the hasHeader call returned True, so getRawHeaders()
                # returns a nonempty list.
                or request.requestHeaders.getRawHeaders("Content-Type")[
                    0]  # type: ignore[index]
                != "application/json"):
            logger.warning(
                "Peer %s made push connection with non-JSON content (type: %s)",
                peer.servername,
                # Type safety: the hasHeader call returned True, so getRawHeaders()
                # returns a nonempty list.
                request.requestHeaders.getRawHeaders("Content-Type")
                [0],  # type: ignore[index]
            )
            raise MatrixRestError(400, "M_NOT_JSON",
                                  "This endpoint expects JSON")

        try:
            # json.loads doesn't allow bytes in Python 3.5
            inJson = json_decoder.decode(
                request.content.read().decode("UTF-8"))
        except ValueError:
            logger.warning("Peer %s made push connection with malformed JSON",
                           peer.servername)
            raise MatrixRestError(400, "M_BAD_JSON", "Malformed JSON")

        if "sgAssocs" not in inJson:
            logger.warning(
                "Peer %s made push connection with no 'sgAssocs' key in JSON",
                peer.servername,
            )
            raise MatrixRestError(400, "M_BAD_JSON",
                                  'No "sgAssocs" key in JSON')

        failedIds: List[int] = []

        globalAssocsStore = GlobalAssociationStore(self.sydent)

        # Ensure items are pulled out of the dictionary in order of origin_id.
        sg_assocs_raw: SignedAssociations = inJson.get("sgAssocs", {})
        sg_assocs = sorted(sg_assocs_raw.items(), key=lambda k: int(k[0]))

        for originId, sgAssoc in sg_assocs:
            try:
                peer.verifySignedAssociation(sgAssoc)
                logger.debug(
                    "Signed association from %s with origin ID %s verified",
                    peer.servername,
                    originId,
                )

                # Don't bother adding if one has already failed: we add all of them or none so
                # we're only going to roll back the transaction anyway (but we continue to try
                # & verify the rest so we can give a complete list of the ones that don't
                # verify)
                if len(failedIds) > 0:
                    continue

                assocObj = threePidAssocFromDict(sgAssoc)

                # ensure we are casefolding email addresses before hashing/storing
                assocObj.address = normalise_address(assocObj.address,
                                                     assocObj.medium)

                if assocObj.mxid is not None:
                    # Calculate the lookup hash with our own pepper for this association
                    pepper = self.hashing_store.get_lookup_pepper()
                    assert pepper is not None
                    str_to_hash = " ".join(
                        [assocObj.address, assocObj.medium, pepper], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # Add this association
                    globalAssocsStore.addAssociation(
                        assocObj,
                        json.dumps(sgAssoc),
                        peer.servername,
                        originId,
                        commit=False,
                    )
                else:
                    logger.info(
                        "Incoming deletion: removing associations for %s / %s",
                        assocObj.medium,
                        assocObj.address,
                    )
                    globalAssocsStore.removeAssociation(
                        assocObj.medium, assocObj.address)
                logger.info("Stored association origin ID %s from %s",
                            originId, peer.servername)
            except Exception:
                failedIds.append(originId)
                logger.warning(
                    "Failed to verify signed association from %s with origin ID %s",
                    peer.servername,
                    originId,
                )
                twisted.python.log.err()

        if len(failedIds) > 0:
            self.sydent.db.rollback()
            request.setResponseCode(400)
            return {
                "errcode": "M_VERIFICATION_FAILED",
                "error": "Verification failed for one or more associations",
                "failed_ids": failedIds,
            }
        else:
            self.sydent.db.commit()
            return {"success": True}
Beispiel #29
0
    def render_POST(self, request: Request) -> JsonDict:
        """
        Perform lookups with potentially hashed 3PID details.

        Depending on our response to /hash_details, the client will choose a
        hash algorithm and pepper, hash the 3PIDs it wants to lookup, and
        send them to us, along with the algorithm and pepper it used.

        We first check this algorithm/pepper combo matches what we expect,
        then compare the 3PID details to what we have in the database.

        Params: A JSON object containing the following keys:
                * 'addresses': List of hashed/plaintext (depending on the
                               algorithm) 3PID addresses and mediums.
                * 'algorithm': The algorithm the client has used to process
                               the 3PIDs.
                * 'pepper': The pepper the client has attached to the 3PIDs.

        Returns: Object with key 'mappings', which is a dictionary of results
                 where each result is a key/value pair of what the client sent, and
                 the matching Matrix User ID that claims to own that 3PID.

                 User IDs for which no mapping is found are omitted.
        """
        send_cors(request)

        authV2(self.sydent, request)

        args = get_args(request, ("addresses", "algorithm", "pepper"))

        addresses = args["addresses"]
        if not isinstance(addresses, list):
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PARAM",
                "error": "addresses must be a list"
            }

        algorithm = str(args["algorithm"])
        if algorithm not in HashDetailsServlet.known_algorithms:
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PARAM",
                "error": "algorithm is not supported"
            }

        # Ensure address count is under the configured limit
        limit = self.sydent.config.general.address_lookup_limit
        if len(addresses) > limit:
            request.setResponseCode(400)
            return {
                "errcode": "M_TOO_LARGE",
                "error": "More than the maximum amount of "
                "addresses provided",
            }

        pepper = str(args["pepper"])
        if pepper != self.lookup_pepper:
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PEPPER",
                "error": "pepper does not match '%s'" % (self.lookup_pepper, ),
                "algorithm": algorithm,
                "lookup_pepper": self.lookup_pepper,
            }

        logger.info("Lookup of %d threepid(s) with algorithm %s",
                    len(addresses), algorithm)
        if algorithm == "none":
            # Lookup without hashing
            medium_address_tuples = []
            for address_and_medium in addresses:
                # Parse medium, address components
                address_medium_split = address_and_medium.split()

                # Forbid addresses that contain a space
                if len(address_medium_split) != 2:
                    request.setResponseCode(400)
                    return {
                        "errcode":
                        "M_UNKNOWN",
                        "error":
                        'Invalid "address medium" pair: "%s"' %
                        address_and_medium,
                    }

                # Get the mxid for the address/medium combo if known
                address, medium = address_medium_split
                medium_address_tuples.append((medium, address))

            # Lookup the mxids
            medium_address_mxid_tuples = self.globalAssociationStore.getMxids(
                medium_address_tuples)

            # Return a dictionary of lookup_string: mxid values
            return {
                "mappings": {
                    "%s %s" % (x[1], x[0]): x[2]
                    for x in medium_address_mxid_tuples
                }
            }

        elif algorithm == "sha256":
            # Lookup using SHA256 with URL-safe base64 encoding
            mappings = self.globalAssociationStore.retrieveMxidsForHashes(
                addresses)

            return {"mappings": mappings}

        request.setResponseCode(400)
        return {
            "errcode": "M_INVALID_PARAM",
            "error": "algorithm is not supported"
        }
Beispiel #30
0
 def setResponseCode(self, code, message):
     invoked.append(get_ident())
     return Request.setResponseCode(self, code, message)
    async def _async_render_POST(self, request: Request) -> None:
        try:
            try:
                # TODO: we should really validate that this gives us a dict, and
                #   not some other json value like str, list, int etc
                # json.loads doesn't allow bytes in Python 3.5
                body: JsonDict = json_decoder.decode(
                    request.content.read().decode("UTF-8"))
            except ValueError:
                request.setResponseCode(HTTPStatus.BAD_REQUEST)
                request.write(
                    dict_to_json_bytes({
                        "errcode": "M_BAD_JSON",
                        "error": "Malformed JSON"
                    }))
                request.finish()
                return

            missing = [k for k in ("threepid", "mxid") if k not in body]
            if len(missing) > 0:
                request.setResponseCode(HTTPStatus.BAD_REQUEST)
                msg = "Missing parameters: " + (",".join(missing))
                request.write(
                    dict_to_json_bytes({
                        "errcode": "M_MISSING_PARAMS",
                        "error": msg
                    }))
                request.finish()
                return

            threepid = body["threepid"]
            mxid = body["mxid"]

            if "medium" not in threepid or "address" not in threepid:
                request.setResponseCode(HTTPStatus.BAD_REQUEST)
                request.write(
                    dict_to_json_bytes({
                        "errcode":
                        "M_MISSING_PARAMS",
                        "error":
                        "Threepid lacks medium / address",
                    }))
                request.finish()
                return

            # We now check for authentication in two different ways, depending
            # on the contents of the request. If the user has supplied "sid"
            # (the Session ID returned by Sydent during the original binding)
            # and "client_secret" fields, they are trying to prove that they
            # were the original author of the bind. We then check that what
            # they supply matches and if it does, allow the unbind.
            #
            # However if these fields are not supplied, we instead check
            # whether the request originated from a homeserver, and if so the
            # same homeserver that originally created the bind. We do this by
            # checking the signature of the request. If it all matches up, we
            # allow the unbind.
            #
            # Only one method of authentication is required.
            if "sid" in body and "client_secret" in body:
                sid = body["sid"]
                client_secret = body["client_secret"]

                if not is_valid_client_secret(client_secret):
                    request.setResponseCode(HTTPStatus.BAD_REQUEST)
                    request.write(
                        dict_to_json_bytes({
                            "errcode":
                            "M_INVALID_PARAM",
                            "error":
                            "Invalid client_secret provided",
                        }))
                    request.finish()
                    return

                valSessionStore = ThreePidValSessionStore(self.sydent)

                try:
                    s = valSessionStore.getValidatedSession(sid, client_secret)
                except (IncorrectClientSecretException,
                        InvalidSessionIdException):
                    request.setResponseCode(HTTPStatus.UNAUTHORIZED)
                    request.write(
                        dict_to_json_bytes({
                            "errcode":
                            "M_NO_VALID_SESSION",
                            "error":
                            "No valid session was found matching that sid and client secret",
                        }))
                    request.finish()
                    return
                except SessionNotValidatedException:
                    request.setResponseCode(HTTPStatus.FORBIDDEN)
                    request.write(
                        dict_to_json_bytes({
                            "errcode":
                            "M_SESSION_NOT_VALIDATED",
                            "error":
                            "This validation session has not yet been completed",
                        }))
                    return

                if s.medium != threepid["medium"] or s.address != threepid[
                        "address"]:
                    request.setResponseCode(HTTPStatus.FORBIDDEN)
                    request.write(
                        dict_to_json_bytes({
                            "errcode":
                            "M_FORBIDDEN",
                            "error":
                            "Provided session information does not match medium/address combo",
                        }))
                    request.finish()
                    return
            else:
                try:
                    origin_server_name = (
                        await self.sydent.sig_verifier.authenticate_request(
                            request, body))
                except SignatureVerifyException as ex:
                    request.setResponseCode(HTTPStatus.UNAUTHORIZED)
                    request.write(
                        dict_to_json_bytes({
                            "errcode": "M_FORBIDDEN",
                            "error": str(ex)
                        }))
                    request.finish()
                    return
                except NoAuthenticationError as ex:
                    request.setResponseCode(HTTPStatus.UNAUTHORIZED)
                    request.write(
                        dict_to_json_bytes({
                            "errcode": "M_FORBIDDEN",
                            "error": str(ex)
                        }))
                    request.finish()
                    return
                except InvalidServerName as ex:
                    request.setResponseCode(HTTPStatus.BAD_REQUEST)
                    request.write(
                        dict_to_json_bytes({
                            "errcode": "M_INVALID_PARAM",
                            "error": str(ex)
                        }))
                    request.finish()
                    return
                except (DNSLookupError, ConnectError, ResponseFailed) as e:
                    msg = (f"Unable to contact the Matrix homeserver to "
                           f"authenticate request ({type(e).__name__})")
                    logger.warning(msg)
                    request.setResponseCode(HTTPStatus.INTERNAL_SERVER_ERROR)
                    request.write(
                        dict_to_json_bytes({
                            "errcode": "M_UNKNOWN",
                            "error": msg,
                        }))
                    request.finish()
                    return
                except Exception:
                    logger.exception(
                        "Exception whilst authenticating unbind request")
                    request.setResponseCode(HTTPStatus.INTERNAL_SERVER_ERROR)
                    request.write(
                        dict_to_json_bytes({
                            "errcode": "M_UNKNOWN",
                            "error": "Internal Server Error"
                        }))
                    request.finish()
                    return

                if not mxid.endswith(":" + origin_server_name):
                    request.setResponseCode(HTTPStatus.FORBIDDEN)
                    request.write(
                        dict_to_json_bytes({
                            "errcode":
                            "M_FORBIDDEN",
                            "error":
                            "Origin server name does not match mxid",
                        }))
                    request.finish()
                    return

            self.sydent.threepidBinder.removeBinding(threepid, mxid)

            request.write(dict_to_json_bytes({}))
            request.finish()
        except Exception as ex:
            logger.exception("Exception whilst handling unbind")
            request.setResponseCode(HTTPStatus.INTERNAL_SERVER_ERROR)
            request.write(
                dict_to_json_bytes({
                    "errcode": "M_UNKNOWN",
                    "error": str(ex)
                }))
            request.finish()
Beispiel #32
0
    def render(self, request: server.Request) -> bytes:
        # Deny by default.
        request.setResponseCode(401)

        # Get session cookie value if any.
        sessionid = request.getCookie(self.cookie)
        if sessionid is not None:
            if sessionid in self.sessions:
                request.setResponseCode(200)
                self.log.info("Session: Validation succeeded")
                return b""
            else:
                self.log.info("Session: Invalid session id")

        # Token is passed as a query parameter in the original URL.
        origurl = http.urlparse(request.getHeader(self.header))
        query = http.parse_qs(origurl.query)
        args = query.get(self.param, [])
        if len(args) != 1:
            self.log.error("Request: Token {param} missing", param=self.param)
            return b""

        try:
            token = jwt.JWT(key=self.key, jwt=args[0].decode())
        except (jwt.JWTExpired, jwt.JWTNotYetValid, jwt.JWTMissingClaim,
                jwt.JWTInvalidClaimValue, jwt.JWTInvalidClaimFormat,
                jwt.JWTMissingKeyID, jwt.JWTMissingKey) as error:
            self.log.error("JWT token: {error}", error=error)
            return b""
        except Exception:
            self.log.failure("JWT token: Unknown exception")
            return b""

        try:
            claims = json.loads(token.claims)
        except json.JSONDecodeError as error:
            self.log.failure("JWT token: Claims {error}", error=error)
            return b""

        # Collect session parameters from claims.
        sessparams = claims.get("session", {})
        kwargs = {
            "expires": sessparams.get("expires", None),
            "domain": sessparams.get("domain", None),
            "path": sessparams.get("path", None),
            "secure": sessparams.get("secure", None),
            "httpOnly": sessparams.get("httpOnly", None),
            "sameSite": sessparams.get("sameSite", None),
        }

        # Use maxAge for session ttl if it is present, convert it into a str
        # type as required by the addCookie call.
        if "maxAge" in sessparams:
            kwargs["max_age"] = str(sessparams["maxAge"])
            sessttl = int(sessparams["maxAge"])
        else:
            sessttl = self.sessttl

        # Generate a new session id and remember it. Also clean it up after
        # ttl seconds.
        sessionid = secrets.token_urlsafe(nbytes=16).encode()
        self.sessions.add(sessionid)
        reactor.callLater(sessttl, self._session_remove, sessionid)
        self.log.info("Session: Created, num sessions: {sessions}",
                      sessions=len(self.sessions))

        # Set cookie in the browser.
        request.addCookie(self.cookie, sessionid, **kwargs)

        request.setResponseCode(200)
        self.log.info("JWT token: Validation succeeded")
        return b""
Beispiel #33
0
    async def render_POST(self, request: Request) -> JsonDict:
        """
        Register with the Identity Server
        """
        send_cors(request)

        args = get_args(request, ("matrix_server_name", "access_token"))

        matrix_server = args["matrix_server_name"].lower()

        if not is_valid_matrix_server_name(matrix_server):
            request.setResponseCode(400)
            return {
                "errcode":
                "M_INVALID_PARAM",
                "error":
                "matrix_server_name must be a valid Matrix server name (IP address or hostname)",
            }

        def federation_request_problem(error: str) -> Dict[str, str]:
            logger.warning(error)
            request.setResponseCode(HTTPStatus.INTERNAL_SERVER_ERROR)
            return {
                "errcode": "M_UNKNOWN",
                "error": error,
            }

        try:
            result = await self.client.get_json(
                "matrix://%s/_matrix/federation/v1/openid/userinfo?access_token=%s"
                % (
                    matrix_server,
                    urllib.parse.quote(args["access_token"]),
                ),
                1024 * 5,
            )
        except (DNSLookupError, ConnectError, ResponseFailed) as e:
            return federation_request_problem(
                f"Unable to contact the Matrix homeserver ({type(e).__name__})"
            )
        except JSONDecodeError:
            return federation_request_problem(
                "The Matrix homeserver returned invalid JSON")

        if "sub" not in result:
            return federation_request_problem(
                "The Matrix homeserver did not include 'sub' in its response",
            )

        user_id = result["sub"]

        if not isinstance(user_id, str):
            return federation_request_problem(
                "The Matrix homeserver returned a malformed reply")

        user_id_components = user_id.split(":", 1)

        # Ensure there's a localpart and domain in the returned user ID.
        if len(user_id_components) != 2:
            return federation_request_problem(
                "The Matrix homeserver returned an invalid MXID")

        user_id_server = user_id_components[1]

        if not is_valid_matrix_server_name(user_id_server):
            return federation_request_problem(
                "The Matrix homeserver returned an invalid MXID")

        if user_id_server != matrix_server:
            return federation_request_problem(
                "The Matrix homeserver returned a MXID belonging to another homeserver"
            )

        tok = issueToken(self.sydent, user_id)

        # XXX: `token` is correct for the spec, but we released with `access_token`
        # for a substantial amount of time. Serve both to make spec-compliant clients
        # happy.
        return {
            "access_token": tok,
            "token": tok,
        }