def render_POST(self, request: Request) -> JsonDict: send_cors(request) if self.require_auth: authV2(self.sydent, request) args = get_args(request, ("private_key", "token", "mxid")) private_key_base64 = args["private_key"] token = args["token"] mxid = args["mxid"] sender = self.tokenStore.getSenderForToken(token) if sender is None: raise MatrixRestError(404, "M_UNRECOGNIZED", "Didn't recognize token") to_sign = { "mxid": mxid, "sender": sender, "token": token, } try: private_key = signedjson.key.decode_signing_key_base64( "ed25519", "0", private_key_base64 ) signed: JsonDict = signedjson.sign.sign_json( to_sign, self.server_name, private_key ) except Exception: logger.exception("signing failed") raise MatrixRestError(500, "M_UNKNOWN", "Internal Server Error") return signed
def render_POST(self, request: Request) -> JsonDict: send_cors(request) if self.require_auth: authV2(self.sydent, request) return self.do_validate_request(request)
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: Request) -> JsonDict: """ Invalidate the given access token """ send_cors(request) authV2(self.sydent, request, False) token = tokenFromRequest(request) if token is None: raise MatrixRestError(400, "M_MISSING_PARAMS", "Missing token") accountStore = AccountStore(self.sydent) accountStore.delToken(token) return {}
def render_POST(self, request: Request) -> JsonDict: """ Mark a set of terms and conditions as having been agreed to """ send_cors(request) account = authV2(self.sydent, request, False) args = get_args(request, ("user_accepts", )) user_accepts = args["user_accepts"] terms = get_terms(self.sydent) unknown_urls = list(set(user_accepts) - terms.getUrlSet()) if len(unknown_urls) > 0: raise MatrixRestError( 400, "M_UNKNOWN", "Unrecognised URLs: %s" % (", ".join(unknown_urls), )) termsStore = TermsStore(self.sydent) termsStore.addAgreedUrls(account.userId, user_accepts) all_accepted_urls = termsStore.getAgreedUrls(account.userId) if terms.urlListIsSufficient(all_accepted_urls): accountStore = AccountStore(self.sydent) accountStore.setConsentVersion(account.userId, terms.getMasterVersion()) return {}
def render_GET(self, request: Request) -> JsonDict: """ Return the hashing algorithms and pepper that this IS supports. The pepper included in the response is stored in the database, or otherwise generated. Returns: An object containing an array of hashing algorithms the server supports, and a `lookup_pepper` field, which is a server-defined value that the client should include in the 3PID information before hashing. """ send_cors(request) authV2(self.sydent, request) return { "algorithms": self.known_algorithms, "lookup_pepper": self.lookup_pepper, }
def render_GET(self, request: Request) -> JsonDict: """ Return information about the user's account (essentially just a 'who am i') """ send_cors(request) account = authV2(self.sydent, request) return { "user_id": account.userId, }
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_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" }
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) args = get_args( request, ( "medium", "address", "room_id", "sender", ), ) medium = args["medium"] address = args["address"] roomId = args["room_id"] sender = args["sender"] # ensure we are casefolding email address before storing normalised_address = normalise_address(address, medium) verified_sender = None if self.require_auth: account = authV2(self.sydent, request) verified_sender = sender if account.userId != sender: raise MatrixRestError(403, "M_UNAUTHORIZED", "'sender' doesn't match") globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, normalised_address) if mxid: request.setResponseCode(400) return { "errcode": "M_THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, } if medium != "email": request.setResponseCode(400) return { "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium, ), } if not (0 < len(address) <= MAX_EMAIL_ADDRESS_LENGTH): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": "Invalid email provided" } token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, normalised_address, roomId, sender, token) # Variables to substitute in the template. substitutions = {} # Include all arguments sent via the request. for k, v in args.items(): if isinstance(v, str): substitutions[k] = v substitutions["token"] = token # Substitutions that the template requires, but are optional to provide # to the API. extra_substitutions = [ "sender_display_name", "token", "room_name", "bracketed_room_name", "room_avatar_url", "sender_avatar_url", "guest_user_id", "guest_access_token", ] for k in extra_substitutions: substitutions.setdefault(k, "") # For MSC3288 room type, prefer the stable field, but fallback to the # unstable field. if "room_type" not in substitutions: substitutions["room_type"] = substitutions.get( "org.matrix.msc3288.room_type", "") substitutions["bracketed_verified_sender"] = "" if verified_sender: substitutions["bracketed_verified_sender"] = "(%s) " % ( verified_sender, ) substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != "": substitutions[ "bracketed_room_name"] = "(%s) " % substitutions["room_name"] substitutions[ "web_client_location"] = self.sydent.config.email.default_web_client_location if "org.matrix.web_client_location" in substitutions: substitutions["web_client_location"] = substitutions[ "org.matrix.web_client_location"] if substitutions["room_type"] == "m.space": subject = self.sydent.config.email.invite_subject_space % substitutions else: subject = self.sydent.config.email.invite_subject % substitutions substitutions["subject_header_value"] = Header(subject, "utf8").encode() brand = self.sydent.brand_from_request(request) # self.sydent.config.email.invite_template is deprecated if self.sydent.config.email.invite_template is None: templateFile = self.sydent.get_branded_template( brand, "invite_template.eml", ) else: templateFile = self.sydent.config.email.invite_template try: sendEmail(self.sydent, templateFile, normalised_address, substitutions) except EmailAddressException: request.setResponseCode(HTTPStatus.BAD_REQUEST) return { "errcode": "M_INVALID_EMAIL", "error": "Invalid email address" } pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % ( self.sydent.config.http.server_http_url_base, ) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact_email_address(address), } 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