def do_validate_request(self, request): """ 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. :type request: twisted.web.server.Request :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. :rtype: dict[str, bool or str] """ 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.email.validateSessionWithToken( sid, clientSecret, tokenString) except IncorrectClientSecretException: return { 'success': False, 'errcode': 'M_INVALID_PARAM', 'error': "Client secret does not match the one given when requesting the token" } except SessionExpiredException: return { 'success': False, 'errcode': 'M_SESSION_EXPIRED', 'error': "This validation session has expired: call requestToken again" } except InvalidSessionIdException: return { 'success': False, 'errcode': 'M_INVALID_PARAM', 'error': "The token doesn't match" } except IncorrectSessionTokenException: return { 'success': False, 'errcode': 'M_NO_VALID_SESSION', 'error': "No session could be found with this sid" }
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", }
def render_POST(self, request): send_cors(request) account = authIfV2(self.sydent, request) args = get_args(request, ('sid', 'client_secret', 'mxid')) sid = args['sid'] mxid = args['mxid'] clientSecret = args['client_secret'] if not is_valid_client_secret(clientSecret): request.setResponseCode(400) return { 'errcode': 'M_INVALID_PARAM', 'error': 'Invalid client_secret provided' } # Return the same error for not found / bad client secret otherwise people can get information about # sessions without knowing the secret noMatchError = { 'errcode': 'M_NO_VALID_SESSION', 'error': "No valid session was found matching that sid and client secret" } if account: # This is a v2 API so only allow binding to the logged in user id if account.userId != mxid: raise MatrixRestError( 403, 'M_UNAUTHORIZED', "This user is prohibited from binding to the mxid") try: valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getValidatedSession(sid, clientSecret) except IncorrectClientSecretException: return noMatchError except SessionExpiredException: return { 'errcode': 'M_SESSION_EXPIRED', 'error': "This validation session has expired: call requestToken again" } except InvalidSessionIdException: return noMatchError except SessionNotValidatedException: return { 'errcode': 'M_SESSION_NOT_VALIDATED', 'error': "This validation session has not yet been completed" } res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid) return res
def render_POST(self, request): send_cors(request) authIfV2(self.sydent, request) args = get_args(request, ('phone_number', 'country', 'client_secret', 'send_attempt')) raw_phone_number = args['phone_number'] country = args['country'] sendAttempt = args['send_attempt'] 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: phone_number_object = phonenumbers.parse(raw_phone_number, country) except Exception as e: logger.warn("Invalid phone number given: %r", e) request.setResponseCode(400) return {'errcode': 'M_INVALID_PHONE_NUMBER', 'error': "Invalid phone number" } msisdn = phonenumbers.format_number( phone_number_object, phonenumbers.PhoneNumberFormat.E164 )[1:] # International formatted number. The same as an E164 but with spaces # in appropriate places to make it nicer for the humans. intl_fmt = phonenumbers.format_number( phone_number_object, phonenumbers.PhoneNumberFormat.INTERNATIONAL ) try: sid = self.sydent.validators.msisdn.requestToken( phone_number_object, clientSecret, sendAttempt ) resp = { 'success': True, 'sid': str(sid), 'msisdn': msisdn, 'intl_fmt': intl_fmt, } except DestinationRejectedException: logger.error("Destination rejected for number: %s", msisdn) request.setResponseCode(400) resp = {'errcode': 'M_DESTINATION_REJECTED', 'error': 'Phone numbers in this country are not currently supported'} except Exception as e: logger.error("Exception sending SMS: %r", e) request.setResponseCode(500) resp = {'errcode': 'M_UNKNOWN', 'error': 'Internal Server Error'} return resp
def render_POST(self, request: Request) -> JsonDict: send_cors(request) account = None if self.require_auth: account = authV2(self.sydent, request) args = get_args(request, ("sid", "client_secret", "mxid")) sid = args["sid"] mxid = args["mxid"] clientSecret = args["client_secret"] if not is_valid_client_secret(clientSecret): raise MatrixRestError(400, "M_INVALID_PARAM", "Invalid client_secret provided") if account: # This is a v2 API so only allow binding to the logged in user id if account.userId != mxid: raise MatrixRestError( 403, "M_UNAUTHORIZED", "This user is prohibited from binding to the mxid", ) try: valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getValidatedSession(sid, clientSecret) except (IncorrectClientSecretException, InvalidSessionIdException): # Return the same error for not found / bad client secret otherwise # people can get information about sessions without knowing the # secret. raise MatrixRestError( 404, "M_NO_VALID_SESSION", "No valid session was found matching that sid and client secret", ) except SessionExpiredException: raise MatrixRestError( 400, "M_SESSION_EXPIRED", "This validation session has expired: call requestToken again", ) except SessionNotValidatedException: raise MatrixRestError( 400, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed", ) res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid) return res
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 }
def render_POST(self, request): send_cors(request) authIfV2(self.sydent, request) args = get_args(request, ('email', 'client_secret', 'send_attempt')) email = args['email'] sendAttempt = args['send_attempt'] clientSecret = args['client_secret'] if not is_valid_client_secret(clientSecret): request.setResponseCode(400) return { 'errcode': 'M_INVALID_PARAM', 'error': 'Invalid client_secret provided' } ipaddress = self.sydent.ip_from_request(request) brand = self.sydent.brand_from_request(request) nextLink = None if 'next_link' in args and not args['next_link'].startswith( "file:///"): nextLink = args['next_link'] try: sid = self.sydent.validators.email.requestToken( email, clientSecret, sendAttempt, nextLink, ipaddress=ipaddress, brand=brand, ) resp = {'sid': str(sid)} except EmailAddressException: request.setResponseCode(400) resp = { 'errcode': 'M_INVALID_EMAIL', 'error': 'Invalid email address' } except EmailSendException: request.setResponseCode(500) resp = { 'errcode': 'M_EMAIL_SEND_ERROR', 'error': 'Failed to send email' } return resp
def render_GET(self, request): authIfV2(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 }
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()
async def render_POST(self, request: Request) -> JsonDict: send_cors(request) if self.require_auth: authV2(self.sydent, request) args = get_args( request, ("phone_number", "country", "client_secret", "send_attempt") ) raw_phone_number = args["phone_number"] country = args["country"] try: # See the comment handling `send_attempt` in emailservlet.py for # more context. sendAttempt = int(args["send_attempt"]) except (TypeError, ValueError): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": f"send_attempt should be an integer (got {args['send_attempt']}", } 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: phone_number_object = phonenumbers.parse(raw_phone_number, country) except Exception as e: logger.warning("Invalid phone number given: %r", e) request.setResponseCode(400) return { "errcode": "M_INVALID_PHONE_NUMBER", "error": "Invalid phone number", } msisdn = phonenumbers.format_number( phone_number_object, phonenumbers.PhoneNumberFormat.E164 )[1:] # International formatted number. The same as an E164 but with spaces # in appropriate places to make it nicer for the humans. intl_fmt = phonenumbers.format_number( phone_number_object, phonenumbers.PhoneNumberFormat.INTERNATIONAL ) brand = self.sydent.brand_from_request(request) try: sid = await self.sydent.validators.msisdn.requestToken( phone_number_object, clientSecret, sendAttempt, brand ) resp = { "success": True, "sid": str(sid), "msisdn": msisdn, "intl_fmt": intl_fmt, } except DestinationRejectedException: logger.warning("Destination rejected for number: %s", msisdn) request.setResponseCode(400) resp = { "errcode": "M_DESTINATION_REJECTED", "error": "Phone numbers in this country are not currently supported", } except Exception: logger.exception("Exception sending SMS") request.setResponseCode(500) resp = {"errcode": "M_UNKNOWN", "error": "Internal Server Error"} return resp
def render_POST(self, request: Request) -> JsonDict: send_cors(request) if self.require_auth: authV2(self.sydent, request) args = get_args(request, ("email", "client_secret", "send_attempt")) email = args["email"] clientSecret = args["client_secret"] try: # if we got this via the v1 API in a querystring or urlencoded body, # then the values in args will be a string. So check that # send_attempt is an int. # # NB: We don't check if we're processing a url-encoded v1 request. # This means we accept string representations of integers for # `send_attempt` in v2 requests, and in v1 requests that supply a # JSON body. This is contrary to the spec and leaves me with a dirty # feeling I can't quite shake off. # # Where's Raymond Hettinger when you need him? (THUMP) There must be # a better way! sendAttempt = int(args["send_attempt"]) except (TypeError, ValueError): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": f"send_attempt should be an integer (got {args['send_attempt']}", } if not is_valid_client_secret(clientSecret): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": "Invalid client_secret provided", } if not (0 < len(email) <= MAX_EMAIL_ADDRESS_LENGTH): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": "Invalid email provided" } ipaddress = self.sydent.ip_from_request(request) brand = self.sydent.brand_from_request(request) nextLink: Optional[str] = None if "next_link" in args and not args["next_link"].startswith( "file:///"): nextLink = args["next_link"] try: sid = self.sydent.validators.email.requestToken( email, clientSecret, sendAttempt, nextLink, ipaddress=ipaddress, brand=brand, ) resp = {"sid": str(sid)} except EmailAddressException: request.setResponseCode(400) resp = { "errcode": "M_INVALID_EMAIL", "error": "Invalid email address" } except EmailSendException: request.setResponseCode(500) resp = { "errcode": "M_EMAIL_SEND_ERROR", "error": "Failed to send email" } return resp
def _async_render_POST(self, request): try: try: # json.loads doesn't allow bytes in Python 3.5 body = json.loads(request.content.read().decode("UTF-8")) except ValueError: request.setResponseCode(400) 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(400) 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(400) 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(400) 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(401) 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(403) 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(403) 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 = yield self.sydent.sig_verifier.authenticate_request( request, body) except SignatureVerifyException as ex: request.setResponseCode(401) request.write( dict_to_json_bytes({ 'errcode': 'M_FORBIDDEN', 'error': str(ex) })) request.finish() return except NoAuthenticationError as ex: request.setResponseCode(401) request.write( dict_to_json_bytes({ 'errcode': 'M_FORBIDDEN', 'error': str(ex) })) request.finish() return except: logger.exception( "Exception whilst authenticating unbind request") request.setResponseCode(500) 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(403) 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(500) request.write( dict_to_json_bytes({ 'errcode': 'M_UNKNOWN', 'error': str(ex) })) request.finish()