diff options
33 files changed, 1287 insertions, 312 deletions
diff --git a/README.rst b/README.rst index cfdc2a1c75..9c44bf61b8 100644 --- a/README.rst +++ b/README.rst @@ -173,7 +173,11 @@ IDs: For the first form, simply pass the required hostname (of the machine) as the --host parameter:: - $ python synapse/app/homeserver.py --host machine.my.domain.name + $ python synapse/app/homeserver.py \ + --server-name machine.my.domain.name \ + --config-path homeserver.config \ + --generate-config + $ python synapse/app/homeserver.py --config-path homeserver.config For the second form, first create your SRV record and publish it in DNS. This needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname @@ -186,7 +190,13 @@ record would then look something like:: At this point, you should then run the homeserver with the hostname of this SRV record, as that is the name other machines will expect it to have:: - $ python synapse/app/homeserver.py --host my.domain.name --port 8448 + $ python synapse/app/homeserver.py \ + --server-name YOURDOMAIN \ + --bind-port 8448 \ + --config-path homeserver.config \ + --generate-config + $ python synapse/app/homeserver.py --config-path homeserver.config + You may additionally want to pass one or more "-v" options, in order to increase the verbosity of logging output; at least for initial testing. diff --git a/demo/demo.tls.dh b/demo/demo.tls.dh new file mode 100644 index 0000000000..cbc58272a0 --- /dev/null +++ b/demo/demo.tls.dh @@ -0,0 +1,9 @@ +2048-bit DH parameters taken from rfc3526 +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb +IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft +awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT +mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh +fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq +5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg== +-----END DH PARAMETERS----- diff --git a/demo/start.sh b/demo/start.sh index 1e591aabb8..56a1344344 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -6,17 +6,27 @@ CWD=$(pwd) cd "$DIR/.." +mkdir -p demo/etc + for port in 8080 8081 8082; do echo "Starting server on port $port... " python -m synapse.app.homeserver \ + --generate-config \ + --config-path "demo/etc/$port.config" \ + -H "localhost:$port" \ -p "$port" \ -H "localhost:$port" \ -f "$DIR/$port.log" \ -d "$DIR/$port.db" \ - -vv \ -D --pid-file "$DIR/$port.pid" \ - --manhole $((port + 1000)) + --manhole $((port + 1000)) \ + --tls-dh-params-path "demo/demo.tls.dh" + + python -m synapse.app.homeserver \ + --config-path "demo/etc/$port.config" \ + -vv \ + done echo "Starting webclient on port 8000..." diff --git a/setup.py b/setup.py index 59ac4fbf94..0eec5c9354 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ setup( "syutil==0.0.1", "Twisted>=14.0.0", "service_identity>=1.0.0", + "pyyaml", "pyasn1", "pynacl", "daemonize", diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 2473a2b2bb..abd7d73b0a 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -17,9 +17,10 @@ from twisted.internet import defer -from synapse.api.constants import Membership +from synapse.api.constants import Membership, JoinRules from synapse.api.errors import AuthError, StoreError, Codes from synapse.api.events.room import RoomMemberEvent +from synapse.util.logutils import log_function import logging @@ -44,16 +45,29 @@ class Auth(object): """ try: if hasattr(event, "room_id"): + is_state = hasattr(event, "state_key") + if event.type == RoomMemberEvent.TYPE: + yield self._can_replace_state(event) allowed = yield self.is_membership_change_allowed(event) defer.returnValue(allowed) + return + + self._check_joined_room( + member=snapshot.membership_state, + user_id=snapshot.user_id, + room_id=snapshot.room_id, + ) + + if is_state: + # TODO (erikj): This really only should be called for *new* + # state + yield self._can_add_state(event) + yield self._can_replace_state(event) else: - self._check_joined_room( - member=snapshot.membership_state, - user_id=snapshot.user_id, - room_id=snapshot.room_id, - ) - defer.returnValue(True) + yield self._can_send_event(event) + + defer.returnValue(True) else: raise AuthError(500, "Unknown event: %s" % event) except AuthError as e: @@ -111,7 +125,14 @@ class Auth(object): membership = event.content["membership"] + join_rule = yield self.store.get_room_join_rule(event.room_id) + if not join_rule: + join_rule = JoinRules.INVITE + if Membership.INVITE == membership: + # TODO (erikj): We should probably handle this more intelligently + # PRIVATE join rules. + # Invites are valid iff caller is in the room and target isn't. if not caller_in_room: # caller isn't joined raise AuthError(403, "You are not in room %s." % event.room_id) @@ -124,18 +145,42 @@ class Auth(object): # joined: It's a NOOP if event.user_id != target_user_id: raise AuthError(403, "Cannot force another user to join.") - elif room.is_public: - pass # anyone can join public rooms. - elif (not caller or caller.membership not in - [Membership.INVITE, Membership.JOIN]): - raise AuthError(403, "You are not invited to this room.") + elif join_rule == JoinRules.PUBLIC or room.is_public: + pass + elif join_rule == JoinRules.INVITE: + if ( + not caller or caller.membership not in + [Membership.INVITE, Membership.JOIN] + ): + raise AuthError(403, "You are not invited to this room.") + else: + # TODO (erikj): may_join list + # TODO (erikj): private rooms + raise AuthError(403, "You are not allowed to join this room") elif Membership.LEAVE == membership: + # TODO (erikj): Implement kicks. + if not caller_in_room: # trying to leave a room you aren't joined raise AuthError(403, "You are not in room %s." % event.room_id) elif target_user_id != event.user_id: # trying to force another user to leave raise AuthError(403, "Cannot force %s to leave." % target_user_id) + elif Membership.BAN == membership: + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + ban_level, _ = yield self.store.get_ops_levels(event.room_id) + + if ban_level: + ban_level = int(ban_level) + else: + ban_level = 5 # FIXME (erikj): What should we do here? + + if user_level < ban_level: + raise AuthError(403, "You don't have permission to ban") else: raise AuthError(500, "Unknown membership %s" % membership) @@ -176,3 +221,85 @@ class Auth(object): except StoreError: raise AuthError(403, "Unrecognised access token.", errcode=Codes.UNKNOWN_TOKEN) + + @defer.inlineCallbacks + @log_function + def _can_send_event(self, event): + send_level = yield self.store.get_send_event_level(event.room_id) + + if send_level: + send_level = int(send_level) + else: + send_level = 0 + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + if user_level: + user_level = int(user_level) + else: + user_level = 0 + + if user_level < send_level: + raise AuthError( + 403, "You don't have permission to post to the room" + ) + + defer.returnValue(True) + + @defer.inlineCallbacks + def _can_add_state(self, event): + add_level = yield self.store.get_add_state_level(event.room_id) + + if not add_level: + defer.returnValue(True) + + add_level = int(add_level) + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + user_level = int(user_level) + + if user_level < add_level: + raise AuthError( + 403, "You don't have permission to add state to the room" + ) + + defer.returnValue(True) + + @defer.inlineCallbacks + def _can_replace_state(self, event): + current_state = yield self.store.get_current_state( + event.room_id, + event.type, + event.state_key, + ) + + if current_state: + current_state = current_state[0] + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + if user_level: + user_level = int(user_level) + else: + user_level = 0 + + logger.debug("Checking power level for %s, %s", event.user_id, user_level) + if current_state and hasattr(current_state, "required_power_level"): + req = current_state.required_power_level + + logger.debug("Checked power level for %s, %s", event.user_id, req) + if user_level < req: + raise AuthError( + 403, + "You don't have permission to change that state" + ) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index f69f2445a2..668ffa07ca 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -23,7 +23,8 @@ class Membership(object): JOIN = u"join" KNOCK = u"knock" LEAVE = u"leave" - LIST = (INVITE, JOIN, KNOCK, LEAVE) + BAN = u"ban" + LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) class Feedback(object): @@ -42,3 +43,10 @@ class PresenceState(object): UNAVAILABLE = u"unavailable" ONLINE = u"online" FREE_FOR_CHAT = u"free_for_chat" + + +class JoinRules(object): + PUBLIC = u"public" + KNOCK = u"knock" + INVITE = u"invite" + PRIVATE = u"private" diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index f9653e0b2a..9502f5df8f 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -42,6 +42,7 @@ class SynapseEvent(JsonEncodedObject): "user_id", # sender/initiator "content", # HTTP body, JSON "state_key", + "required_power_level", ] internal_keys = [ @@ -52,6 +53,7 @@ class SynapseEvent(JsonEncodedObject): "destinations", "origin", "outlier", + "power_level", ] required_keys = [ @@ -152,3 +154,10 @@ class SynapseEvent(JsonEncodedObject): msg = self._check_json(entry, template[key][0]) if msg: return msg + + +class SynapseStateEvent(SynapseEvent): + def __init__(self, **kwargs): + if "state_key" not in kwargs: + kwargs["state_key"] = "" + super(SynapseStateEvent, self).__init__(**kwargs) diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index c2cdcddf41..159728b2d2 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -16,6 +16,8 @@ from synapse.api.events.room import ( RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent, InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent, + RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent, + RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent ) from synapse.util.stringutils import random_string @@ -30,7 +32,13 @@ class EventFactory(object): RoomMemberEvent, FeedbackEvent, InviteJoinEvent, - RoomConfigEvent + RoomConfigEvent, + RoomPowerLevelsEvent, + RoomJoinRulesEvent, + RoomCreateEvent, + RoomAddStateLevelEvent, + RoomSendEventLevelEvent, + RoomOpsPowerLevelsEvent, ] def __init__(self, hs): diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index 9faad57ac0..f6d3c59a9a 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -15,7 +15,7 @@ from synapse.api.constants import Feedback, Membership from synapse.api.errors import SynapseError -from . import SynapseEvent +from . import SynapseEvent, SynapseStateEvent class GenericEvent(SynapseEvent): @@ -132,3 +132,45 @@ class RoomConfigEvent(SynapseEvent): def get_content_template(self): return {} + + +class RoomCreateEvent(SynapseStateEvent): + TYPE = "m.room.create" + + def get_content_template(self): + return {} + + +class RoomJoinRulesEvent(SynapseStateEvent): + TYPE = "m.room.join_rules" + + def get_content_template(self): + return {} + + +class RoomPowerLevelsEvent(SynapseStateEvent): + TYPE = "m.room.power_levels" + + def get_content_template(self): + return {} + + +class RoomAddStateLevelEvent(SynapseStateEvent): + TYPE = "m.room.add_state_level" + + def get_content_template(self): + return {} + + +class RoomSendEventLevelEvent(SynapseStateEvent): + TYPE = "m.room.send_event_level" + + def get_content_template(self): + return {} + + +class RoomOpsPowerLevelsEvent(SynapseStateEvent): + TYPE = "m.room.ops_levels" + + def get_content_template(self): + return {} diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 6d292ccf9a..606c9c650d 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -20,7 +20,6 @@ from synapse.server import HomeServer from twisted.internet import reactor from twisted.enterprise import adbapi -from twisted.python.log import PythonLoggingObserver from twisted.web.resource import Resource from twisted.web.static import File from twisted.web.server import Site @@ -29,16 +28,17 @@ from synapse.http.client import TwistedHttpClient from synapse.api.urls import ( CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX ) +from synapse.config.homeserver import HomeServerConfig +from synapse.crypto import context_factory from daemonize import Daemonize import twisted.manhole.telnet -import argparse import logging -import logging.config import sqlite3 import os import re +import sys logger = logging.getLogger(__name__) @@ -56,13 +56,13 @@ SCHEMAS = [ # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 1 +SCHEMA_VERSION = 2 class SynapseHomeServer(HomeServer): def build_http_client(self): - return TwistedHttpClient() + return TwistedHttpClient(self) def build_resource_for_client(self): return JsonResource() @@ -206,37 +206,17 @@ class SynapseHomeServer(HomeServer): """ return "%s-%s" % (resource, path_seg) - def start_listening(self, port): - reactor.listenTCP(port, Site(self.root_resource)) - logger.info("Synapse now listening on port %d", port) - - -def setup_logging(verbosity=0, filename=None, config_path=None): - """ Sets up logging with verbosity levels. - - Args: - verbosity: The verbosity level. - filename: Log to the given file rather than to the console. - config_path: Path to a python logging config file. - """ - - if config_path is None: - log_format = ( - '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s' - ) - - level = logging.INFO - if verbosity: - level = logging.DEBUG - - # FIXME: we need a logging.WARN for a -q quiet option - - logging.basicConfig(level=level, filename=filename, format=log_format) - else: - logging.config.fileConfig(config_path) - - observer = PythonLoggingObserver() - observer.start() + def start_listening(self, secure_port, unsecure_port): + if secure_port is not None: + reactor.listenSSL( + secure_port, Site(self.root_resource), self.tls_context_factory + ) + logger.info("Synapse now listening on port %d", secure_port) + if unsecure_port is not None: + reactor.listenTCP( + unsecure_port, Site(self.root_resource) + ) + logger.info("Synapse now listening on port %d", unsecure_port) def run(): @@ -244,78 +224,53 @@ def run(): def setup(): - parser = argparse.ArgumentParser() - parser.add_argument("-p", "--port", dest="port", type=int, default=8080, - help="The port to listen on.") - parser.add_argument("-d", "--database", dest="db", default="homeserver.db", - help="The database name.") - parser.add_argument("-H", "--host", dest="host", default="localhost", - help="The hostname of the server.") - parser.add_argument('-v', '--verbose', dest="verbose", action='count', - help="The verbosity level.") - parser.add_argument('-f', '--log-file', dest="log_file", default=None, - help="File to log to.") - parser.add_argument('--log-config', dest="log_config", default=None, - help="Python logging config") - parser.add_argument('-D', '--daemonize', action='store_true', - default=False, help="Daemonize the home server") - parser.add_argument('--pid-file', dest="pid", help="When running as a " - "daemon, the file to store the pid in", - default="hs.pid") - parser.add_argument("-W", "--webclient", dest="webclient", default=True, - action="store_false", help="Don't host a web client.") - parser.add_argument("--manhole", dest="manhole", type=int, default=None, - help="Turn on the twisted telnet manhole service.") - args = parser.parse_args() - - verbosity = int(args.verbose) if args.verbose else None - - # Because if/when we daemonize we change to root dir. - db_name = os.path.abspath(args.db) - log_file = args.log_file - if log_file: - log_file = os.path.abspath(log_file) - - setup_logging( - verbosity=verbosity, - filename=log_file, - config_path=args.log_config, + config = HomeServerConfig.load_config( + "Synapse Homeserver", + sys.argv[1:], + generate_section="Homeserver" ) - logger.info("Server hostname: %s", args.host) + config.setup_logging() - if re.search(":[0-9]+$", args.host): - domain_with_port = args.host + logger.info("Server hostname: %s", config.server_name) + + if re.search(":[0-9]+$", config.server_name): + domain_with_port = config.server_name else: - domain_with_port = "%s:%s" % (args.host, args.port) + domain_with_port = "%s:%s" % (config.server_name, config.bind_port) + + tls_context_factory = context_factory.ServerContextFactory(config) hs = SynapseHomeServer( - args.host, + config.server_name, domain_with_port=domain_with_port, upload_dir=os.path.abspath("uploads"), - db_name=db_name, + db_name=config.database_path, + tls_context_factory=tls_context_factory, ) hs.register_servlets() hs.create_resource_tree( - web_client=args.webclient, - redirect_root_to_web_client=True) - hs.start_listening(args.port) + web_client=config.webclient, + redirect_root_to_web_client=True, + ) + hs.start_listening(config.bind_port, config.unsecure_port) hs.get_db_pool() - if args.manhole: + if config.manhole: f = twisted.manhole.telnet.ShellFactory() f.username = "matrix" f.password = "rabbithole" f.namespace['hs'] = hs - reactor.listenTCP(args.manhole, f, interface='127.0.0.1') + reactor.listenTCP(config.manhole, f, interface='127.0.0.1') - if args.daemonize: + if config.daemonize: + print config.pid_file daemon = Daemonize( app="synapse-homeserver", - pid=args.pid, + pid=config.pid_file, action=run, auto_close_fds=False, verbose=True, diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py new file mode 100644 index 0000000000..fe8a073cd3 --- /dev/null +++ b/synapse/config/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. diff --git a/synapse/config/_base.py b/synapse/config/_base.py new file mode 100644 index 0000000000..08de6ee5ec --- /dev/null +++ b/synapse/config/_base.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 ConfigParser as configparser +import argparse +import sys +import os +import yaml + + +class Config(object): + def __init__(self, args): + pass + + @staticmethod + def abspath(file_path): + return os.path.abspath(file_path) if file_path else file_path + + @staticmethod + def read_file(file_path): + with open(file_path) as file_stream: + return file_stream.read() + + @staticmethod + def read_config_file(file_path): + with open(file_path) as file_stream: + return yaml.load(file_stream) + + @classmethod + def add_arguments(cls, parser): + pass + + @classmethod + def generate_config(cls, args, config_dir_path): + pass + + @classmethod + def load_config(cls, description, argv, generate_section=None): + config_parser = argparse.ArgumentParser(add_help=False) + config_parser.add_argument( + "-c", "--config-path", + metavar="CONFIG_FILE", + help="Specify config file" + ) + config_parser.add_argument( + "--generate-config", + action="store_true", + help="Generate config file" + ) + config_args, remaining_args = config_parser.parse_known_args(argv) + + if config_args.generate_config: + if not config_args.config_path: + config_parser.error( + "Must specify where to generate the config file" + ) + config_dir_path = os.path.dirname(config_args.config_path) + if os.path.exists(config_args.config_path): + defaults = cls.read_config_file(config_args.config_path) + else: + defaults = {} + else: + if config_args.config_path: + defaults = cls.read_config_file(config_args.config_path) + else: + defaults = {} + + parser = argparse.ArgumentParser( + parents=[config_parser], + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + cls.add_arguments(parser) + parser.set_defaults(**defaults) + + args = parser.parse_args(remaining_args) + + if config_args.generate_config: + config_dir_path = os.path.dirname(config_args.config_path) + config_dir_path = os.path.abspath(config_dir_path) + cls.generate_config(args, config_dir_path) + config = {} + for key, value in vars(args).items(): + if (key not in set(["config_path", "generate_config"]) + and value is not None): + config[key] = value + with open(config_args.config_path, "w") as config_file: + yaml.dump(config, config_file, default_flow_style=False) + sys.exit(0) + + return cls(args) + + + diff --git a/synapse/config/database.py b/synapse/config/database.py new file mode 100644 index 0000000000..edf2361914 --- /dev/null +++ b/synapse/config/database.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 ._base import Config +import os + +class DatabaseConfig(Config): + def __init__(self, args): + super(DatabaseConfig, self).__init__(args) + self.database_path = self.abspath(args.database_path) + + @classmethod + def add_arguments(cls, parser): + super(DatabaseConfig, cls).add_arguments(parser) + db_group = parser.add_argument_group("database") + db_group.add_argument( + "-d", "--database-path", default="homeserver.db", + help="The database name." + ) + + @classmethod + def generate_config(cls, args, config_dir_path): + super(DatabaseConfig, cls).generate_config(args, config_dir_path) + args.database_path = os.path.abspath(args.database_path) + diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py new file mode 100644 index 0000000000..18072e3196 --- /dev/null +++ b/synapse/config/homeserver.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 .tls import TlsConfig +from .server import ServerConfig +from .logger import LoggingConfig +from .database import DatabaseConfig + +class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig): + pass + +if __name__=='__main__': + import sys + HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer") diff --git a/synapse/config/logger.py b/synapse/config/logger.py new file mode 100644 index 0000000000..8db6621ae8 --- /dev/null +++ b/synapse/config/logger.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 ._base import Config + +from twisted.python.log import PythonLoggingObserver +import logging +import logging.config + +class LoggingConfig(Config): + def __init__(self, args): + super(LoggingConfig, self).__init__(args) + self.verbosity = int(args.verbose) if args.verbose else None + self.log_config = self.abspath(args.log_config) + self.log_file = self.abspath(args.log_file) + + @classmethod + def add_arguments(cls, parser): + super(LoggingConfig, cls).add_arguments(parser) + logging_group = parser.add_argument_group("logging") + logging_group.add_argument( + '-v', '--verbose', dest="verbose", action='count', + help="The verbosity level." + ) + logging_group.add_argument( + '-f', '--log-file', dest="log_file", default=None, + help="File to log to." + ) + logging_group.add_argument( + '--log-config', dest="log_config", default=None, + help="Python logging config file" + ) + + def setup_logging(self): + log_format = ( + '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s' + ) + if self.log_config is None: + + level = logging.INFO + if self.verbosity: + level = logging.DEBUG + + # FIXME: we need a logging.WARN for a -q quiet option + + logging.basicConfig( + level=level, + filename=self.log_file, + format=log_format + ) + else: + logging.config.fileConfig(self.log_config) + + observer = PythonLoggingObserver() + observer.start() diff --git a/synapse/config/server.py b/synapse/config/server.py new file mode 100644 index 0000000000..1f20d55d8f --- /dev/null +++ b/synapse/config/server.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 nacl.signing +import os +from ._base import Config +from syutil.base64util import encode_base64, decode_base64 + + +class ServerConfig(Config): + def __init__(self, args): + super(ServerConfig, self).__init__(args) + self.server_name = args.server_name + self.signing_key = self.read_signing_key(args.signing_key_path) + self.bind_port = args.bind_port + self.bind_host = args.bind_host + self.unsecure_port = args.unsecure_port + self.daemonize = args.daemonize + self.pid_file = self.abspath(args.pid_file) + self.webclient = args.no_webclient + self.manhole = args.manhole + + @classmethod + def add_arguments(cls, parser): + super(ServerConfig, cls).add_arguments(parser) + server_group = parser.add_argument_group("server") + server_group.add_argument("-H", "--server-name", default="localhost", + help="The name of the server") + server_group.add_argument("--signing-key-path", + help="The signing key to sign messages with") + server_group.add_argument("-p", "--bind-port", metavar="PORT", + type=int, help="https port to listen on") + server_group.add_argument("--unsecure-port", metavar="PORT", + type=int, help="http port to listen on") + server_group.add_argument("--bind-host", default="", + help="Local interface to listen on") + server_group.add_argument("-D", "--daemonize", action='store_true', + help="Daemonize the home server") + server_group.add_argument('--pid-file', default="hs.pid", + help="When running as a daemon, the file to" + " store the pid in") + server_group.add_argument("-W", "--no-webclient", default=True, + action="store_false", + help="Don't host a web client.") + server_group.add_argument("--manhole", metavar="PORT", dest="manhole", + type=int, + help="Turn on the twisted telnet manhole" + " service on the given port.") + + def read_signing_key(self, signing_key_path): + signing_key_base64 = self.read_file(signing_key_path) + signing_key_bytes = decode_base64(signing_key_base64) + return nacl.signing.SigningKey(signing_key_bytes) + + @classmethod + def generate_config(cls, args, config_dir_path): + super(ServerConfig, cls).generate_config(args, config_dir_path) + base_key_name = os.path.join(config_dir_path, args.server_name) + + args.pid_file = os.path.abspath(args.pid_file) + + if not args.signing_key_path: + args.signing_key_path = base_key_name + ".signing.key" + + if not os.path.exists(args.signing_key_path): + with open(args.signing_key_path, "w") as signing_key_file: + key = nacl.signing.SigningKey.generate() + signing_key_file.write(encode_base64(key.encode())) diff --git a/synapse/config/tls.py b/synapse/config/tls.py new file mode 100644 index 0000000000..005fc1d16e --- /dev/null +++ b/synapse/config/tls.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 ._base import Config + +from OpenSSL import crypto +import subprocess +import os + +GENERATE_DH_PARAMS=False + + +class TlsConfig(Config): + def __init__(self, args): + super(TlsConfig, self).__init__(args) + self.tls_certificate = self.read_tls_certificate( + args.tls_certificate_path + ) + self.tls_private_key = self.read_tls_private_key( + args.tls_private_key_path + ) + self.tls_dh_params_path = self.abspath(args.tls_dh_params_path) + + @classmethod + def add_arguments(cls, parser): + super(TlsConfig, cls).add_arguments(parser) + tls_group = parser.add_argument_group("tls") + tls_group.add_argument("--tls-certificate-path", + help="PEM encoded X509 certificate for TLS") + tls_group.add_argument("--tls-private-key-path", + help="PEM encoded private key for TLS") + tls_group.add_argument("--tls-dh-params-path", + help="PEM dh parameters for ephemeral keys") + + def read_tls_certificate(self, cert_path): + cert_pem = self.read_file(cert_path) + return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) + + def read_tls_private_key(self, private_key_path): + private_key_pem = self.read_file(private_key_path) + return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem) + + @classmethod + def generate_config(cls, args, config_dir_path): + super(TlsConfig, cls).generate_config(args, config_dir_path) + base_key_name = os.path.join(config_dir_path, args.server_name) + + if args.tls_certificate_path is None: + args.tls_certificate_path = base_key_name + ".tls.crt" + + if args.tls_private_key_path is None: + args.tls_private_key_path = base_key_name + ".tls.key" + + if args.tls_dh_params_path is None: + args.tls_dh_params_path = base_key_name + ".tls.dh" + + if not os.path.exists(args.tls_private_key_path): + with open(args.tls_private_key_path, "w") as private_key_file: + tls_private_key = crypto.PKey() + tls_private_key.generate_key(crypto.TYPE_RSA, 2048) + private_key_pem = crypto.dump_privatekey( + crypto.FILETYPE_PEM, tls_private_key + ) + private_key_file.write(private_key_pem) + else: + with open(args.tls_private_key_path) as private_key_file: + private_key_pem = private_key_file.read() + tls_private_key = crypto.load_privatekey( + crypto.FILETYPE_PEM, private_key_pem + ) + + if not os.path.exists(args.tls_certificate_path): + with open(args.tls_certificate_path, "w") as certifcate_file: + cert = crypto.X509() + subject = cert.get_subject() + subject.CN = args.server_name + + cert.set_serial_number(1000) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(tls_private_key) + + cert.sign(tls_private_key, 'sha256') + + cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + + certifcate_file.write(cert_pem) + + if not os.path.exists(args.tls_dh_params_path): + if GENERATE_DH_PARAMS: + subprocess.check_call([ + "openssl", "dhparam", + "-outform", "PEM", + "-out", args.tls_dh_params_path, + "2048" + ]) + else: + with open(args.tls_dh_params_path, "w") as dh_params_file: + dh_params_file.write( + "2048-bit DH parameters taken from rfc3526\n" + "-----BEGIN DH PARAMETERS-----\n" + "MIIBCAKCAQEA///////////JD9qiIWjC" + "NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n" + "IlFKCHmONATd75UZs806QxswKwpt8l8U" + "N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n" + "awv/XLb0Brft7jhr+1qJn6WunyQRfEsf" + "5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n" + "mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS" + "u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n" + "fDKQXkYuNs474553LBgOhgObJ4Oi7Aei" + "j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n" + "5RXSJhiY+gUQFXKOWoqsqmj/////////" + "/wIBAg==\n" + "-----END DH PARAMETERS-----\n" + ) diff --git a/synapse/crypto/config.py b/synapse/crypto/config.py deleted file mode 100644 index 2330133e71..0000000000 --- a/synapse/crypto/config.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 matrix.org -# -# 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 ConfigParser as configparser -import argparse -import socket -import sys -import os -from OpenSSL import crypto -import nacl.signing -from syutil.base64util import encode_base64 -import subprocess - - -def load_config(description, argv): - config_parser = argparse.ArgumentParser(add_help=False) - config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE", - help="Specify config file") - config_args, remaining_args = config_parser.parse_known_args(argv) - if config_args.config_path: - config = configparser.SafeConfigParser() - config.read([config_args.config_path]) - defaults = dict(config.items("KeyServer")) - else: - defaults = {} - parser = argparse.ArgumentParser( - parents=[config_parser], - description=description, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.set_defaults(**defaults) - parser.add_argument("--server-name", default=socket.getfqdn(), - help="The name of the server") - parser.add_argument("--signing-key-path", - help="The signing key to sign responses with") - parser.add_argument("--tls-certificate-path", - help="PEM encoded X509 certificate for TLS") - parser.add_argument("--tls-private-key-path", - help="PEM encoded private key for TLS") - parser.add_argument("--tls-dh-params-path", - help="PEM encoded dh parameters for ephemeral keys") - parser.add_argument("--bind-port", type=int, - help="TCP port to listen on") - parser.add_argument("--bind-host", default="", - help="Local interface to listen on") - - args = parser.parse_args(remaining_args) - - server_config = vars(args) - del server_config["config_path"] - return server_config - - -def generate_config(argv): - parser = argparse.ArgumentParser() - parser.add_argument("-c", "--config-path", help="Specify config file", - metavar="CONFIG_FILE", required=True) - parser.add_argument("--server-name", default=socket.getfqdn(), - help="The name of the server") - parser.add_argument("--signing-key-path", - help="The signing key to sign responses with") - parser.add_argument("--tls-certificate-path", - help="PEM encoded X509 certificate for TLS") - parser.add_argument("--tls-private-key-path", - help="PEM encoded private key for TLS") - parser.add_argument("--tls-dh-params-path", - help="PEM encoded dh parameters for ephemeral keys") - parser.add_argument("--bind-port", type=int, required=True, - help="TCP port to listen on") - parser.add_argument("--bind-host", default="", - help="Local interface to listen on") - - args = parser.parse_args(argv) - - dir_name = os.path.dirname(args.config_path) - base_key_name = os.path.join(dir_name, args.server_name) - - if args.signing_key_path is None: - args.signing_key_path = base_key_name + ".signing.key" - - if args.tls_certificate_path is None: - args.tls_certificate_path = base_key_name + ".tls.crt" - - if args.tls_private_key_path is None: - args.tls_private_key_path = base_key_name + ".tls.key" - - if args.tls_dh_params_path is None: - args.tls_dh_params_path = base_key_name + ".tls.dh" - - if not os.path.exists(args.signing_key_path): - with open(args.signing_key_path, "w") as signing_key_file: - key = nacl.signing.SigningKey.generate() - signing_key_file.write(encode_base64(key.encode())) - - if not os.path.exists(args.tls_private_key_path): - with open(args.tls_private_key_path, "w") as private_key_file: - tls_private_key = crypto.PKey() - tls_private_key.generate_key(crypto.TYPE_RSA, 2048) - private_key_pem = crypto.dump_privatekey( - crypto.FILETYPE_PEM, tls_private_key - ) - private_key_file.write(private_key_pem) - else: - with open(args.tls_private_key_path) as private_key_file: - private_key_pem = private_key_file.read() - tls_private_key = crypto.load_privatekey( - crypto.FILETYPE_PEM, private_key_pem - ) - - if not os.path.exists(args.tls_certificate_path): - with open(args.tls_certificate_path, "w") as certifcate_file: - cert = crypto.X509() - subject = cert.get_subject() - subject.CN = args.server_name - - cert.set_serial_number(1000) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(tls_private_key) - - cert.sign(tls_private_key, 'sha256') - - cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - - certifcate_file.write(cert_pem) - - if not os.path.exists(args.tls_dh_params_path): - subprocess.check_call([ - "openssl", "dhparam", - "-outform", "PEM", - "-out", args.tls_dh_params_path, - "2048" - ]) - - config = configparser.SafeConfigParser() - config.add_section("KeyServer") - for key, value in vars(args).items(): - if key != "config_path": - config.set("KeyServer", key, str(value)) - - with open(args.config_path, "w") as config_file: - config.write(config_file) - - -if __name__ == "__main__": - generate_config(sys.argv[1:]) diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py new file mode 100644 index 0000000000..45958abbf5 --- /dev/null +++ b/synapse/crypto/context_factory.py @@ -0,0 +1,29 @@ +from twisted.internet import reactor, ssl +from OpenSSL import SSL +from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName + + +class ServerContextFactory(ssl.ContextFactory): + """Factory for PyOpenSSL SSL contexts that are used to handle incoming + connections and to make connections to remote servers.""" + + def __init__(self, config): + self._context = SSL.Context(SSL.SSLv23_METHOD) + self.configure_context(self._context, config) + + @staticmethod + def configure_context(context, config): + try: + _ecCurve = _OpenSSLECCurve(_defaultCurveName) + _ecCurve.addECKeyToContext(context) + except: + pass + context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3) + context.use_certificate(config.tls_certificate) + context.use_privatekey(config.tls_private_key) + context.load_tmp_dh(config.tls_dh_params_path) + context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH") + + def getContext(self): + return self._context + diff --git a/synapse/federation/units.py b/synapse/federation/units.py index 2b2f11f36a..b468f70546 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject): "power_level", "prev_state_id", "prev_state_origin", + "required_power_level", ] internal_keys = [ diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index f55bea58a5..7372a10f9b 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -150,10 +150,6 @@ class PresenceHandler(BaseHandler): @defer.inlineCallbacks def is_presence_visible(self, observer_user, observed_user): - defer.returnValue(True) - # return - # FIXME (erikj): This code path absolutely kills the database. - assert(observed_user.is_mine) if observer_user == observed_user: @@ -488,10 +484,6 @@ class PresenceHandler(BaseHandler): def _start_polling_local(self, user, target_user): target_localpart = target_user.localpart - if not self.is_presence_visible(observer_user=user, - observed_user=target_user): - return - if target_localpart not in self._local_pushmap: self._local_pushmap[target_localpart] = set() diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index c54e0f963b..3551615228 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -17,10 +17,12 @@ from twisted.internet import defer from synapse.types import UserID, RoomAlias, RoomID -from synapse.api.constants import Membership +from synapse.api.constants import Membership, JoinRules from synapse.api.errors import StoreError, SynapseError from synapse.api.events.room import ( - RoomMemberEvent, RoomConfigEvent + RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent, + RoomJoinRulesEvent, RoomAddStateLevelEvent, + RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, ) from synapse.util import stringutils from ._base import BaseRoomHandler @@ -62,6 +64,8 @@ class RoomCreationHandler(BaseRoomHandler): else: room_alias = None + is_public = config.get("visibility", None) == "public" + if room_id: # Ensure room_id is the correct type room_id_obj = RoomID.from_string(room_id, self.hs) @@ -71,7 +75,7 @@ class RoomCreationHandler(BaseRoomHandler): yield self.store.store_room( room_id=room_id, room_creator_user_id=user_id, - is_public=config["visibility"] == "public" + is_public=is_public ) else: # autogen room IDs and try to create it. We may clash, so just @@ -85,7 +89,7 @@ class RoomCreationHandler(BaseRoomHandler): yield self.store.store_room( room_id=gen_room_id.to_string(), room_creator_user_id=user_id, - is_public=config["visibility"] == "public" + is_public=is_public ) room_id = gen_room_id.to_string() break @@ -94,18 +98,10 @@ class RoomCreationHandler(BaseRoomHandler): if not room_id: raise StoreError(500, "Couldn't generate a room ID.") - config_event = self.event_factory.create_event( - etype=RoomConfigEvent.TYPE, - room_id=room_id, - user_id=user_id, - content=config, - ) - snapshot = yield self.store.snapshot_room( - room_id=room_id, - user_id=user_id, - state_type=RoomConfigEvent.TYPE, - state_key="", + user = self.hs.parse_userid(user_id) + creation_events = self._create_events_for_new_room( + user, room_id, is_public=is_public ) if room_alias: @@ -115,11 +111,18 @@ class RoomCreationHandler(BaseRoomHandler): servers=[self.hs.hostname], ) - yield self.state_handler.handle_new_event(config_event, snapshot) - # store_id = persist... - federation_handler = self.hs.get_handlers().federation_handler - yield federation_handler.handle_new_event(config_event, snapshot) + + for event in creation_events: + snapshot = yield self.store.snapshot_room( + room_id=room_id, + user_id=user_id, + ) + + logger.debug("Event: %s", event) + + yield self.state_handler.handle_new_event(event, snapshot) + yield self._on_new_room_event(event, snapshot, extra_users=[user]) content = {"membership": Membership.JOIN} join_event = self.event_factory.create_event( @@ -142,6 +145,62 @@ class RoomCreationHandler(BaseRoomHandler): defer.returnValue(result) + def _create_events_for_new_room(self, creator, room_id, is_public=False): + event_keys = { + "room_id": room_id, + "user_id": creator.to_string(), + "required_power_level": 10, + } + + def create(etype, **content): + return self.event_factory.create_event( + etype=etype, + content=content, + **event_keys + ) + + creation_event = create( + etype=RoomCreateEvent.TYPE, + creator=creator.to_string(), + ) + + power_levels_event = self.event_factory.create_event( + etype=RoomPowerLevelsEvent.TYPE, + content={creator.to_string(): 10, "default": 0}, + **event_keys + ) + + join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE + join_rules_event = create( + etype=RoomJoinRulesEvent.TYPE, + join_rule=join_rule, + ) + + add_state_event = create( + etype=RoomAddStateLevelEvent.TYPE, + level=10, + ) + + send_event = create( + etype=RoomSendEventLevelEvent.TYPE, + level=0, + ) + + ops = create( + etype=RoomOpsPowerLevelsEvent.TYPE, + ban_level=5, + kick_level=5, + ) + + return [ + creation_event, + power_levels_event, + join_rules_event, + add_state_event, + send_event, + ops, + ] + class RoomMemberHandler(BaseRoomHandler): # TODO(paul): This handler currently contains a messy conflation of @@ -285,6 +344,16 @@ class RoomMemberHandler(BaseRoomHandler): if do_auth: yield self.auth.check(event, snapshot, raises=True) + # If we're banning someone, set a req power level + if event.membership == Membership.BAN: + if not hasattr(event, "required_power_level") or event.required_power_level is None: + # Add some default required_power_level + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + event.required_power_level = user_level + if prev_state and prev_state.membership == event.membership: # double same action, treat this event as a NOOP. defer.returnValue({}) @@ -445,10 +514,9 @@ class RoomMemberHandler(BaseRoomHandler): host = target_user.domain destinations.append(host) - # If we are joining a remote HS, include that. - if membership == Membership.JOIN: - host = target_user.domain - destinations.append(host) + # Always include target domain + host = target_user.domain + destinations.append(host) return self._on_new_room_event( event, snapshot, extra_destinations=destinations, diff --git a/synapse/http/client.py b/synapse/http/client.py index 36ba2c6591..093bdf0e3f 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -113,8 +113,9 @@ class TwistedHttpClient(HttpClient): requests. """ - def __init__(self): + def __init__(self, hs): self.agent = MatrixHttpAgent(reactor) + self.hs = hs @defer.inlineCallbacks def put_json(self, destination, path, data): @@ -177,7 +178,10 @@ class TwistedHttpClient(HttpClient): retries_left = 5 # TODO: setup and pass in an ssl_context to enable TLS - endpoint = matrix_endpoint(reactor, destination, timeout=10) + endpoint = matrix_endpoint( + reactor, destination, timeout=10, + ssl_context_factory=self.hs.tls_context_factory + ) while True: try: diff --git a/synapse/http/endpoint.py b/synapse/http/endpoint.py index d91500b07d..a6ebe23567 100644 --- a/synapse/http/endpoint.py +++ b/synapse/http/endpoint.py @@ -53,7 +53,7 @@ def matrix_endpoint(reactor, destination, ssl_context_factory=None, default_port = 8080 else: transport_endpoint = SSL4ClientEndpoint - endpoint_kw_args.update(ssl_context_factory=ssl_context_factory) + endpoint_kw_args.update(sslContextFactory=ssl_context_factory) default_port = 443 if port is None: diff --git a/synapse/http/server.py b/synapse/http/server.py index 243b71744d..0b87718bfa 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -325,7 +325,11 @@ class ContentRepoResource(resource.Resource): # FIXME (erikj): These should use constants. file_name = os.path.basename(fname) - url = "http://%s/_matrix/content/%s" % ( + # FIXME: we can't assume what the public mounted path of the repo is + # ...plus self-signed SSL won't work to remote clients anyway + # ...and we can't assume that it's SSL anyway, as we might want to + # server it via the non-SSL listener... + url = "https://%s/_matrix/content/%s" % ( self.hs.domain_with_port, file_name ) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index e8faba3eeb..7a441391f0 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -19,6 +19,10 @@ from synapse.api.events.room import ( RoomMemberEvent, RoomTopicEvent, FeedbackEvent, # RoomConfigEvent, RoomNameEvent, + RoomJoinRulesEvent, + RoomPowerLevelsEvent, + RoomAddStateLevelEvent, + RoomSendEventLevelEvent, ) from synapse.util.logutils import log_function @@ -33,6 +37,7 @@ from .roommember import RoomMemberStore from .stream import StreamStore from .pdu import StatePduStore, PduStore from .transactions import TransactionStore +from .keys import KeyStore import json import logging @@ -45,7 +50,7 @@ logger = logging.getLogger(__name__) class DataStore(RoomMemberStore, RoomStore, RegistrationStore, StreamStore, ProfileStore, FeedbackStore, PresenceStore, PduStore, StatePduStore, TransactionStore, - DirectoryStore): + DirectoryStore, KeyStore): def __init__(self, hs): super(DataStore, self).__init__(hs) @@ -122,13 +127,19 @@ class DataStore(RoomMemberStore, RoomStore, if event.type == RoomMemberEvent.TYPE: self._store_room_member_txn(txn, event) elif event.type == FeedbackEvent.TYPE: - self._store_feedback_txn(txn,event) -# elif event.type == RoomConfigEvent.TYPE: -# self._store_room_config_txn(txn, event) + self._store_feedback_txn(txn, event) elif event.type == RoomNameEvent.TYPE: self._store_room_name_txn(txn, event) elif event.type == RoomTopicEvent.TYPE: self._store_room_topic_txn(txn, event) + elif event.type == RoomJoinRulesEvent.TYPE: + self._store_join_rule(txn, event) + elif event.type == RoomPowerLevelsEvent.TYPE: + self._store_power_levels(txn, event) + elif event.type == RoomAddStateLevelEvent.TYPE: + self._store_add_state_level(txn, event) + elif event.type == RoomSendEventLevelEvent.TYPE: + self._store_send_event_level(txn, event) vals = { "topological_ordering": event.depth, @@ -222,7 +233,6 @@ class DataStore(RoomMemberStore, RoomStore, defer.returnValue(self.min_token) - def snapshot_room(self, room_id, user_id, state_type=None, state_key=None): """Snapshot the room for an update by a user Args: diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py new file mode 100644 index 0000000000..4d19b9f641 --- /dev/null +++ b/synapse/storage/keys.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 _base import SQLBaseStore + +from twisted.internet import defer + +import OpenSSL +import nacl.signing + +class KeyStore(SQLBaseStore): + """Persistence for signature verification keys and tls X.509 certificates + """ + + @defer.inlineCallbacks + def get_server_certificate(self, server_name): + """Retrieve the TLS X.509 certificate for the given server + Args: + server_name (bytes): The name of the server. + Returns: + (OpenSSL.crypto.X509): The tls certificate. + """ + tls_certificate_bytes, = yield self._simple_select_one( + table="server_tls_certificates", + keyvalues={"server_name": server_name}, + retcols=("tls_certificate",), + ) + tls_certificate = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes, + ) + defer.returnValue(tls_certificate) + + def store_server_certificate(self, server_name, key_server, ts_now_ms, + tls_certificate): + """Stores the TLS X.509 certificate for the given server + Args: + server_name (bytes): The name of the server. + key_server (bytes): Where the certificate was looked up + ts_now_ms (int): The time now in milliseconds + tls_certificate (OpenSSL.crypto.X509): The X.509 certificate. + """ + tls_certificate_bytes = OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_ASN1, tls_certificate + ) + return self._simple_insert( + table="server_tls_certificates", + keyvalues={ + "server_name": server_name, + "key_server": key_server, + "ts_added_ms": ts_now_ms, + "tls_certificate": tls_certificate_bytes, + }, + ) + + @defer.inlineCallbacks + def get_server_verification_key(self, server_name): + """Retrieve the NACL verification key for a given server + Args: + server_name (bytes): The name of the server. + Returns: + (nacl.signing.VerifyKey): The verification key. + """ + verification_key_bytes, = yield self._simple_select_one( + table="server_signature_keys", + key_values={"server_name": server_name}, + retcols=("tls_certificate",), + ) + verification_key = nacl.signing.VerifyKey(verification_key_bytes) + defer.returnValue(verification_key) + + def store_server_verification_key(self, server_name, key_version, + key_server, ts_now_ms, verification_key): + """Stores a NACL verification key for the given server. + Args: + server_name (bytes): The name of the server. + key_version (bytes): The version of the key for the server. + key_server (bytes): Where the verification key was looked up + ts_now_ms (int): The time now in milliseconds + verification_key (nacl.signing.VerifyKey): The NACL verify key. + """ + verification_key_bytes = verification_key.encode() + return self._simple_insert( + table="server_signature_keys", + key_values={ + "server_name": server_name, + "key_version": key_version, + "key_server": key_server, + "ts_added_ms": ts_now_ms, + "verification_key": verification_key_bytes, + }, + ) diff --git a/synapse/storage/room.py b/synapse/storage/room.py index d1f1a232f8..3ca07f4350 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -27,6 +27,9 @@ import logging logger = logging.getLogger(__name__) +OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level")) + + class RoomStore(SQLBaseStore): @defer.inlineCallbacks @@ -129,6 +132,98 @@ class RoomStore(SQLBaseStore): defer.returnValue(ret) + @defer.inlineCallbacks + def get_room_join_rule(self, room_id): + sql = ( + "SELECT join_rule FROM room_join_rules as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = yield self._execute(None, sql, room_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + else: + defer.returnValue(None) + + def get_power_level(self, room_id, user_id): + return self._db_pool.runInteraction( + self._get_power_level, + room_id, user_id, + ) + + def _get_power_level(self, txn, room_id, user_id): + sql = ( + "SELECT level FROM room_power_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? AND r.user_id = ? " + ) + + rows = txn.execute(sql, (room_id, user_id,)).fetchall() + + if len(rows) == 1: + return rows[0][0] + + sql = ( + "SELECT level FROM room_default_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = txn.execute(sql, (room_id,)).fetchall() + + if len(rows) == 1: + return rows[0][0] + else: + return None + + def get_ops_levels(self, room_id): + return self._db_pool.runInteraction( + self._get_ops_levels, + room_id, + ) + + def _get_ops_levels(self, txn, room_id): + sql = ( + "SELECT ban_level, kick_level FROM room_ops_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = txn.execute(sql, (room_id,)).fetchall() + + if len(rows) == 1: + return OpsLevel(rows[0][0], rows[0][1]) + else: + return OpsLevel(None, None) + + def get_add_state_level(self, room_id): + return self._get_level_from_table("room_add_state_levels", room_id) + + def get_send_event_level(self, room_id): + return self._get_level_from_table("room_send_event_levels", room_id) + + @defer.inlineCallbacks + def _get_level_from_table(self, table, room_id): + sql = ( + "SELECT level FROM %(table)s as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) % {"table": table} + + rows = yield self._execute(None, sql, room_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + else: + defer.returnValue(None) + def _store_room_topic_txn(self, txn, event): self._simple_insert_txn( txn, @@ -151,6 +246,92 @@ class RoomStore(SQLBaseStore): } ) + def _store_join_rule(self, txn, event): + self._simple_insert_txn( + txn, + "room_join_rules", + { + "event_id": event.event_id, + "room_id": event.room_id, + "join_rule": event.content["join_rule"], + }, + ) + + def _store_power_levels(self, txn, event): + for user_id, level in event.content.items(): + if user_id == "default": + self._simple_insert_txn( + txn, + "room_default_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": level, + }, + ) + else: + self._simple_insert_txn( + txn, + "room_power_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "user_id": user_id, + "level": level + }, + ) + + def _store_default_level(self, txn, event): + self._simple_insert_txn( + txn, + "room_default_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": event.content["default_level"], + }, + ) + + def _store_add_state_level(self, txn, event): + self._simple_insert_txn( + txn, + "room_add_state_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": event.content["level"], + }, + ) + + def _store_send_event_level(self, txn, event): + self._simple_insert_txn( + txn, + "room_send_event_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": event.content["level"], + }, + ) + + def _store_ops_level(self, txn, event): + content = { + "event_id": event.event_id, + "room_id": event.room_id, + } + + if "kick_level" in event.content: + content["kick_level"] = event.content["kick_level"] + + if "ban_level" in event.content: + content["ban_level"] = event.content["ban_level"] + + self._simple_insert_txn( + txn, + "room_send_event_levels", + content, + ) + class RoomsTable(Table): table_name = "rooms" diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index e92f21ef3b..dbefbbda31 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -96,8 +96,71 @@ CREATE TABLE IF NOT EXISTS rooms( creator TEXT ); +CREATE TABLE IF NOT EXISTS room_join_rules( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + join_rule TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id); +CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id); + + +CREATE TABLE IF NOT EXISTS room_power_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + level INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id); + + +CREATE TABLE IF NOT EXISTS room_default_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id); +CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_add_state_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id); +CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_send_event_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id); +CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_ops_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + ban_level INTEGER, + kick_level INTEGER +); + +CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id); +CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id); + + CREATE TABLE IF NOT EXISTS room_hosts( room_id TEXT NOT NULL, host TEXT NOT NULL, CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE ); + +CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id); diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql new file mode 100644 index 0000000000..45cdbcecae --- /dev/null +++ b/synapse/storage/schema/keys.sql @@ -0,0 +1,30 @@ +/* Copyright 2014 matrix.org + * + * 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. + */ +CREATE TABLE IF NOT EXISTS server_tls_certificates( + server_name TEXT, -- Server name. + key_server TEXT, -- Which key server the certificate was fetched from. + ts_added_ms INTEGER, -- When the certifcate was added. + tls_certificate BLOB, -- DER encoded x509 certificate. + CONSTRAINT uniqueness UNIQUE (server_name) +); + +CREATE TABLE IF NOT EXISTS server_signature_keys( + server_name TEXT, -- Server name. + key_version TEXT, -- Key version. + key_server TEXT, -- Which key server the key was fetched form. + ts_added_ms INTEGER, -- When the key was added. + verification_key BLOB, -- NACL verification key. + CONSTRAINT uniqueness UNIQUE (server_name, key_version) +); diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 844c4e3fb7..2eea474391 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -196,8 +196,6 @@ class PresenceStateTestCase(unittest.TestCase): SynapseError ) - test_get_disallowed_state.skip = "Presence permissions are disabled" - @defer.inlineCallbacks def test_set_my_state(self): mocked_set = self.datastore.set_presence_state diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index 09d2a92e16..219a53c426 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -330,6 +330,8 @@ class RoomCreationTest(unittest.TestCase): datastore=NonCallableMock(spec_set=[ "store_room", "snapshot_room", + "persist_event", + "get_joined_hosts_for_room", ]), http_client=NonCallableMock(spec_set=[]), notifier=NonCallableMock(spec_set=["on_new_room_event"]), @@ -362,6 +364,10 @@ class RoomCreationTest(unittest.TestCase): ]) self.room_member_handler = self.handlers.room_member_handler + def hosts(room): + return defer.succeed([]) + self.datastore.get_joined_hosts_for_room.side_effect = hosts + @defer.inlineCallbacks def test_room_creation(self): user_id = "@foo:red" @@ -385,9 +391,3 @@ class RoomCreationTest(unittest.TestCase): self.assertTrue(self.state_handler.handle_new_event.called) self.assertTrue(self.federation.handle_new_event.called) - config_event = self.federation.handle_new_event.call_args[0][0] - - self.assertEquals(RoomConfigEvent.TYPE, config_event.type) - self.assertEquals(room_id, config_event.room_id) - self.assertEquals(user_id, config_event.user_id) - self.assertEquals(config, config_event.content) diff --git a/tests/utils.py b/tests/utils.py index ea7d6893c6..2c146eab55 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -234,6 +234,20 @@ class MemoryDataStore(object): def get_room_events_max_id(self): return 0 # TODO (erikj) + def get_send_event_level(self, room_id): + return defer.succeed(0) + + def get_power_level(self, room_id, user_id): + return defer.succeed(0) + + def get_add_state_level(self, room_id): + return defer.succeed(0) + + def get_room_join_rule(self, room_id): + # TODO (erikj): This should be configurable + return defer.succeed("invite") + + def _format_call(args, kwargs): return ", ".join( ["%r" % (a) for a in args] + |