def register_txn_path(servlet, regex_string, http_server, with_get=False): """Registers a transaction-based path. This registers two paths: PUT regex_string/$txnid POST regex_string Args: regex_string (str): The regex string to register. Must NOT have a trailing $ as this string will be appended to. http_server : The http_server to register paths with. with_get: True to also register respective GET paths for the PUTs. """ http_server.register_path( "POST", client_path_pattern(regex_string + "$"), servlet.on_POST ) http_server.register_path( "PUT", client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_PUT ) if with_get: http_server.register_path( "GET", client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_GET )
def register(self, http_server): PATTERN = "/createRoom" register_txn_path(self, PATTERN, http_server) # define CORS for all of /rooms in RoomCreateRestServlet for simplicity http_server.register_path("OPTIONS", client_path_pattern("/rooms(?:/.*)?$"), self.on_OPTIONS) # define CORS for /createRoom[/txnid] http_server.register_path("OPTIONS", client_path_pattern("/createRoom(?:/.*)?$"), self.on_OPTIONS)
class ProfileAvatarURLRestServlet(RestServlet): PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/avatar_url") @defer.inlineCallbacks def on_GET(self, request, user_id): user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) avatar_url = yield self.handlers.profile_handler.get_avatar_url(user, ) defer.returnValue((200, {"avatar_url": avatar_url})) @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) try: content = json.loads(request.content.read()) new_name = content["avatar_url"] except: defer.returnValue((400, "Unable to parse name")) yield self.handlers.profile_handler.set_avatar_url( user, auth_user, new_name) defer.returnValue((200, {})) def on_OPTIONS(self, request, user_id): return (200, {})
class ProfileDisplaynameRestServlet(RestServlet): PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/displayname") @defer.inlineCallbacks def on_GET(self, request, user_id): user = self.hs.parse_userid(user_id) displayname = yield self.handlers.profile_handler.get_displayname( user, ) defer.returnValue((200, {"displayname": displayname})) @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user = self.hs.parse_userid(user_id) try: content = json.loads(request.content.read()) new_name = content["displayname"] except: defer.returnValue((400, "Unable to parse name")) yield self.handlers.profile_handler.set_displayname( user, auth_user, new_name) defer.returnValue((200, {})) def on_OPTIONS(self, request, user_id): return (200, {})
class RoomTypingRestServlet(RestServlet): PATTERN = client_path_pattern( "/rooms/(?P<room_id>[^/]*)/typing/(?P<user_id>[^/]*)$") @defer.inlineCallbacks def on_PUT(self, request, room_id, user_id): auth_user = yield self.auth.get_user_by_req(request) room_id = urllib.unquote(room_id) target_user = self.hs.parse_userid(urllib.unquote(user_id)) content = _parse_json(request) typing_handler = self.handlers.typing_notification_handler if content["typing"]: yield typing_handler.started_typing( target_user=target_user, auth_user=auth_user, room_id=room_id, timeout=content.get("timeout", 30000), ) else: yield typing_handler.stopped_typing( target_user=target_user, auth_user=auth_user, room_id=room_id, ) defer.returnValue((200, {}))
class LoginRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login$") PASS_TYPE = "m.login.password" SAML2_TYPE = "m.login.saml2" def __init__(self, hs): super(LoginRestServlet, self).__init__(hs) self.idp_redirect_url = hs.config.saml2_idp_redirect_url self.saml2_enabled = hs.config.saml2_enabled def on_GET(self, request): flows = [{"type": LoginRestServlet.PASS_TYPE}] if self.saml2_enabled: flows.append({"type": LoginRestServlet.SAML2_TYPE}) return (200, {"flows": flows}) def on_OPTIONS(self, request): return (200, {}) @defer.inlineCallbacks def on_POST(self, request): login_submission = _parse_json(request) try: if login_submission["type"] == LoginRestServlet.PASS_TYPE: result = yield self.do_password_login(login_submission) defer.returnValue(result) elif self.saml2_enabled and (login_submission["type"] == LoginRestServlet.SAML2_TYPE): relay_state = "" if "relay_state" in login_submission: relay_state = "&RelayState=" + urllib.quote( login_submission["relay_state"]) result = {"uri": "%s%s" % (self.idp_redirect_url, relay_state)} defer.returnValue((200, result)) else: raise SynapseError(400, "Bad login type.") except KeyError: raise SynapseError(400, "Missing JSON keys.") @defer.inlineCallbacks def do_password_login(self, login_submission): if 'medium' in login_submission and 'address' in login_submission: user_id = yield self.hs.get_datastore().get_user_id_by_threepid( login_submission['medium'], login_submission['address']) else: user_id = login_submission['user'] if not user_id.startswith('@'): user_id = UserID.create(user_id, self.hs.hostname).to_string() user_id, token = yield self.handlers.auth_handler.login_with_password( user_id=user_id, password=login_submission["password"]) result = { "user_id": user_id, # may have changed "access_token": token, "home_server": self.hs.hostname, } defer.returnValue((200, result))
def register(self, http_server): # /room/$roomid/state/$eventtype no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$" # /room/$roomid/state/$eventtype/$statekey state_key = ("/rooms/(?P<room_id>[^/]*)/state/" "(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$") http_server.register_path("GET", client_path_pattern(state_key), self.on_GET) http_server.register_path("PUT", client_path_pattern(state_key), self.on_PUT) http_server.register_path("GET", client_path_pattern(no_state_key), self.on_GET_no_state_key) http_server.register_path("PUT", client_path_pattern(no_state_key), self.on_PUT_no_state_key)
class LoginFallbackRestServlet(RestServlet): PATTERN = client_path_pattern("/login/fallback$") def on_GET(self, request): # TODO(kegan): This should be returning some HTML which is capable of # hitting LoginRestServlet return (200, {})
class RoomInitialSyncRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$") @defer.inlineCallbacks def on_GET(self, request, room_id): user = yield self.auth.get_user_by_req(request) # TODO: Get all the initial sync data for this room and return in the # same format as initial sync, that is: # { # membership: join, # messages: [ # chunk: [ msg events ], # start: s_tok, # end: e_tok # ], # room_id: foo, # state: [ # { state event } , { state event } # ] # } # Probably worth keeping the keys room_id and membership for parity with # /initialSync even though they must be joined to sync this and know the # room ID, so clients can reuse the same code (room_id and membership # are MANDATORY for /initialSync, so the code will expect it to be # there) defer.returnValue((200, {}))
class VoipRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/voip/turnServer$") @defer.inlineCallbacks def on_GET(self, request): auth_user, client = yield self.auth.get_user_by_req(request) turnUris = self.hs.config.turn_uris turnSecret = self.hs.config.turn_shared_secret userLifetime = self.hs.config.turn_user_lifetime if not turnUris or not turnSecret or not userLifetime: defer.returnValue((200, {})) expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 username = "******" % (expiry, auth_user.to_string()) mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) # We need to use standard base64 encoding here, *not* syutil's # encode_base64 because we need to add the standard padding to get the # same result as the TURN server. password = base64.b64encode(mac.digest()) defer.returnValue((200, { 'username': username, 'password': password, 'ttl': userLifetime / 1000, 'uris': turnUris, })) def on_OPTIONS(self, request): return (200, {})
class RoomMemberListRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$") @defer.inlineCallbacks def on_GET(self, request, room_id): # TODO support Pagination stream API (limit/tokens) user = yield self.auth.get_user_by_req(request) handler = self.handlers.room_member_handler members = yield handler.get_room_members_as_pagination_chunk( room_id=urllib.unquote(room_id), user_id=user.to_string()) for event in members["chunk"]: # FIXME: should probably be state_key here, not user_id target_user = self.hs.parse_userid(event["user_id"]) # Presence is an optional cache; don't fail if we can't fetch it try: presence_state = yield self.handlers.presence_handler.get_state( target_user=target_user, auth_user=user ) event["content"].update(presence_state) except: pass defer.returnValue((200, members))
class PublicRoomListRestServlet(RestServlet): PATTERN = client_path_pattern("/publicRooms$") @defer.inlineCallbacks def on_GET(self, request): handler = self.handlers.room_list_handler data = yield handler.get_public_room_list() defer.returnValue((200, data))
class RoomStateRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$") @defer.inlineCallbacks def on_GET(self, request, room_id): user = yield self.auth.get_user_by_req(request) # TODO: Get all the current state for this room and return in the same # format as initial sync, that is: # [ # { state event }, { state event } # ] defer.returnValue((200, []))
class RoomStateRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$") @defer.inlineCallbacks def on_GET(self, request, room_id): user = yield self.auth.get_user_by_req(request) handler = self.handlers.message_handler # Get all the current state for this room events = yield handler.get_state_events( room_id=room_id, user_id=user.to_string(), ) defer.returnValue((200, events))
class RoomInitialSyncRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$") @defer.inlineCallbacks def on_GET(self, request, room_id): user = yield self.auth.get_user_by_req(request) pagination_config = PaginationConfig.from_request(request) content = yield self.handlers.message_handler.room_initial_sync( room_id=room_id, user_id=user.to_string(), pagin_config=pagination_config, ) defer.returnValue((200, content))
class RoomTriggerBackfill(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$") @defer.inlineCallbacks def on_GET(self, request, room_id): remote_server = urllib.unquote(request.args["remote"][0]) room_id = urllib.unquote(room_id) limit = int(request.args["limit"][0]) handler = self.handlers.federation_handler events = yield handler.backfill(remote_server, room_id, limit) res = [self.hs.serialize_event(event) for event in events] defer.returnValue((200, res))
class ClientDirectoryServer(RestServlet): PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$") @defer.inlineCallbacks def on_GET(self, request, room_alias): room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias)) dir_handler = self.handlers.directory_handler res = yield dir_handler.get_association(room_alias) defer.returnValue((200, res)) @defer.inlineCallbacks def on_PUT(self, request, room_alias): user = yield self.auth.get_user_by_req(request) content = _parse_json(request) if not "room_id" in content: raise SynapseError(400, "Missing room_id key", errcode=Codes.BAD_JSON) logger.debug("Got content: %s", content) room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias)) logger.debug("Got room name: %s", room_alias.to_string()) room_id = content["room_id"] servers = content["servers"] if "servers" in content else None logger.debug("Got room_id: %s", room_id) logger.debug("Got servers: %s", servers) # TODO(erikj): Check types. # TODO(erikj): Check that room exists dir_handler = self.handlers.directory_handler try: yield dir_handler.create_association(user.to_string(), room_alias, room_id, servers) except SynapseError as e: raise e except: logger.exception("Failed to create association") raise defer.returnValue((200, {}))
class InitialSyncRestServlet(RestServlet): PATTERN = client_path_pattern("/initialSync$") @defer.inlineCallbacks def on_GET(self, request): user = yield self.auth.get_user_by_req(request) with_feedback = "feedback" in request.args pagination_config = PaginationConfig.from_request(request) handler = self.handlers.message_handler content = yield handler.snapshot_all_rooms( user_id=user.to_string(), pagin_config=pagination_config, feedback=with_feedback) defer.returnValue((200, content))
class RoomMessageListRestServlet(RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$") @defer.inlineCallbacks def on_GET(self, request, room_id): user = yield self.auth.get_user_by_req(request) pagination_config = PaginationConfig.from_request(request) with_feedback = "feedback" in request.args handler = self.handlers.message_handler msgs = yield handler.get_messages( room_id=urllib.unquote(room_id), user_id=user.to_string(), pagin_config=pagination_config, feedback=with_feedback) defer.returnValue((200, msgs))
class ProfileRestServlet(RestServlet): PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)") @defer.inlineCallbacks def on_GET(self, request, user_id): user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) displayname = yield self.handlers.profile_handler.get_displayname( user, ) avatar_url = yield self.handlers.profile_handler.get_avatar_url(user, ) defer.returnValue((200, { "displayname": displayname, "avatar_url": avatar_url }))
class PasswordResetRestServlet(RestServlet): PATTERN = client_path_pattern("/login/reset") @defer.inlineCallbacks def on_POST(self, request): reset_info = _parse_json(request) try: email = reset_info["email"] user_id = reset_info["user_id"] handler = self.handlers.login_handler yield handler.reset_password(user_id, email) # purposefully give no feedback to avoid people hammering different # combinations. defer.returnValue((200, {})) except KeyError: raise SynapseError( 400, "Missing keys. Requires 'email' and 'user_id'.")
class PresenceStatusRestServlet(RestServlet): PATTERN = client_path_pattern("/presence/(?P<user_id>[^/]*)/status") @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) state = yield self.handlers.presence_handler.get_state( target_user=user, auth_user=auth_user) defer.returnValue((200, state)) @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) state = {} try: content = json.loads(request.content.read()) state["presence"] = content.pop("presence") if "status_msg" in content: state["status_msg"] = content.pop("status_msg") if not isinstance(state["status_msg"], basestring): raise SynapseError(400, "status_msg must be a string.") if content: raise KeyError() except SynapseError as e: raise e except: raise SynapseError(400, "Unable to parse state") yield self.handlers.presence_handler.set_state( target_user=user, auth_user=auth_user, state=state) defer.returnValue((200, "")) def on_OPTIONS(self, request): return (200, {})
class WhoisRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/admin/whois/(?P<user_id>[^/]*)") @defer.inlineCallbacks def on_GET(self, request, user_id): target_user = UserID.from_string(user_id) auth_user, client = yield self.auth.get_user_by_req(request) is_admin = yield self.auth.is_server_admin(auth_user) if not is_admin and target_user != auth_user: raise AuthError(403, "You are not a server admin") if not self.hs.is_mine(target_user): raise SynapseError(400, "Can only whois a local user") ret = yield self.handlers.admin_handler.get_whois(target_user) defer.returnValue((200, ret))
class SAML2RestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login/saml2") def __init__(self, hs): super(SAML2RestServlet, self).__init__(hs) self.sp_config = hs.config.saml2_config_path @defer.inlineCallbacks def on_POST(self, request): saml2_auth = None try: conf = config.SPConfig() conf.load_file(self.sp_config) SP = Saml2Client(conf) saml2_auth = SP.parse_authn_request_response( request.args['SAMLResponse'][0], BINDING_HTTP_POST) except Exception, e: # Not authenticated logger.exception(e) if saml2_auth and saml2_auth.status_ok() and not saml2_auth.not_signed: username = saml2_auth.name_id.text handler = self.handlers.registration_handler (user_id, token) = yield handler.register_saml2(username) # Forward to the RelayState callback along with ava if 'RelayState' in request.args: request.redirect( urllib.unquote(request.args['RelayState'][0]) + '?status=authenticated&access_token=' + token + '&user_id=' + user_id + '&ava=' + urllib.quote(json.dumps(saml2_auth.ava))) request.finish() defer.returnValue(None) defer.returnValue((200, { "status": "authenticated", "user_id": user_id, "token": token, "ava": saml2_auth.ava })) elif 'RelayState' in request.args: request.redirect( urllib.unquote(request.args['RelayState'][0]) + '?status=not_authenticated') request.finish() defer.returnValue(None) defer.returnValue((200, {"status": "not_authenticated"}))
class RoomMessageListRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$") @defer.inlineCallbacks def on_GET(self, request, room_id): user, client = yield self.auth.get_user_by_req(request) pagination_config = PaginationConfig.from_request( request, default_limit=10, ) with_feedback = "feedback" in request.args as_client_event = "raw" not in request.args handler = self.handlers.message_handler msgs = yield handler.get_messages(room_id=room_id, user_id=user.to_string(), pagin_config=pagination_config, feedback=with_feedback, as_client_event=as_client_event) defer.returnValue((200, msgs))
class RoomTriggerBackfill(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$") def __init__(self, hs): super(RoomTriggerBackfill, self).__init__(hs) self.clock = hs.get_clock() @defer.inlineCallbacks def on_GET(self, request, room_id): remote_server = urllib.unquote( request.args["remote"][0]).decode("UTF-8") limit = int(request.args["limit"][0]) handler = self.handlers.federation_handler events = yield handler.backfill(remote_server, room_id, limit) time_now = self.clock.time_msec() res = [serialize_event(event, time_now) for event in events] defer.returnValue((200, res))
class LoginRestServlet(RestServlet): PATTERN = client_path_pattern("/login$") PASS_TYPE = "m.login.password" def on_GET(self, request): return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]}) def on_OPTIONS(self, request): return (200, {}) @defer.inlineCallbacks def on_POST(self, request): login_submission = _parse_json(request) try: if login_submission["type"] == LoginRestServlet.PASS_TYPE: result = yield self.do_password_login(login_submission) defer.returnValue(result) else: raise SynapseError(400, "Bad login type.") except KeyError: raise SynapseError(400, "Missing JSON keys.") @defer.inlineCallbacks def do_password_login(self, login_submission): if not login_submission["user"].startswith('@'): login_submission["user"] = UserID.create( login_submission["user"], self.hs.hostname).to_string() handler = self.handlers.login_handler token = yield handler.login( user=login_submission["user"], password=login_submission["password"]) result = { "user_id": login_submission["user"], # may have changed "access_token": token, "home_server": self.hs.hostname, } defer.returnValue((200, result))
class PresenceListRestServlet(RestServlet): PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)") @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user = self.hs.parse_userid(user_id) if not self.hs.is_mine(user): raise SynapseError(400, "User not hosted on this Home Server") if auth_user != user: raise SynapseError(400, "Cannot get another user's presence list") presence = yield self.handlers.presence_handler.get_presence_list( observer_user=user, accepted=True) for p in presence: observed_user = p.pop("observed_user") p["user_id"] = observed_user.to_string() defer.returnValue((200, presence)) @defer.inlineCallbacks def on_POST(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) user = self.hs.parse_userid(user_id) if not self.hs.is_mine(user): raise SynapseError(400, "User not hosted on this Home Server") if auth_user != user: raise SynapseError( 400, "Cannot modify another user's presence list") try: content = json.loads(request.content.read()) except: logger.exception("JSON parse error") raise SynapseError(400, "Unable to parse content") if "invite" in content: for u in content["invite"]: if not isinstance(u, basestring): raise SynapseError(400, "Bad invite value.") if len(u) == 0: continue invited_user = self.hs.parse_userid(u) yield self.handlers.presence_handler.send_invite( observer_user=user, observed_user=invited_user ) if "drop" in content: for u in content["drop"]: if not isinstance(u, basestring): raise SynapseError(400, "Bad drop value.") if len(u) == 0: continue dropped_user = self.hs.parse_userid(u) yield self.handlers.presence_handler.drop( observer_user=user, observed_user=dropped_user ) defer.returnValue((200, {})) def on_OPTIONS(self, request): return (200, {})
class RegisterRestServlet(RestServlet): """Handles registration with the home server. This servlet is in control of the registration flow; the registration handler doesn't have a concept of multi-stages or sessions. """ PATTERN = client_path_pattern("/register$") def __init__(self, hs): super(RegisterRestServlet, self).__init__(hs) # sessions are stored as: # self.sessions = { # "session_id" : { __session_dict__ } # } # TODO: persistent storage self.sessions = {} def on_GET(self, request): if self.hs.config.enable_registration_captcha: return (200, { "flows": [{ "type": LoginType.RECAPTCHA, "stages": ([ LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY, LoginType.PASSWORD ]) }, { "type": LoginType.RECAPTCHA, "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] }] }) else: return (200, { "flows": [{ "type": LoginType.EMAIL_IDENTITY, "stages": ([LoginType.EMAIL_IDENTITY, LoginType.PASSWORD]) }, { "type": LoginType.PASSWORD }] }) @defer.inlineCallbacks def on_POST(self, request): register_json = _parse_json(request) session = (register_json["session"] if "session" in register_json else None) login_type = None if "type" not in register_json: raise SynapseError(400, "Missing 'type' key.") try: login_type = register_json["type"] stages = { LoginType.RECAPTCHA: self._do_recaptcha, LoginType.PASSWORD: self._do_password, LoginType.EMAIL_IDENTITY: self._do_email_identity } session_info = self._get_session_info(request, session) logger.debug("%s : session info %s request info %s", login_type, session_info, register_json) response = yield stages[login_type](request, register_json, session_info) if "access_token" not in response: # isn't a final response response["session"] = session_info["id"] defer.returnValue((200, response)) except KeyError as e: logger.exception(e) raise SynapseError( 400, "Missing JSON keys for login type %s." % login_type) def on_OPTIONS(self, request): return (200, {}) def _get_session_info(self, request, session_id): if not session_id: # create a new session while session_id is None or session_id in self.sessions: session_id = stringutils.random_string(24) self.sessions[session_id] = { "id": session_id, LoginType.EMAIL_IDENTITY: False, LoginType.RECAPTCHA: False } return self.sessions[session_id] def _save_session(self, session): # TODO: Persistent storage logger.debug("Saving session %s", session) self.sessions[session["id"]] = session def _remove_session(self, session): logger.debug("Removing session %s", session) self.sessions.pop(session["id"]) @defer.inlineCallbacks def _do_recaptcha(self, request, register_json, session): if not self.hs.config.enable_registration_captcha: raise SynapseError(400, "Captcha not required.") challenge = None user_response = None try: challenge = register_json["challenge"] user_response = register_json["response"] except KeyError: raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) # May be an X-Forwarding-For header depending on config ip_addr = request.getClientIP() if self.hs.config.captcha_ip_origin_is_x_forwarded: # use the header if request.requestHeaders.hasHeader("X-Forwarded-For"): ip_addr = request.requestHeaders.getRawHeaders( "X-Forwarded-For")[0] handler = self.handlers.registration_handler yield handler.check_recaptcha(ip_addr, self.hs.config.recaptcha_private_key, challenge, user_response) session[LoginType.RECAPTCHA] = True # mark captcha as done self._save_session(session) defer.returnValue( {"next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]}) @defer.inlineCallbacks def _do_email_identity(self, request, register_json, session): if (self.hs.config.enable_registration_captcha and not session[LoginType.RECAPTCHA]): raise SynapseError(400, "Captcha is required.") threepidCreds = register_json['threepidCreds'] handler = self.handlers.registration_handler yield handler.register_email(threepidCreds) session["threepidCreds"] = threepidCreds # store creds for next stage session[LoginType.EMAIL_IDENTITY] = True # mark email as done self._save_session(session) defer.returnValue({"next": LoginType.PASSWORD}) @defer.inlineCallbacks def _do_password(self, request, register_json, session): if (self.hs.config.enable_registration_captcha and not session[LoginType.RECAPTCHA]): # captcha should've been done by this stage! raise SynapseError(400, "Captcha is required.") password = register_json["password"].encode("utf-8") desired_user_id = (register_json["user"].encode("utf-8") if "user" in register_json else None) if desired_user_id and urllib.quote( desired_user_id) != desired_user_id: raise SynapseError( 400, "User ID must only contain characters which do not " + "require URL encoding.") handler = self.handlers.registration_handler (user_id, token) = yield handler.register(localpart=desired_user_id, password=password) if session[LoginType.EMAIL_IDENTITY]: yield handler.bind_emails(user_id, session["threepidCreds"]) result = { "user_id": user_id, "access_token": token, "home_server": self.hs.hostname, } self._remove_session(session) defer.returnValue(result)
class RegisterRestServlet(ClientV1RestServlet): """Handles registration with the home server. This servlet is in control of the registration flow; the registration handler doesn't have a concept of multi-stages or sessions. """ PATTERN = client_path_pattern("/register$") def __init__(self, hs): super(RegisterRestServlet, self).__init__(hs) # sessions are stored as: # self.sessions = { # "session_id" : { __session_dict__ } # } # TODO: persistent storage self.sessions = {} def on_GET(self, request): if self.hs.config.enable_registration_captcha: return ( 200, {"flows": [ { "type": LoginType.RECAPTCHA, "stages": [ LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY, LoginType.PASSWORD ] }, { "type": LoginType.RECAPTCHA, "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] } ]} ) else: return ( 200, {"flows": [ { "type": LoginType.EMAIL_IDENTITY, "stages": [ LoginType.EMAIL_IDENTITY, LoginType.PASSWORD ] }, { "type": LoginType.PASSWORD } ]} ) @defer.inlineCallbacks def on_POST(self, request): register_json = _parse_json(request) session = (register_json["session"] if "session" in register_json else None) login_type = None if "type" not in register_json: raise SynapseError(400, "Missing 'type' key.") try: login_type = register_json["type"] stages = { LoginType.RECAPTCHA: self._do_recaptcha, LoginType.PASSWORD: self._do_password, LoginType.EMAIL_IDENTITY: self._do_email_identity, LoginType.APPLICATION_SERVICE: self._do_app_service } session_info = self._get_session_info(request, session) logger.debug("%s : session info %s request info %s", login_type, session_info, register_json) response = yield stages[login_type]( request, register_json, session_info ) if "access_token" not in response: # isn't a final response response["session"] = session_info["id"] defer.returnValue((200, response)) except KeyError as e: logger.exception(e) raise SynapseError(400, "Missing JSON keys for login type %s." % ( login_type, )) def on_OPTIONS(self, request): return (200, {}) def _get_session_info(self, request, session_id): if not session_id: # create a new session while session_id is None or session_id in self.sessions: session_id = stringutils.random_string(24) self.sessions[session_id] = { "id": session_id, LoginType.EMAIL_IDENTITY: False, LoginType.RECAPTCHA: False } return self.sessions[session_id] def _save_session(self, session): # TODO: Persistent storage logger.debug("Saving session %s", session) self.sessions[session["id"]] = session def _remove_session(self, session): logger.debug("Removing session %s", session) self.sessions.pop(session["id"]) @defer.inlineCallbacks def _do_recaptcha(self, request, register_json, session): if not self.hs.config.enable_registration_captcha: raise SynapseError(400, "Captcha not required.") yield self._check_recaptcha(request, register_json, session) session[LoginType.RECAPTCHA] = True # mark captcha as done self._save_session(session) defer.returnValue({ "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] }) @defer.inlineCallbacks def _check_recaptcha(self, request, register_json, session): if ("captcha_bypass_hmac" in register_json and self.hs.config.captcha_bypass_secret): if "user" not in register_json: raise SynapseError(400, "Captcha bypass needs 'user'") want = hmac.new( key=self.hs.config.captcha_bypass_secret, msg=register_json["user"], digestmod=sha1, ).hexdigest() # str() because otherwise hmac complains that 'unicode' does not # have the buffer interface got = str(register_json["captcha_bypass_hmac"]) if compare_digest(want, got): session["user"] = register_json["user"] defer.returnValue(None) else: raise SynapseError( 400, "Captcha bypass HMAC incorrect", errcode=Codes.CAPTCHA_NEEDED ) challenge = None user_response = None try: challenge = register_json["challenge"] user_response = register_json["response"] except KeyError: raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) ip_addr = self.hs.get_ip_from_request(request) handler = self.handlers.registration_handler yield handler.check_recaptcha( ip_addr, self.hs.config.recaptcha_private_key, challenge, user_response ) @defer.inlineCallbacks def _do_email_identity(self, request, register_json, session): if (self.hs.config.enable_registration_captcha and not session[LoginType.RECAPTCHA]): raise SynapseError(400, "Captcha is required.") threepidCreds = register_json['threepidCreds'] handler = self.handlers.registration_handler logger.debug("Registering email. threepidcreds: %s" % (threepidCreds)) yield handler.register_email(threepidCreds) session["threepidCreds"] = threepidCreds # store creds for next stage session[LoginType.EMAIL_IDENTITY] = True # mark email as done self._save_session(session) defer.returnValue({ "next": LoginType.PASSWORD }) @defer.inlineCallbacks def _do_password(self, request, register_json, session): yield run_on_reactor() if (self.hs.config.enable_registration_captcha and not session[LoginType.RECAPTCHA]): # captcha should've been done by this stage! raise SynapseError(400, "Captcha is required.") if ("user" in session and "user" in register_json and session["user"] != register_json["user"]): raise SynapseError( 400, "Cannot change user ID during registration" ) password = register_json["password"].encode("utf-8") desired_user_id = (register_json["user"].encode("utf-8") if "user" in register_json else None) if (desired_user_id and urllib.quote(desired_user_id) != desired_user_id): raise SynapseError( 400, "User ID must only contain characters which do not " + "require URL encoding.") handler = self.handlers.registration_handler (user_id, token) = yield handler.register( localpart=desired_user_id, password=password ) if session[LoginType.EMAIL_IDENTITY]: logger.debug("Binding emails %s to %s" % ( session["threepidCreds"], user_id) ) yield handler.bind_emails(user_id, session["threepidCreds"]) result = { "user_id": user_id, "access_token": token, "home_server": self.hs.hostname, } self._remove_session(session) defer.returnValue(result) @defer.inlineCallbacks def _do_app_service(self, request, register_json, session): if "access_token" not in request.args: raise SynapseError(400, "Expected application service token.") if "user" not in register_json: raise SynapseError(400, "Expected 'user' key.") as_token = request.args["access_token"][0] user_localpart = register_json["user"].encode("utf-8") handler = self.handlers.registration_handler (user_id, token) = yield handler.appservice_register( user_localpart, as_token ) self._remove_session(session) defer.returnValue({ "user_id": user_id, "access_token": token, "home_server": self.hs.hostname, })