summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--changelog.d/4085.feature1
-rw-r--r--changelog.d/4089.feature1
-rw-r--r--changelog.d/4099.feature1
-rw-r--r--changelog.d/4108.misc1
-rw-r--r--docker/conf/homeserver.yaml4
-rwxr-xr-xscripts/register_new_matrix_user204
-rw-r--r--synapse/_scripts/__init__.py0
-rw-r--r--synapse/_scripts/register_new_matrix_user.py215
-rw-r--r--synapse/handlers/room.py125
-rw-r--r--synapse/replication/tcp/client.py2
-rw-r--r--tests/scripts/__init__.py0
-rw-r--r--tests/scripts/test_new_matrix_user.py160
12 files changed, 465 insertions, 249 deletions
diff --git a/changelog.d/4085.feature b/changelog.d/4085.feature
new file mode 100644
index 0000000000..4bd3ddcf2c
--- /dev/null
+++ b/changelog.d/4085.feature
@@ -0,0 +1 @@
+The register_new_matrix_user script is now ported to Python 3.
diff --git a/changelog.d/4089.feature b/changelog.d/4089.feature
new file mode 100644
index 0000000000..62c9d839bb
--- /dev/null
+++ b/changelog.d/4089.feature
@@ -0,0 +1 @@
+ Configure Docker image to listen on both ipv4 and ipv6.
diff --git a/changelog.d/4099.feature b/changelog.d/4099.feature
new file mode 100644
index 0000000000..a3f7dbdcdd
--- /dev/null
+++ b/changelog.d/4099.feature
@@ -0,0 +1 @@
+Support for replacing rooms with new ones
diff --git a/changelog.d/4108.misc b/changelog.d/4108.misc
new file mode 100644
index 0000000000..85810c3d83
--- /dev/null
+++ b/changelog.d/4108.misc
@@ -0,0 +1 @@
+The "Received rdata" log messages on workers is now logged at DEBUG, not INFO.
diff --git a/docker/conf/homeserver.yaml b/docker/conf/homeserver.yaml
index a38b929f50..1b0f655d26 100644
--- a/docker/conf/homeserver.yaml
+++ b/docker/conf/homeserver.yaml
@@ -21,7 +21,7 @@ listeners:
   {% if not SYNAPSE_NO_TLS %}
   -
     port: 8448
-    bind_addresses: ['0.0.0.0']
+    bind_addresses: ['::']
     type: http
     tls: true
     x_forwarded: false
@@ -34,7 +34,7 @@ listeners:
 
   - port: 8008
     tls: false
-    bind_addresses: ['0.0.0.0']
+    bind_addresses: ['::']
     type: http
     x_forwarded: false
 
diff --git a/scripts/register_new_matrix_user b/scripts/register_new_matrix_user
index 89143c5d59..b450712ab7 100755
--- a/scripts/register_new_matrix_user
+++ b/scripts/register_new_matrix_user
@@ -16,207 +16,7 @@
 
 from __future__ import print_function
 
-import argparse
-import getpass
-import hashlib
-import hmac
-import json
-import sys
-import urllib2
-
-from six import input
-
-import yaml
-
-
-def request_registration(user, password, server_location, shared_secret, admin=False):
-    req = urllib2.Request(
-        "%s/_matrix/client/r0/admin/register" % (server_location,),
-        headers={'Content-Type': 'application/json'},
-    )
-
-    try:
-        if sys.version_info[:3] >= (2, 7, 9):
-            # As of version 2.7.9, urllib2 now checks SSL certs
-            import ssl
-
-            f = urllib2.urlopen(req, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
-        else:
-            f = urllib2.urlopen(req)
-        body = f.read()
-        f.close()
-        nonce = json.loads(body)["nonce"]
-    except urllib2.HTTPError as e:
-        print("ERROR! Received %d %s" % (e.code, e.reason))
-        if 400 <= e.code < 500:
-            if e.info().type == "application/json":
-                resp = json.load(e)
-                if "error" in resp:
-                    print(resp["error"])
-        sys.exit(1)
-
-    mac = hmac.new(key=shared_secret, digestmod=hashlib.sha1)
-
-    mac.update(nonce)
-    mac.update("\x00")
-    mac.update(user)
-    mac.update("\x00")
-    mac.update(password)
-    mac.update("\x00")
-    mac.update("admin" if admin else "notadmin")
-
-    mac = mac.hexdigest()
-
-    data = {
-        "nonce": nonce,
-        "username": user,
-        "password": password,
-        "mac": mac,
-        "admin": admin,
-    }
-
-    server_location = server_location.rstrip("/")
-
-    print("Sending registration request...")
-
-    req = urllib2.Request(
-        "%s/_matrix/client/r0/admin/register" % (server_location,),
-        data=json.dumps(data),
-        headers={'Content-Type': 'application/json'},
-    )
-    try:
-        if sys.version_info[:3] >= (2, 7, 9):
-            # As of version 2.7.9, urllib2 now checks SSL certs
-            import ssl
-
-            f = urllib2.urlopen(req, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
-        else:
-            f = urllib2.urlopen(req)
-        f.read()
-        f.close()
-        print("Success.")
-    except urllib2.HTTPError as e:
-        print("ERROR! Received %d %s" % (e.code, e.reason))
-        if 400 <= e.code < 500:
-            if e.info().type == "application/json":
-                resp = json.load(e)
-                if "error" in resp:
-                    print(resp["error"])
-        sys.exit(1)
-
-
-def register_new_user(user, password, server_location, shared_secret, admin):
-    if not user:
-        try:
-            default_user = getpass.getuser()
-        except Exception:
-            default_user = None
-
-        if default_user:
-            user = input("New user localpart [%s]: " % (default_user,))
-            if not user:
-                user = default_user
-        else:
-            user = input("New user localpart: ")
-
-    if not user:
-        print("Invalid user name")
-        sys.exit(1)
-
-    if not password:
-        password = getpass.getpass("Password: ")
-
-        if not password:
-            print("Password cannot be blank.")
-            sys.exit(1)
-
-        confirm_password = getpass.getpass("Confirm password: ")
-
-        if password != confirm_password:
-            print("Passwords do not match")
-            sys.exit(1)
-
-    if admin is None:
-        admin = input("Make admin [no]: ")
-        if admin in ("y", "yes", "true"):
-            admin = True
-        else:
-            admin = False
-
-    request_registration(user, password, server_location, shared_secret, bool(admin))
-
+from synapse._scripts.register_new_matrix_user import main
 
 if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        description="Used to register new users with a given home server when"
-        " registration has been disabled. The home server must be"
-        " configured with the 'registration_shared_secret' option"
-        " set."
-    )
-    parser.add_argument(
-        "-u",
-        "--user",
-        default=None,
-        help="Local part of the new user. Will prompt if omitted.",
-    )
-    parser.add_argument(
-        "-p",
-        "--password",
-        default=None,
-        help="New password for user. Will prompt if omitted.",
-    )
-    admin_group = parser.add_mutually_exclusive_group()
-    admin_group.add_argument(
-        "-a",
-        "--admin",
-        action="store_true",
-        help=(
-            "Register new user as an admin. "
-            "Will prompt if --no-admin is not set either."
-        ),
-    )
-    admin_group.add_argument(
-        "--no-admin",
-        action="store_true",
-        help=(
-            "Register new user as a regular user. "
-            "Will prompt if --admin is not set either."
-        ),
-    )
-
-    group = parser.add_mutually_exclusive_group(required=True)
-    group.add_argument(
-        "-c",
-        "--config",
-        type=argparse.FileType('r'),
-        help="Path to server config file. Used to read in shared secret.",
-    )
-
-    group.add_argument(
-        "-k", "--shared-secret", help="Shared secret as defined in server config file."
-    )
-
-    parser.add_argument(
-        "server_url",
-        default="https://localhost:8448",
-        nargs='?',
-        help="URL to use to talk to the home server. Defaults to "
-        " 'https://localhost:8448'.",
-    )
-
-    args = parser.parse_args()
-
-    if "config" in args and args.config:
-        config = yaml.safe_load(args.config)
-        secret = config.get("registration_shared_secret", None)
-        if not secret:
-            print("No 'registration_shared_secret' defined in config.")
-            sys.exit(1)
-    else:
-        secret = args.shared_secret
-
-    admin = None
-    if args.admin or args.no_admin:
-        admin = args.admin
-
-    register_new_user(args.user, args.password, args.server_url, secret, admin)
+    main()
diff --git a/synapse/_scripts/__init__.py b/synapse/_scripts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/synapse/_scripts/__init__.py
diff --git a/synapse/_scripts/register_new_matrix_user.py b/synapse/_scripts/register_new_matrix_user.py
new file mode 100644
index 0000000000..70cecde486
--- /dev/null
+++ b/synapse/_scripts/register_new_matrix_user.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015, 2016 OpenMarket Ltd
+# Copyright 2018 New Vector
+#
+# 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.
+
+from __future__ import print_function
+
+import argparse
+import getpass
+import hashlib
+import hmac
+import logging
+import sys
+
+from six.moves import input
+
+import requests as _requests
+import yaml
+
+
+def request_registration(
+    user,
+    password,
+    server_location,
+    shared_secret,
+    admin=False,
+    requests=_requests,
+    _print=print,
+    exit=sys.exit,
+):
+
+    url = "%s/_matrix/client/r0/admin/register" % (server_location,)
+
+    # Get the nonce
+    r = requests.get(url, verify=False)
+
+    if r.status_code is not 200:
+        _print("ERROR! Received %d %s" % (r.status_code, r.reason))
+        if 400 <= r.status_code < 500:
+            try:
+                _print(r.json()["error"])
+            except Exception:
+                pass
+        return exit(1)
+
+    nonce = r.json()["nonce"]
+
+    mac = hmac.new(key=shared_secret.encode('utf8'), digestmod=hashlib.sha1)
+
+    mac.update(nonce.encode('utf8'))
+    mac.update(b"\x00")
+    mac.update(user.encode('utf8'))
+    mac.update(b"\x00")
+    mac.update(password.encode('utf8'))
+    mac.update(b"\x00")
+    mac.update(b"admin" if admin else b"notadmin")
+
+    mac = mac.hexdigest()
+
+    data = {
+        "nonce": nonce,
+        "username": user,
+        "password": password,
+        "mac": mac,
+        "admin": admin,
+    }
+
+    _print("Sending registration request...")
+    r = requests.post(url, json=data, verify=False)
+
+    if r.status_code is not 200:
+        _print("ERROR! Received %d %s" % (r.status_code, r.reason))
+        if 400 <= r.status_code < 500:
+            try:
+                _print(r.json()["error"])
+            except Exception:
+                pass
+        return exit(1)
+
+    _print("Success!")
+
+
+def register_new_user(user, password, server_location, shared_secret, admin):
+    if not user:
+        try:
+            default_user = getpass.getuser()
+        except Exception:
+            default_user = None
+
+        if default_user:
+            user = input("New user localpart [%s]: " % (default_user,))
+            if not user:
+                user = default_user
+        else:
+            user = input("New user localpart: ")
+
+    if not user:
+        print("Invalid user name")
+        sys.exit(1)
+
+    if not password:
+        password = getpass.getpass("Password: ")
+
+        if not password:
+            print("Password cannot be blank.")
+            sys.exit(1)
+
+        confirm_password = getpass.getpass("Confirm password: ")
+
+        if password != confirm_password:
+            print("Passwords do not match")
+            sys.exit(1)
+
+    if admin is None:
+        admin = input("Make admin [no]: ")
+        if admin in ("y", "yes", "true"):
+            admin = True
+        else:
+            admin = False
+
+    request_registration(user, password, server_location, shared_secret, bool(admin))
+
+
+def main():
+
+    logging.captureWarnings(True)
+
+    parser = argparse.ArgumentParser(
+        description="Used to register new users with a given home server when"
+        " registration has been disabled. The home server must be"
+        " configured with the 'registration_shared_secret' option"
+        " set."
+    )
+    parser.add_argument(
+        "-u",
+        "--user",
+        default=None,
+        help="Local part of the new user. Will prompt if omitted.",
+    )
+    parser.add_argument(
+        "-p",
+        "--password",
+        default=None,
+        help="New password for user. Will prompt if omitted.",
+    )
+    admin_group = parser.add_mutually_exclusive_group()
+    admin_group.add_argument(
+        "-a",
+        "--admin",
+        action="store_true",
+        help=(
+            "Register new user as an admin. "
+            "Will prompt if --no-admin is not set either."
+        ),
+    )
+    admin_group.add_argument(
+        "--no-admin",
+        action="store_true",
+        help=(
+            "Register new user as a regular user. "
+            "Will prompt if --admin is not set either."
+        ),
+    )
+
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument(
+        "-c",
+        "--config",
+        type=argparse.FileType('r'),
+        help="Path to server config file. Used to read in shared secret.",
+    )
+
+    group.add_argument(
+        "-k", "--shared-secret", help="Shared secret as defined in server config file."
+    )
+
+    parser.add_argument(
+        "server_url",
+        default="https://localhost:8448",
+        nargs='?',
+        help="URL to use to talk to the home server. Defaults to "
+        " 'https://localhost:8448'.",
+    )
+
+    args = parser.parse_args()
+
+    if "config" in args and args.config:
+        config = yaml.safe_load(args.config)
+        secret = config.get("registration_shared_secret", None)
+        if not secret:
+            print("No 'registration_shared_secret' defined in config.")
+            sys.exit(1)
+    else:
+        secret = args.shared_secret
+
+    admin = None
+    if args.admin or args.no_admin:
+        admin = args.admin
+
+    register_new_user(args.user, args.password, args.server_url, secret, admin)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index c59c02527c..1d9417ff1a 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -136,53 +136,91 @@ class RoomCreationHandler(BaseHandler):
                 requester, tombstone_event, tombstone_context,
             )
 
+            # and finally, shut down the PLs in the old room, and update them in the new
+            # room.
             old_room_state = yield tombstone_context.get_current_state_ids(self.store)
-            old_room_pl_event_id = old_room_state.get((EventTypes.PowerLevels, ""))
 
-            if old_room_pl_event_id is None:
-                logger.warning(
-                    "Not supported: upgrading a room with no PL event. Not setting PLs "
-                    "in old room.",
+            yield self._update_upgraded_room_pls(
+                requester, old_room_id, new_room_id, old_room_state,
+            )
+
+            defer.returnValue(new_room_id)
+
+    @defer.inlineCallbacks
+    def _update_upgraded_room_pls(
+            self, requester, old_room_id, new_room_id, old_room_state,
+    ):
+        """Send updated power levels in both rooms after an upgrade
+
+        Args:
+            requester (synapse.types.Requester): the user requesting the upgrade
+            old_room_id (unicode): the id of the room to be replaced
+            new_room_id (unicode): the id of the replacement room
+            old_room_state (dict[tuple[str, str], str]): the state map for the old room
+
+        Returns:
+            Deferred
+        """
+        old_room_pl_event_id = old_room_state.get((EventTypes.PowerLevels, ""))
+
+        if old_room_pl_event_id is None:
+            logger.warning(
+                "Not supported: upgrading a room with no PL event. Not setting PLs "
+                "in old room.",
+            )
+            return
+
+        old_room_pl_state = yield self.store.get_event(old_room_pl_event_id)
+
+        # we try to stop regular users from speaking by setting the PL required
+        # to send regular events and invites to 'Moderator' level. That's normally
+        # 50, but if the default PL in a room is 50 or more, then we set the
+        # required PL above that.
+
+        pl_content = dict(old_room_pl_state.content)
+        users_default = int(pl_content.get("users_default", 0))
+        restricted_level = max(users_default + 1, 50)
+
+        updated = False
+        for v in ("invite", "events_default"):
+            current = int(pl_content.get(v, 0))
+            if current < restricted_level:
+                logger.info(
+                    "Setting level for %s in %s to %i (was %i)",
+                    v, old_room_id, restricted_level, current,
                 )
+                pl_content[v] = restricted_level
+                updated = True
             else:
-                # we try to stop regular users from speaking by setting the PL required
-                # to send regular events and invites to 'Moderator' level. That's normally
-                # 50, but if the default PL in a room is 50 or more, then we set the
-                # required PL above that.
-
-                old_room_pl_state = yield self.store.get_event(old_room_pl_event_id)
-                pl_content = dict(old_room_pl_state.content)
-                users_default = int(pl_content.get("users_default", 0))
-                restricted_level = max(users_default + 1, 50)
-
-                updated = False
-                for v in ("invite", "events_default"):
-                    current = int(pl_content.get(v, 0))
-                    if current < restricted_level:
-                        logger.debug(
-                            "Setting level for %s in %s to %i (was %i)",
-                            v, old_room_id, restricted_level, current,
-                        )
-                        pl_content[v] = restricted_level
-                        updated = True
-                    else:
-                        logger.debug(
-                            "Not setting level for %s (already %i)",
-                            v, current,
-                        )
-
-                if updated:
-                    yield self.event_creation_handler.create_and_send_nonmember_event(
-                        requester, {
-                            "type": EventTypes.PowerLevels,
-                            "state_key": '',
-                            "room_id": old_room_id,
-                            "sender": user_id,
-                            "content": pl_content,
-                        }, ratelimit=False,
-                    )
-
-        defer.returnValue(new_room_id)
+                logger.info(
+                    "Not setting level for %s (already %i)",
+                    v, current,
+                )
+
+        if updated:
+            try:
+                yield self.event_creation_handler.create_and_send_nonmember_event(
+                    requester, {
+                        "type": EventTypes.PowerLevels,
+                        "state_key": '',
+                        "room_id": old_room_id,
+                        "sender": requester.user.to_string(),
+                        "content": pl_content,
+                    }, ratelimit=False,
+                )
+            except AuthError as e:
+                logger.warning("Unable to update PLs in old room: %s", e)
+
+        logger.info("Setting correct PLs in new room")
+        yield self.event_creation_handler.create_and_send_nonmember_event(
+            requester, {
+                "type": EventTypes.PowerLevels,
+                "state_key": '',
+                "room_id": new_room_id,
+                "sender": requester.user.to_string(),
+                "content": old_room_pl_state.content,
+            }, ratelimit=False,
+        )
 
     @defer.inlineCallbacks
     def clone_exiting_room(
@@ -223,7 +261,6 @@ class RoomCreationHandler(BaseHandler):
         initial_state = dict()
 
         types_to_copy = (
-            (EventTypes.PowerLevels, ""),
             (EventTypes.JoinRules, ""),
             (EventTypes.Name, ""),
             (EventTypes.Topic, ""),
diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py
index cbe9645817..586dddb40b 100644
--- a/synapse/replication/tcp/client.py
+++ b/synapse/replication/tcp/client.py
@@ -106,7 +106,7 @@ class ReplicationClientHandler(object):
 
         Can be overriden in subclasses to handle more.
         """
-        logger.info("Received rdata %s -> %s", stream_name, token)
+        logger.debug("Received rdata %s -> %s", stream_name, token)
         return self.store.process_replication_rows(stream_name, token, rows)
 
     def on_position(self, stream_name, token):
diff --git a/tests/scripts/__init__.py b/tests/scripts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/scripts/__init__.py
diff --git a/tests/scripts/test_new_matrix_user.py b/tests/scripts/test_new_matrix_user.py
new file mode 100644
index 0000000000..6f56893f5e
--- /dev/null
+++ b/tests/scripts/test_new_matrix_user.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 New Vector
+#
+# 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.
+
+from mock import Mock
+
+from synapse._scripts.register_new_matrix_user import request_registration
+
+from tests.unittest import TestCase
+
+
+class RegisterTestCase(TestCase):
+    def test_success(self):
+        """
+        The script will fetch a nonce, and then generate a MAC with it, and then
+        post that MAC.
+        """
+
+        def get(url, verify=None):
+            r = Mock()
+            r.status_code = 200
+            r.json = lambda: {"nonce": "a"}
+            return r
+
+        def post(url, json=None, verify=None):
+            # Make sure we are sent the correct info
+            self.assertEqual(json["username"], "user")
+            self.assertEqual(json["password"], "pass")
+            self.assertEqual(json["nonce"], "a")
+            # We want a 40-char hex MAC
+            self.assertEqual(len(json["mac"]), 40)
+
+            r = Mock()
+            r.status_code = 200
+            return r
+
+        requests = Mock()
+        requests.get = get
+        requests.post = post
+
+        # The fake stdout will be written here
+        out = []
+        err_code = []
+
+        request_registration(
+            "user",
+            "pass",
+            "matrix.org",
+            "shared",
+            admin=False,
+            requests=requests,
+            _print=out.append,
+            exit=err_code.append,
+        )
+
+        # We should get the success message making sure everything is OK.
+        self.assertIn("Success!", out)
+
+        # sys.exit shouldn't have been called.
+        self.assertEqual(err_code, [])
+
+    def test_failure_nonce(self):
+        """
+        If the script fails to fetch a nonce, it throws an error and quits.
+        """
+
+        def get(url, verify=None):
+            r = Mock()
+            r.status_code = 404
+            r.reason = "Not Found"
+            r.json = lambda: {"not": "error"}
+            return r
+
+        requests = Mock()
+        requests.get = get
+
+        # The fake stdout will be written here
+        out = []
+        err_code = []
+
+        request_registration(
+            "user",
+            "pass",
+            "matrix.org",
+            "shared",
+            admin=False,
+            requests=requests,
+            _print=out.append,
+            exit=err_code.append,
+        )
+
+        # Exit was called
+        self.assertEqual(err_code, [1])
+
+        # We got an error message
+        self.assertIn("ERROR! Received 404 Not Found", out)
+        self.assertNotIn("Success!", out)
+
+    def test_failure_post(self):
+        """
+        The script will fetch a nonce, and then if the final POST fails, will
+        report an error and quit.
+        """
+
+        def get(url, verify=None):
+            r = Mock()
+            r.status_code = 200
+            r.json = lambda: {"nonce": "a"}
+            return r
+
+        def post(url, json=None, verify=None):
+            # Make sure we are sent the correct info
+            self.assertEqual(json["username"], "user")
+            self.assertEqual(json["password"], "pass")
+            self.assertEqual(json["nonce"], "a")
+            # We want a 40-char hex MAC
+            self.assertEqual(len(json["mac"]), 40)
+
+            r = Mock()
+            # Then 500 because we're jerks
+            r.status_code = 500
+            r.reason = "Broken"
+            return r
+
+        requests = Mock()
+        requests.get = get
+        requests.post = post
+
+        # The fake stdout will be written here
+        out = []
+        err_code = []
+
+        request_registration(
+            "user",
+            "pass",
+            "matrix.org",
+            "shared",
+            admin=False,
+            requests=requests,
+            _print=out.append,
+            exit=err_code.append,
+        )
+
+        # Exit was called
+        self.assertEqual(err_code, [1])
+
+        # We got an error message
+        self.assertIn("ERROR! Received 500 Broken", out)
+        self.assertNotIn("Success!", out)