diff options
Diffstat (limited to 'synapse')
-rw-r--r-- | synapse/api/errors.py | 37 | ||||
-rw-r--r-- | synapse/api/ratelimiting.py | 79 | ||||
-rwxr-xr-x | synapse/app/homeserver.py | 1 | ||||
-rw-r--r-- | synapse/config/_base.py | 2 | ||||
-rw-r--r-- | synapse/config/homeserver.py | 4 | ||||
-rw-r--r-- | synapse/config/ratelimiting.py | 35 | ||||
-rw-r--r-- | synapse/crypto/context_factory.py | 16 | ||||
-rw-r--r-- | synapse/handlers/_base.py | 15 | ||||
-rw-r--r-- | synapse/handlers/message.py | 2 | ||||
-rw-r--r-- | synapse/handlers/room.py | 3 | ||||
-rw-r--r-- | synapse/http/server.py | 14 | ||||
-rw-r--r-- | synapse/rest/room.py | 4 | ||||
-rw-r--r-- | synapse/server.py | 5 | ||||
-rw-r--r-- | synapse/storage/schema/room_aliases.sql | 15 | ||||
-rw-r--r-- | synapse/util/logutils.py | 1 |
15 files changed, 212 insertions, 21 deletions
diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 21ededc5ae..ad79bc7ff9 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -28,6 +28,7 @@ class Codes(object): UNKNOWN = "M_UNKNOWN" NOT_FOUND = "M_NOT_FOUND" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" + LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" class CodeMessageException(Exception): @@ -38,11 +39,15 @@ class CodeMessageException(Exception): super(CodeMessageException, self).__init__("%d: %s" % (code, msg)) self.code = code self.msg = msg + self.response_code_message = None + + def error_dict(self): + return cs_error(self.msg) class SynapseError(CodeMessageException): """A base error which can be caught for all synapse events.""" - def __init__(self, code, msg, errcode=""): + def __init__(self, code, msg, errcode=Codes.UNKNOWN): """Constructs a synapse error. Args: @@ -53,6 +58,11 @@ class SynapseError(CodeMessageException): super(SynapseError, self).__init__(code, msg) self.errcode = errcode + def error_dict(self): + return cs_error( + self.msg, + self.errcode, + ) class RoomError(SynapseError): """An error raised when a room event fails.""" @@ -91,13 +101,26 @@ class StoreError(SynapseError): pass -def cs_exception(exception): - if isinstance(exception, SynapseError): +class LimitExceededError(SynapseError): + """A client has sent too many requests and is being throttled. + """ + def __init__(self, code=429, msg="Too Many Requests", retry_after_ms=None, + errcode=Codes.LIMIT_EXCEEDED): + super(LimitExceededError, self).__init__(code, msg, errcode) + self.retry_after_ms = retry_after_ms + self.response_code_message = "Too Many Requests" + + def error_dict(self): return cs_error( - exception.msg, - Codes.UNKNOWN if not exception.errcode else exception.errcode) - elif isinstance(exception, CodeMessageException): - return cs_error(exception.msg) + self.msg, + self.errcode, + retry_after_ms=self.retry_after_ms, + ) + + +def cs_exception(exception): + if isinstance(exception, CodeMessageException): + return exception.error_dict() else: logging.error("Unknown exception type: %s", type(exception)) diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py new file mode 100644 index 0000000000..b9b6a9de1a --- /dev/null +++ b/synapse/api/ratelimiting.py @@ -0,0 +1,79 @@ +# 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 collections + + +class Ratelimiter(object): + """ + Ratelimit message sending by user. + """ + + def __init__(self): + self.message_counts = collections.OrderedDict() + + def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count): + """Can the user send a message? + Args: + user_id: The user sending a message. + time_now_s: The time now. + msg_rate_hz: The long term number of messages a user can send in a + second. + burst_count: How many messages the user can send before being + limited. + Returns: + A pair of a bool indicating if they can send a message now and a + time in seconds of when they can next send a message. + """ + self.prune_message_counts(time_now_s) + message_count, time_start, _ignored = self.message_counts.pop( + user_id, (0., time_now_s, None), + ) + time_delta = time_now_s - time_start + sent_count = message_count - time_delta * msg_rate_hz + if sent_count < 0: + allowed = True + time_start = time_now_s + message_count = 1. + elif sent_count > burst_count - 1.: + allowed = False + else: + allowed = True + message_count += 1 + + self.message_counts[user_id] = ( + message_count, time_start, msg_rate_hz + ) + + if msg_rate_hz > 0: + time_allowed = ( + time_start + (message_count - burst_count + 1) / msg_rate_hz + ) + if time_allowed < time_now_s: + time_allowed = time_now_s + else: + time_allowed = -1 + + return allowed, time_allowed + + def prune_message_counts(self, time_now_s): + for user_id in self.message_counts.keys(): + message_count, time_start, msg_rate_hz = ( + self.message_counts[user_id] + ) + time_delta = time_now_s - time_start + if message_count - time_delta * msg_rate_hz > 0: + break + else: + del self.message_counts[user_id] diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 606c9c650d..8a7cd07fec 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -247,6 +247,7 @@ def setup(): upload_dir=os.path.abspath("uploads"), db_name=config.database_path, tls_context_factory=tls_context_factory, + config=config, ) hs.register_servlets() diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 1913179c3a..4b445806da 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import ConfigParser as configparser import argparse import sys import os diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 18072e3196..a9aa4c735c 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -17,8 +17,10 @@ from .tls import TlsConfig from .server import ServerConfig from .logger import LoggingConfig from .database import DatabaseConfig +from .ratelimiting import RatelimitConfig -class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig): +class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, + RatelimitConfig): pass if __name__=='__main__': diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py new file mode 100644 index 0000000000..ee572e1fbf --- /dev/null +++ b/synapse/config/ratelimiting.py @@ -0,0 +1,35 @@ +# 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 + +class RatelimitConfig(Config): + + def __init__(self, args): + super(RatelimitConfig, self).__init__(args) + self.rc_messages_per_second = args.rc_messages_per_second + self.rc_message_burst_count = args.rc_message_burst_count + + @classmethod + def add_arguments(cls, parser): + super(RatelimitConfig, cls).add_arguments(parser) + rc_group = parser.add_argument_group("ratelimiting") + rc_group.add_argument( + "--rc-messages-per-second", type=float, default=0.2, + help="number of messages a client can send per second" + ) + rc_group.add_argument( + "--rc-message-burst-count", type=float, default=10, + help="number of message a client can send before being throttled" + ) diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py index 45958abbf5..344f2dd218 100644 --- a/synapse/crypto/context_factory.py +++ b/synapse/crypto/context_factory.py @@ -1,4 +1,18 @@ -from twisted.internet import reactor, ssl +# 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 twisted.internet import ssl from OpenSSL import SSL from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index b37c8be964..57429ad2ef 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -14,6 +14,7 @@ # limitations under the License. from twisted.internet import defer +from synapse.api.errors import LimitExceededError class BaseHandler(object): @@ -25,8 +26,22 @@ class BaseHandler(object): self.room_lock = hs.get_room_lock_manager() self.state_handler = hs.get_state_handler() self.distributor = hs.get_distributor() + self.ratelimiter = hs.get_ratelimiter() + self.clock = hs.get_clock() self.hs = hs + def ratelimit(self, user_id): + time_now = self.clock.time() + allowed, time_allowed = self.ratelimiter.send_message( + user_id, time_now, + msg_rate_hz=self.hs.config.rc_messages_per_second, + burst_count=self.hs.config.rc_message_burst_count, + ) + if not allowed: + raise LimitExceededError( + retry_after_ms=int(1000*(time_allowed - time_now)), + ) + class BaseRoomHandler(BaseHandler): diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4aeb2089f5..c9e3c4e451 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -76,6 +76,8 @@ class MessageHandler(BaseRoomHandler): Raises: SynapseError if something went wrong. """ + + self.ratelimit(event.user_id) # TODO(paul): Why does 'event' not have a 'user' object? user = self.hs.parse_userid(event.user_id) assert user.is_mine, "User must be our own: %s" % (user,) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 53aa77405c..58afced8f5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -49,6 +49,7 @@ class RoomCreationHandler(BaseRoomHandler): SynapseError if the room ID was taken, couldn't be stored, or something went horribly wrong. """ + self.ratelimit(user_id) if "room_alias_name" in config: room_alias = RoomAlias.create_local( @@ -110,8 +111,6 @@ class RoomCreationHandler(BaseRoomHandler): servers=[self.hs.hostname], ) - federation_handler = self.hs.get_handlers().federation_handler - @defer.inlineCallbacks def handle_event(event): snapshot = yield self.store.snapshot_room( diff --git a/synapse/http/server.py b/synapse/http/server.py index 0b87718bfa..74c220e869 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -140,7 +140,8 @@ class JsonResource(HttpServer, resource.Resource): self._send_response( request, e.code, - cs_exception(e) + cs_exception(e), + response_code_message=e.response_code_message ) except Exception as e: logger.exception(e) @@ -150,7 +151,8 @@ class JsonResource(HttpServer, resource.Resource): {"error": "Internal server error"} ) - def _send_response(self, request, code, response_json_object): + def _send_response(self, request, code, response_json_object, + response_code_message=None): # could alternatively use request.notifyFinish() and flip a flag when # the Deferred fires, but since the flag is RIGHT THERE it seems like # a waste. @@ -166,7 +168,8 @@ class JsonResource(HttpServer, resource.Resource): json_bytes = encode_pretty_printed_json(response_json_object) # TODO: Only enable CORS for the requests that need it. - respond_with_json_bytes(request, code, json_bytes, send_cors=True) + respond_with_json_bytes(request, code, json_bytes, send_cors=True, + response_code_message=response_code_message) @staticmethod def _request_user_agent_is_curl(request): @@ -350,7 +353,8 @@ class ContentRepoResource(resource.Resource): send_cors=True) -def respond_with_json_bytes(request, code, json_bytes, send_cors=False): +def respond_with_json_bytes(request, code, json_bytes, send_cors=False, + response_code_message=None): """Sends encoded JSON in response to the given request. Args: @@ -362,7 +366,7 @@ def respond_with_json_bytes(request, code, json_bytes, send_cors=False): Returns: twisted.web.server.NOT_DONE_YET""" - request.setResponseCode(code) + request.setResponseCode(code, message=response_code_message) request.setHeader(b"Content-Type", b"application/json") if send_cors: diff --git a/synapse/rest/room.py b/synapse/rest/room.py index a10b3b54f9..d76a2f5cd4 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -388,7 +388,7 @@ class RoomMembershipRestServlet(RestServlet): def register(self, http_server): # /rooms/$roomid/[invite|join|leave] PATTERN = ("/rooms/(?P<room_id>[^/]*)/" + - "(?P<membership_action>join|invite|leave)") + "(?P<membership_action>join|invite|leave|ban)") register_txn_path(self, PATTERN, http_server) @defer.inlineCallbacks @@ -399,7 +399,7 @@ class RoomMembershipRestServlet(RestServlet): # target user is you unless it is an invite state_key = user.to_string() - if membership_action == "invite": + if membership_action in ["invite", "ban"]: if "user_id" not in content: raise SynapseError(400, "Missing user_id key.") state_key = content["user_id"] diff --git a/synapse/server.py b/synapse/server.py index 3e72b2bcd5..35e311a47d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -32,6 +32,7 @@ from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager from synapse.streams.events import EventSources +from synapse.api.ratelimiting import Ratelimiter class BaseHomeServer(object): @@ -73,6 +74,7 @@ class BaseHomeServer(object): 'resource_for_web_client', 'resource_for_content_repo', 'event_sources', + 'ratelimiter', ] def __init__(self, hostname, **kwargs): @@ -190,6 +192,9 @@ class HomeServer(BaseHomeServer): def build_event_sources(self): return EventSources(self) + def build_ratelimiter(self): + return Ratelimiter() + def register_servlets(self): """ Register all servlets associated with this HomeServer. """ diff --git a/synapse/storage/schema/room_aliases.sql b/synapse/storage/schema/room_aliases.sql index 71a8b90e4d..d72b79e6ed 100644 --- a/synapse/storage/schema/room_aliases.sql +++ b/synapse/storage/schema/room_aliases.sql @@ -1,3 +1,18 @@ +/* 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 room_aliases( room_alias TEXT NOT NULL, room_id TEXT NOT NULL diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py index b94a749786..3d7c46ae44 100644 --- a/synapse/util/logutils.py +++ b/synapse/util/logutils.py @@ -19,7 +19,6 @@ from functools import wraps import logging import inspect -import traceback def log_function(f): |