diff --git a/changelog.d/4334.removal b/changelog.d/4334.removal new file mode 100644 index 0000000000..d4b8c56b7d --- /dev/null +++ b/changelog.d/4334.removal @@ -0,0 +1 @@ +Remove the deprecated v1/register API on Python 2. It was never ported to Python 3. diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 5f35c2d1be..66585c991f 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -14,8 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from six import PY3 - from synapse.http.server import JsonResource from synapse.rest.client import versions from synapse.rest.client.v1 import ( @@ -56,11 +54,6 @@ from synapse.rest.client.v2_alpha import ( user_directory, ) -if not PY3: - from synapse.rest.client.v1_only import ( - register as v1_register, - ) - class ClientRestResource(JsonResource): """A resource for version 1 of the matrix client API.""" @@ -73,10 +66,6 @@ class ClientRestResource(JsonResource): def register_servlets(client_resource, hs): versions.register_servlets(client_resource) - if not PY3: - # "v1" (Python 2 only) - v1_register.register_servlets(hs, client_resource) - # Deprecated in r0 initial_sync.register_servlets(hs, client_resource) room.register_deprecated_servlets(hs, client_resource) diff --git a/synapse/rest/client/v1_only/__init__.py b/synapse/rest/client/v1_only/__init__.py deleted file mode 100644 index 936f902ace..0000000000 --- a/synapse/rest/client/v1_only/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -REST APIs that are only used in v1 (the legacy API). -""" diff --git a/synapse/rest/client/v1_only/base.py b/synapse/rest/client/v1_only/base.py deleted file mode 100644 index 9d4db7437c..0000000000 --- a/synapse/rest/client/v1_only/base.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains base REST classes for constructing client v1 servlets. -""" - -import re - -from synapse.api.urls import CLIENT_PREFIX - - -def v1_only_client_path_patterns(path_regex, include_in_unstable=True): - """Creates a regex compiled client path with the correct client path - prefix. - - Args: - path_regex (str): The regex string to match. This should NOT have a ^ - as this will be prefixed. - Returns: - list of SRE_Pattern - """ - patterns = [re.compile("^" + CLIENT_PREFIX + path_regex)] - if include_in_unstable: - unstable_prefix = CLIENT_PREFIX.replace("/api/v1", "/unstable") - patterns.append(re.compile("^" + unstable_prefix + path_regex)) - return patterns diff --git a/synapse/rest/client/v1_only/register.py b/synapse/rest/client/v1_only/register.py deleted file mode 100644 index dadb376b02..0000000000 --- a/synapse/rest/client/v1_only/register.py +++ /dev/null @@ -1,392 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with registration: /register""" -import hmac -import logging -from hashlib import sha1 - -from twisted.internet import defer - -import synapse.util.stringutils as stringutils -from synapse.api.constants import LoginType -from synapse.api.errors import Codes, SynapseError -from synapse.config.server import is_threepid_reserved -from synapse.http.servlet import assert_params_in_dict, parse_json_object_from_request -from synapse.rest.client.v1.base import ClientV1RestServlet -from synapse.types import create_requester - -from .base import v1_only_client_path_patterns - -logger = logging.getLogger(__name__) - - -# We ought to be using hmac.compare_digest() but on older pythons it doesn't -# exist. It's a _really minor_ security flaw to use plain string comparison -# because the timing attack is so obscured by all the other code here it's -# unlikely to make much difference -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - def compare_digest(a, b): - return a == b - - -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 = v1_only_client_path_patterns("/register$", include_in_unstable=False) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super(RegisterRestServlet, self).__init__(hs) - # sessions are stored as: - # self.sessions = { - # "session_id" : { __session_dict__ } - # } - # TODO: persistent storage - self.sessions = {} - self.enable_registration = hs.config.enable_registration - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - self.handlers = hs.get_handlers() - - def on_GET(self, request): - - require_email = 'email' in self.hs.config.registrations_require_3pid - require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid - - flows = [] - if self.hs.config.enable_registration_captcha: - # only support the email-only flow if we don't require MSISDN 3PIDs - if not require_msisdn: - flows.extend([ - { - "type": LoginType.RECAPTCHA, - "stages": [ - LoginType.RECAPTCHA, - LoginType.EMAIL_IDENTITY, - LoginType.PASSWORD - ] - }, - ]) - # only support 3PIDless registration if no 3PIDs are required - if not require_email and not require_msisdn: - flows.extend([ - { - "type": LoginType.RECAPTCHA, - "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] - } - ]) - else: - # only support the email-only flow if we don't require MSISDN 3PIDs - if require_email or not require_msisdn: - flows.extend([ - { - "type": LoginType.EMAIL_IDENTITY, - "stages": [ - LoginType.EMAIL_IDENTITY, LoginType.PASSWORD - ] - } - ]) - # only support 3PIDless registration if no 3PIDs are required - if not require_email and not require_msisdn: - flows.extend([ - { - "type": LoginType.PASSWORD - } - ]) - return (200, {"flows": flows}) - - @defer.inlineCallbacks - def on_POST(self, request): - register_json = parse_json_object_from_request(request) - - session = (register_json["session"] - if "session" in register_json else None) - login_type = None - assert_params_in_dict(register_json, ["type"]) - - try: - login_type = register_json["type"] - - is_application_server = login_type == LoginType.APPLICATION_SERVICE - can_register = ( - self.enable_registration - or is_application_server - ) - 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, - } - - 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): - 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 - ) - threepid = None - if session.get(LoginType.EMAIL_IDENTITY): - threepid = session["threepidCreds"] - - handler = self.handlers.registration_handler - (user_id, token) = yield handler.register( - localpart=desired_user_id, - password=password, - threepid=threepid, - ) - # Necessary due to auth checks prior to the threepid being - # written to the db - if is_threepid_reserved(self.hs.config, threepid): - yield self.store.upsert_monthly_active_user(user_id) - - 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): - as_token = self.auth.get_access_token_from_request(request) - - assert_params_in_dict(register_json, ["user"]) - user_localpart = register_json["user"].encode("utf-8") - - handler = self.handlers.registration_handler - user_id = yield handler.appservice_register( - user_localpart, as_token - ) - token = yield self.auth_handler.issue_access_token(user_id) - self._remove_session(session) - defer.returnValue({ - "user_id": user_id, - "access_token": token, - "home_server": self.hs.hostname, - }) - - -class CreateUserRestServlet(ClientV1RestServlet): - """Handles user creation via a server-to-server interface - """ - - PATTERNS = v1_only_client_path_patterns("/createUser$") - - def __init__(self, hs): - super(CreateUserRestServlet, self).__init__(hs) - self.store = hs.get_datastore() - self.handlers = hs.get_handlers() - - @defer.inlineCallbacks - def on_POST(self, request): - user_json = parse_json_object_from_request(request) - - access_token = self.auth.get_access_token_from_request(request) - app_service = self.store.get_app_service_by_token( - access_token - ) - if not app_service: - raise SynapseError(403, "Invalid application service token.") - - requester = create_requester(app_service.sender) - - logger.debug("creating user: %s", user_json) - response = yield self._do_create(requester, user_json) - - defer.returnValue((200, response)) - - def on_OPTIONS(self, request): - return 403, {} - - @defer.inlineCallbacks - def _do_create(self, requester, user_json): - assert_params_in_dict(user_json, ["localpart", "displayname"]) - - localpart = user_json["localpart"].encode("utf-8") - displayname = user_json["displayname"].encode("utf-8") - password_hash = user_json["password_hash"].encode("utf-8") \ - if user_json.get("password_hash") else None - - handler = self.handlers.registration_handler - user_id, token = yield handler.get_or_create_user( - requester=requester, - localpart=localpart, - displayname=displayname, - password_hash=password_hash - ) - - defer.returnValue({ - "user_id": user_id, - "access_token": token, - "home_server": self.hs.hostname, - }) - - -def register_servlets(hs, http_server): - RegisterRestServlet(hs).register(http_server) - CreateUserRestServlet(hs).register(http_server) diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index 956f7fc4c4..483bebc832 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -16,64 +16,49 @@ """ Tests REST events for /events paths.""" from mock import Mock, NonCallableMock -from six import PY3 -from twisted.internet import defer +from synapse.rest.client.v1 import admin, events, login, room -from ....utils import MockHttpResource, setup_test_homeserver -from .utils import RestTestCase - -PATH_PREFIX = "/_matrix/client/api/v1" +from tests import unittest -class EventStreamPermissionsTestCase(RestTestCase): +class EventStreamPermissionsTestCase(unittest.HomeserverTestCase): """ Tests event streaming (GET /events). """ - if PY3: - skip = "Skip on Py3 until ported to use not V1 only register." + servlets = [ + events.register_servlets, + room.register_servlets, + admin.register_servlets, + login.register_servlets, + ] - @defer.inlineCallbacks - def setUp(self): - import synapse.rest.client.v1.events - import synapse.rest.client.v1_only.register - import synapse.rest.client.v1.room + def make_homeserver(self, reactor, clock): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + config = self.default_config() + config.enable_registration_captcha = False + config.enable_registration = True + config.auto_join_rooms = [] - hs = yield setup_test_homeserver( - self.addCleanup, - http_client=None, - federation_client=Mock(), - ratelimiter=NonCallableMock(spec_set=["send_message"]), + hs = self.setup_test_homeserver( + config=config, ratelimiter=NonCallableMock(spec_set=["send_message"]) ) self.ratelimiter = hs.get_ratelimiter() self.ratelimiter.send_message.return_value = (True, 0) - hs.config.enable_registration_captcha = False - hs.config.enable_registration = True - hs.config.auto_join_rooms = [] hs.get_handlers().federation_handler = Mock() - synapse.rest.client.v1_only.register.register_servlets(hs, self.mock_resource) - synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource) - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + return hs + + def prepare(self, hs, reactor, clock): # register an account - self.user_id = "sid1" - response = yield self.register(self.user_id) - self.token = response["access_token"] - self.user_id = response["user_id"] + self.user_id = self.register_user("sid1", "pass") + self.token = self.login(self.user_id, "pass") # register a 2nd account - self.other_user = "other1" - response = yield self.register(self.other_user) - self.other_token = response["access_token"] - self.other_user = response["user_id"] + self.other_user = self.register_user("other2", "pass") + self.other_token = self.login(self.other_user, "pass") - def tearDown(self): - pass - - @defer.inlineCallbacks def test_stream_basic_permissions(self): # invalid token, expect 401 # note: this is in violation of the original v1 spec, which expected @@ -81,34 +66,37 @@ class EventStreamPermissionsTestCase(RestTestCase): # implementation is now part of the r0 implementation, the newer # behaviour is used instead to be consistent with the r0 spec. # see issue #2602 - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s" % ("invalid" + self.token,) + request, channel = self.make_request( + "GET", "/events?access_token=%s" % ("invalid" + self.token,) ) - self.assertEquals(401, code, msg=str(response)) + self.render(request) + self.assertEquals(channel.code, 401, msg=channel.result) # valid token, expect content - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) + request, channel = self.make_request( + "GET", "/events?access_token=%s&timeout=0" % (self.token,) ) - self.assertEquals(200, code, msg=str(response)) - self.assertTrue("chunk" in response) - self.assertTrue("start" in response) - self.assertTrue("end" in response) + self.render(request) + self.assertEquals(channel.code, 200, msg=channel.result) + self.assertTrue("chunk" in channel.json_body) + self.assertTrue("start" in channel.json_body) + self.assertTrue("end" in channel.json_body) - @defer.inlineCallbacks def test_stream_room_permissions(self): - room_id = yield self.create_room_as(self.other_user, tok=self.other_token) - yield self.send(room_id, tok=self.other_token) + room_id = self.helper.create_room_as(self.other_user, tok=self.other_token) + self.helper.send(room_id, tok=self.other_token) # invited to room (expect no content for room) - yield self.invite( + self.helper.invite( room_id, src=self.other_user, targ=self.user_id, tok=self.other_token ) - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) + # valid token, expect content + request, channel = self.make_request( + "GET", "/events?access_token=%s&timeout=0" % (self.token,) ) - self.assertEquals(200, code, msg=str(response)) + self.render(request) + self.assertEquals(channel.code, 200, msg=channel.result) # We may get a presence event for ourselves down self.assertEquals( @@ -116,7 +104,7 @@ class EventStreamPermissionsTestCase(RestTestCase): len( [ c - for c in response["chunk"] + for c in channel.json_body["chunk"] if not ( c.get("type") == "m.presence" and c["content"].get("user_id") == self.user_id @@ -126,7 +114,7 @@ class EventStreamPermissionsTestCase(RestTestCase): ) # joined room (expect all content for room) - yield self.join(room=room_id, user=self.user_id, tok=self.token) + self.helper.join(room=room_id, user=self.user_id, tok=self.token) # left to room (expect no content for room) diff --git a/tests/rest/client/v1/test_register.py b/tests/rest/client/v1/test_register.py deleted file mode 100644 index f973eff8cf..0000000000 --- a/tests/rest/client/v1/test_register.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -from mock import Mock -from six import PY3 - -from twisted.test.proto_helpers import MemoryReactorClock - -from synapse.http.server import JsonResource -from synapse.rest.client.v1_only.register import register_servlets -from synapse.util import Clock - -from tests import unittest -from tests.server import make_request, render, setup_test_homeserver - - -class CreateUserServletTestCase(unittest.TestCase): - """ - Tests for CreateUserRestServlet. - """ - - if PY3: - skip = "Not ported to Python 3." - - def setUp(self): - self.registration_handler = Mock() - - self.appservice = Mock(sender="@as:test") - self.datastore = Mock( - get_app_service_by_token=Mock(return_value=self.appservice) - ) - - handlers = Mock(registration_handler=self.registration_handler) - self.reactor = MemoryReactorClock() - self.hs_clock = Clock(self.reactor) - - self.hs = self.hs = setup_test_homeserver( - self.addCleanup, http_client=None, clock=self.hs_clock, reactor=self.reactor - ) - self.hs.get_datastore = Mock(return_value=self.datastore) - self.hs.get_handlers = Mock(return_value=handlers) - - def test_POST_createuser_with_valid_user(self): - - res = JsonResource(self.hs) - register_servlets(self.hs, res) - - request_data = json.dumps( - { - "localpart": "someone", - "displayname": "someone interesting", - "duration_seconds": 200, - } - ) - - url = b'/_matrix/client/api/v1/createUser?access_token=i_am_an_app_service' - - user_id = "@someone:interesting" - token = "my token" - - self.registration_handler.get_or_create_user = Mock( - return_value=(user_id, token) - ) - - request, channel = make_request(self.reactor, b"POST", url, request_data) - render(request, res, self.reactor) - - self.assertEquals(channel.result["code"], b"200") - - det_data = { - "user_id": user_id, - "access_token": token, - "home_server": self.hs.hostname, - } - self.assertDictContainsSubset(det_data, json.loads(channel.result["body"]))