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_paths( "POST", client_path_patterns(regex_string + "$"), servlet.on_POST ) http_server.register_paths( "PUT", client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_PUT ) if with_get: http_server.register_paths( "GET", client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_GET )
def register(self, http_server): PATTERNS = "/createRoom" register_txn_path(self, PATTERNS, http_server) # define CORS for all of /rooms in RoomCreateRestServlet for simplicity http_server.register_paths("OPTIONS", client_path_patterns("/rooms(?:/.*)?$"), self.on_OPTIONS) # define CORS for /createRoom[/txnid] http_server.register_paths( "OPTIONS", client_path_patterns("/createRoom(?:/.*)?$"), self.on_OPTIONS)
def register(self, http_server): PATTERNS = "/createRoom" register_txn_path(self, PATTERNS, http_server) # define CORS for all of /rooms in RoomCreateRestServlet for simplicity http_server.register_paths("OPTIONS", client_path_patterns("/rooms(?:/.*)?$"), self.on_OPTIONS) # define CORS for /createRoom[/txnid] http_server.register_paths("OPTIONS", client_path_patterns("/createRoom(?:/.*)?$"), self.on_OPTIONS)
class RoomEventContext(ClientV1RestServlet): PATTERNS = client_path_patterns( "/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$") def __init__(self, hs): super(RoomEventContext, self).__init__(hs) self.clock = hs.get_clock() @defer.inlineCallbacks def on_GET(self, request, room_id, event_id): user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) limit = int(request.args.get("limit", [10])[0]) results = yield self.handlers.room_context_handler.get_event_context( user, room_id, event_id, limit, is_guest) time_now = self.clock.time_msec() results["events_before"] = [ serialize_event(event, time_now) for event in results["events_before"] ] results["events_after"] = [ serialize_event(event, time_now) for event in results["events_after"] ] results["state"] = [ serialize_event(event, time_now) for event in results["state"] ] logger.info("Responding with %r", results) defer.returnValue((200, results))
class RoomTypingRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns( "/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 = UserID.from_string(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 RoomMemberListRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/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.message_handler events = yield handler.get_state_events( room_id=room_id, user_id=user.to_string(), ) chunk = [] for event in events: if event["type"] != EventTypes.Member: continue chunk.append(event) # FIXME: should probably be state_key here, not user_id target_user = UserID.from_string(event["user_id"]) # Presence is an optional cache; don't fail if we can't fetch it try: presence_handler = self.handlers.presence_handler presence_state = yield presence_handler.get_state( target_user=target_user, auth_user=user) event["content"].update(presence_state) except: pass defer.returnValue((200, {"chunk": chunk}))
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_paths("GET", client_path_patterns(state_key), self.on_GET) http_server.register_paths("PUT", client_path_patterns(state_key), self.on_PUT) http_server.register_paths("GET", client_path_patterns(no_state_key), self.on_GET_no_state_key) http_server.register_paths("PUT", client_path_patterns(no_state_key), self.on_PUT_no_state_key)
class PublicRoomListRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/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 CasRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/login/cas", releases=()) def __init__(self, hs): super(CasRestServlet, self).__init__(hs) self.cas_server_url = hs.config.cas_server_url def on_GET(self, request): return (200, {"serverUrl": self.cas_server_url})
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_paths("POST", client_path_patterns(regex_string + "$"), servlet.on_POST) http_server.register_paths( "PUT", client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_PUT) if with_get: http_server.register_paths( "GET", client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"), servlet.on_GET)
class SearchRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/search$") @defer.inlineCallbacks def on_POST(self, request): auth_user, _, _ = yield self.auth.get_user_by_req(request) content = _parse_json(request) batch = request.args.get("next_batch", [None])[0] results = yield self.handlers.search_handler.search( auth_user, content, batch) defer.returnValue((200, results))
class RoomInitialSyncRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/initialSync$") @defer.inlineCallbacks def on_GET(self, request, room_id): user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) 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, is_guest=is_guest, ) defer.returnValue((200, content))
class RoomStateRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/state$") @defer.inlineCallbacks def on_GET(self, request, room_id): user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) 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(), is_guest=is_guest, ) defer.returnValue((200, events))
class InitialSyncRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/initialSync$") @defer.inlineCallbacks def on_GET(self, request): user, _, _ = yield self.auth.get_user_by_req(request) as_client_event = "raw" not in request.args pagination_config = PaginationConfig.from_request(request) handler = self.handlers.message_handler include_archived = request.args.get("archived", None) == ["true"] content = yield handler.snapshot_all_rooms( user_id=user.to_string(), pagin_config=pagination_config, as_client_event=as_client_event, include_archived=include_archived, ) defer.returnValue((200, content))
class WhoisRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/admin/whois/(?P<user_id>[^/]*)") @defer.inlineCallbacks def on_GET(self, request, user_id): target_user = UserID.from_string(user_id) auth_user, _, _ = 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 RoomMessageListRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/messages$") @defer.inlineCallbacks def on_GET(self, request, room_id): user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) pagination_config = PaginationConfig.from_request( request, default_limit=10, ) 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(), is_guest=is_guest, pagin_config=pagination_config, as_client_event=as_client_event) defer.returnValue((200, msgs))
class SAML2RestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/login/saml2", releases=()) 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 CasRedirectServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/login/cas/redirect", releases=()) def __init__(self, hs): super(CasRedirectServlet, self).__init__(hs) self.cas_server_url = hs.config.cas_server_url self.cas_service_url = hs.config.cas_service_url def on_GET(self, request): args = request.args if "redirectUrl" not in args: return (400, "Redirect URL not specified for CAS auth") client_redirect_url_param = urllib.urlencode({ "redirectUrl": args["redirectUrl"][0] }) hs_redirect_url = self.cas_service_url + "/_matrix/client/api/v1/login/cas/ticket" service_param = urllib.urlencode({ "service": "%s?%s" % (hs_redirect_url, client_redirect_url_param) }) request.redirect("%s?%s" % (self.cas_server_url, service_param)) request.finish()
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. """ PATTERNS = client_path_patterns("/register$", releases=(), include_in_unstable=False) def __init__(self, hs): super(RegisterRestServlet, self).__init__(hs) # sessions are stored as: # self.sessions = { # "session_id" : { __session_dict__ } # } # TODO: persistent storage self.sessions = {} self.disable_registration = hs.config.disable_registration 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"] is_application_server = login_type == LoginType.APPLICATION_SERVICE is_using_shared_secret = login_type == LoginType.SHARED_SECRET can_register = (not self.disable_registration or is_application_server or is_using_shared_secret) if not can_register: raise SynapseError(403, "Registration has been disabled") 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, LoginType.SHARED_SECRET: self._do_shared_secret, } 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) 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, }) @defer.inlineCallbacks def _do_shared_secret(self, request, register_json, session): yield run_on_reactor() if not isinstance(register_json.get("mac", None), basestring): raise SynapseError(400, "Expected mac.") if not isinstance(register_json.get("user", None), basestring): raise SynapseError(400, "Expected 'user' key.") if not isinstance(register_json.get("password", None), basestring): raise SynapseError(400, "Expected 'password' key.") if not self.hs.config.registration_shared_secret: raise SynapseError(400, "Shared secret registration is not enabled") user = register_json["user"].encode("utf-8") # str() because otherwise hmac complains that 'unicode' does not # have the buffer interface got_mac = str(register_json["mac"]) want_mac = hmac.new( key=self.hs.config.registration_shared_secret, msg=user, digestmod=sha1, ).hexdigest() password = register_json["password"].encode("utf-8") if compare_digest(want_mac, got_mac): handler = self.handlers.registration_handler user_id, token = yield handler.register( localpart=user, password=password, ) self._remove_session(session) defer.returnValue({ "user_id": user_id, "access_token": token, "home_server": self.hs.hostname, }) else: raise SynapseError( 403, "HMAC incorrect", )
class LoginRestServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/login$") PASS_TYPE = "m.login.password" SAML2_TYPE = "m.login.saml2" CAS_TYPE = "m.login.cas" TOKEN_TYPE = "m.login.token" def __init__(self, hs): super(LoginRestServlet, self).__init__(hs) self.idp_redirect_url = hs.config.saml2_idp_redirect_url self.password_enabled = hs.config.password_enabled self.saml2_enabled = hs.config.saml2_enabled self.cas_enabled = hs.config.cas_enabled self.cas_server_url = hs.config.cas_server_url self.cas_required_attributes = hs.config.cas_required_attributes self.servername = hs.config.server_name self.http_client = hs.get_simple_http_client() def on_GET(self, request): flows = [] if self.saml2_enabled: flows.append({"type": LoginRestServlet.SAML2_TYPE}) if self.cas_enabled: flows.append({"type": LoginRestServlet.CAS_TYPE}) # While its valid for us to advertise this login type generally, # synapse currently only gives out these tokens as part of the # CAS login flow. # Generally we don't want to advertise login flows that clients # don't know how to implement, since they (currently) will always # fall back to the fallback API if they don't understand one of the # login flow types returned. flows.append({"type": LoginRestServlet.TOKEN_TYPE}) if self.password_enabled: flows.append({"type": LoginRestServlet.PASS_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: if not self.password_enabled: raise SynapseError(400, "Password login has been disabled.") 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)) # TODO Delete this after all CAS clients switch to token login instead elif self.cas_enabled and (login_submission["type"] == LoginRestServlet.CAS_TYPE): uri = "%s/proxyValidate" % (self.cas_server_url,) args = { "ticket": login_submission["ticket"], "service": login_submission["service"] } body = yield self.http_client.get_raw(uri, args) result = yield self.do_cas_login(body) defer.returnValue(result) elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: result = yield self.do_token_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 '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'] ) if not user_id: raise LoginError(403, "", errcode=Codes.FORBIDDEN) else: user_id = login_submission['user'] if not user_id.startswith('@'): user_id = UserID.create( user_id, self.hs.hostname ).to_string() auth_handler = self.handlers.auth_handler user_id, access_token, refresh_token = yield auth_handler.login_with_password( user_id=user_id, password=login_submission["password"]) result = { "user_id": user_id, # may have changed "access_token": access_token, "refresh_token": refresh_token, "home_server": self.hs.hostname, } defer.returnValue((200, result)) @defer.inlineCallbacks def do_token_login(self, login_submission): token = login_submission['token'] auth_handler = self.handlers.auth_handler user_id = ( yield auth_handler.validate_short_term_login_token_and_get_user_id(token) ) user_id, access_token, refresh_token = ( yield auth_handler.get_login_tuple_for_user_id(user_id) ) result = { "user_id": user_id, # may have changed "access_token": access_token, "refresh_token": refresh_token, "home_server": self.hs.hostname, } defer.returnValue((200, result)) # TODO Delete this after all CAS clients switch to token login instead @defer.inlineCallbacks def do_cas_login(self, cas_response_body): user, attributes = self.parse_cas_response(cas_response_body) for required_attribute, required_value in self.cas_required_attributes.items(): # If required attribute was not in CAS Response - Forbidden if required_attribute not in attributes: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) # Also need to check value if required_value is not None: actual_value = attributes[required_attribute] # If required attribute value does not match expected - Forbidden if required_value != actual_value: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) user_id = UserID.create(user, self.hs.hostname).to_string() auth_handler = self.handlers.auth_handler user_exists = yield auth_handler.does_user_exist(user_id) if user_exists: user_id, access_token, refresh_token = ( yield auth_handler.get_login_tuple_for_user_id(user_id) ) result = { "user_id": user_id, # may have changed "access_token": access_token, "refresh_token": refresh_token, "home_server": self.hs.hostname, } else: user_id, access_token = ( yield self.handlers.registration_handler.register(localpart=user) ) result = { "user_id": user_id, # may have changed "access_token": access_token, "home_server": self.hs.hostname, } defer.returnValue((200, result)) # TODO Delete this after all CAS clients switch to token login instead def parse_cas_response(self, cas_response_body): root = ET.fromstring(cas_response_body) if not root.tag.endswith("serviceResponse"): raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) if not root[0].tag.endswith("authenticationSuccess"): raise LoginError(401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED) for child in root[0]: if child.tag.endswith("user"): user = child.text if child.tag.endswith("attributes"): attributes = {} for attribute in child: # ElementTree library expands the namespace in attribute tags # to the full URL of the namespace. # See (https://docs.python.org/2/library/xml.etree.elementtree.html) # We don't care about namespace here and it will always be encased in # curly braces, so we remove them. if "}" in attribute.tag: attributes[attribute.tag.split("}")[1]] = attribute.text else: attributes[attribute.tag] = attribute.text if user is None or attributes is None: raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) return (user, attributes)
class CasTicketServlet(ClientV1RestServlet): PATTERNS = client_path_patterns("/login/cas/ticket", releases=()) def __init__(self, hs): super(CasTicketServlet, self).__init__(hs) self.cas_server_url = hs.config.cas_server_url self.cas_service_url = hs.config.cas_service_url self.cas_required_attributes = hs.config.cas_required_attributes @defer.inlineCallbacks def on_GET(self, request): client_redirect_url = request.args["redirectUrl"][0] http_client = self.hs.get_simple_http_client() uri = self.cas_server_url + "/proxyValidate" args = { "ticket": request.args["ticket"], "service": self.cas_service_url } body = yield http_client.get_raw(uri, args) result = yield self.handle_cas_response(request, body, client_redirect_url) defer.returnValue(result) @defer.inlineCallbacks def handle_cas_response(self, request, cas_response_body, client_redirect_url): user, attributes = self.parse_cas_response(cas_response_body) for required_attribute, required_value in self.cas_required_attributes.items(): # If required attribute was not in CAS Response - Forbidden if required_attribute not in attributes: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) # Also need to check value if required_value is not None: actual_value = attributes[required_attribute] # If required attribute value does not match expected - Forbidden if required_value != actual_value: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) user_id = UserID.create(user, self.hs.hostname).to_string() auth_handler = self.handlers.auth_handler user_exists = yield auth_handler.does_user_exist(user_id) if not user_exists: user_id, _ = ( yield self.handlers.registration_handler.register(localpart=user) ) login_token = auth_handler.generate_short_term_login_token(user_id) redirect_url = self.add_login_token_to_redirect_url(client_redirect_url, login_token) request.redirect(redirect_url) request.finish() def add_login_token_to_redirect_url(self, url, token): url_parts = list(urlparse.urlparse(url)) query = dict(urlparse.parse_qsl(url_parts[4])) query.update({"loginToken": token}) url_parts[4] = urllib.urlencode(query) return urlparse.urlunparse(url_parts) def parse_cas_response(self, cas_response_body): root = ET.fromstring(cas_response_body) if not root.tag.endswith("serviceResponse"): raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) if not root[0].tag.endswith("authenticationSuccess"): raise LoginError(401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED) for child in root[0]: if child.tag.endswith("user"): user = child.text if child.tag.endswith("attributes"): attributes = {} for attribute in child: # ElementTree library expands the namespace in attribute tags # to the full URL of the namespace. # See (https://docs.python.org/2/library/xml.etree.elementtree.html) # We don't care about namespace here and it will always be encased in # curly braces, so we remove them. if "}" in attribute.tag: attributes[attribute.tag.split("}")[1]] = attribute.text else: attributes[attribute.tag] = attribute.text if user is None or attributes is None: raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) return (user, attributes)