Merge branch 'develop' of github.com:matrix-org/synapse into postgres

This commit is contained in:
Erik Johnston 2015-04-27 14:41:40 +01:00
commit 6f8e2d517e
24 changed files with 641 additions and 390 deletions

View File

@ -86,7 +86,7 @@ Homeserver Installation
======================= =======================
System requirements: System requirements:
- POSIX-compliant system (tested on Linux & OSX) - POSIX-compliant system (tested on Linux & OS X)
- Python 2.7 - Python 2.7
Synapse is written in python but some of the libraries is uses are written in Synapse is written in python but some of the libraries is uses are written in
@ -128,6 +128,15 @@ To set up your homeserver, run (in your virtualenv, as before)::
Substituting your host and domain name as appropriate. Substituting your host and domain name as appropriate.
This will generate you a config file that you can then customise, but it will
also generate a set of keys for you. These keys will allow your Home Server to
identify itself to other Home Servers, so don't lose or delete them. It would be
wise to back them up somewhere safe. If, for whatever reason, you do need to
change your Home Server's keys, you may find that other Home Servers have the
old key cached. If you update the signing key, you should change the name of the
key in the <server name>.signing.key file (the second word, which by default is
, 'auto') to something different.
By default, registration of new users is disabled. You can either enable By default, registration of new users is disabled. You can either enable
registration in the config by specifying ``enable_registration: true`` registration in the config by specifying ``enable_registration: true``
(it is then recommended to also set up CAPTCHA), or (it is then recommended to also set up CAPTCHA), or
@ -367,10 +376,6 @@ SRV record, as that is the name other machines will expect it to have::
You may additionally want to pass one or more "-v" options, in order to 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. increase the verbosity of logging output; at least for initial testing.
For the initial alpha release, the homeserver is not speaking TLS for
either client-server or server-server traffic for ease of debugging. We have
also not spent any time yet getting the homeserver to run behind loadbalancers.
Running a Demo Federation of Homeservers Running a Demo Federation of Homeservers
---------------------------------------- ----------------------------------------
@ -433,7 +438,7 @@ track 3PID logins and publish end-user public keys.
It's currently early days for identity servers as Matrix is not yet using 3PIDs It's currently early days for identity servers as Matrix is not yet using 3PIDs
as the primary means of identity and E2E encryption is not complete. As such, as the primary means of identity and E2E encryption is not complete. As such,
we are running a single identity server (http://matrix.org:8090) at the current we are running a single identity server (https://matrix.org) at the current
time. time.

View File

@ -1,3 +1,37 @@
Upgrading to v0.x.x
===================
Application services have had a breaking API change in this version.
They can no longer register themselves with a home server using the AS HTTP API. This
decision was made because a compromised application service with free reign to register
any regex in effect grants full read/write access to the home server if a regex of ``.*``
is used. An attack where a compromised AS re-registers itself with ``.*`` was deemed too
big of a security risk to ignore, and so the ability to register with the HS remotely has
been removed.
It has been replaced by specifying a list of application service registrations in
``homeserver.yaml``::
app_service_config_files: ["registration-01.yaml", "registration-02.yaml"]
Where ``registration-01.yaml`` looks like::
url: <String> # e.g. "https://my.application.service.com"
as_token: <String>
hs_token: <String>
sender_localpart: <String> # This is a new field which denotes the user_id localpart when using the AS token
namespaces:
users:
- exclusive: <Boolean>
regex: <String> # e.g. "@prefix_.*"
aliases:
- exclusive: <Boolean>
regex: <String>
rooms:
- exclusive: <Boolean>
regex: <String>
Upgrading to v0.8.0 Upgrading to v0.8.0
=================== ===================

50
docs/metrics-howto.rst Normal file
View File

@ -0,0 +1,50 @@
How to monitor Synapse metrics using Prometheus
===============================================
1: Install prometheus:
Follow instructions at http://prometheus.io/docs/introduction/install/
2: Enable synapse metrics:
Simply setting a (local) port number will enable it. Pick a port.
prometheus itself defaults to 9090, so starting just above that for
locally monitored services seems reasonable. E.g. 9092:
Add to homeserver.yaml
metrics_port: 9092
Restart synapse
3: Check out synapse-prometheus-config
https://github.com/matrix-org/synapse-prometheus-config
4: Add ``synapse.html`` and ``synapse.rules``
The ``.html`` file needs to appear in prometheus's ``consoles`` directory,
and the ``.rules`` file needs to be invoked somewhere in the main config
file. A symlink to each from the git checkout into the prometheus directory
might be easiest to ensure ``git pull`` keeps it updated.
5: Add a prometheus target for synapse
This is easiest if prometheus runs on the same machine as synapse, as it can
then just use localhost::
global: {
rule_file: "synapse.rules"
}
job: {
name: "synapse"
target_group: {
target: "http://localhost:9092/"
}
}
6: Start prometheus::
./prometheus -config.file=prometheus.conf
7: Wait a few seconds for it to start and perform the first scrape,
then visit the console:
http://server-where-prometheus-runs:9090/consoles/synapse.html

View File

@ -183,18 +183,10 @@ class Auth(object):
else: else:
join_rule = JoinRules.INVITE join_rule = JoinRules.INVITE
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
ban_level, kick_level, redact_level = ( # FIXME (erikj): What should we do here as the default?
self._get_ops_level_from_event_state( ban_level = self._get_named_level(auth_events, "ban", 50)
event,
auth_events,
)
)
logger.debug( logger.debug(
"is_membership_change_allowed: %s", "is_membership_change_allowed: %s",
@ -210,11 +202,6 @@ class Auth(object):
} }
) )
if ban_level:
ban_level = int(ban_level)
else:
ban_level = 50 # FIXME (erikj): What should we do here?
if Membership.JOIN != membership: if Membership.JOIN != membership:
# JOIN is the only action you can perform if you're not in the room # JOIN is the only action you can perform if you're not in the room
if not caller_in_room: # caller isn't joined if not caller_in_room: # caller isn't joined
@ -259,10 +246,7 @@ class Auth(object):
403, "You cannot unban user &s." % (target_user_id,) 403, "You cannot unban user &s." % (target_user_id,)
) )
elif target_user_id != event.user_id: elif target_user_id != event.user_id:
if kick_level: kick_level = self._get_named_level(auth_events, "kick", 50)
kick_level = int(kick_level)
else:
kick_level = 50 # FIXME (erikj): What should we do here?
if user_level < kick_level: if user_level < kick_level:
raise AuthError( raise AuthError(
@ -276,34 +260,42 @@ class Auth(object):
return True return True
def _get_power_level_from_event_state(self, event, user_id, auth_events): def _get_power_level_event(self, auth_events):
key = (EventTypes.PowerLevels, "", ) key = (EventTypes.PowerLevels, "", )
power_level_event = auth_events.get(key) return auth_events.get(key)
level = None
def _get_user_power_level(self, user_id, auth_events):
power_level_event = self._get_power_level_event(auth_events)
if power_level_event: if power_level_event:
level = power_level_event.content.get("users", {}).get(user_id) level = power_level_event.content.get("users", {}).get(user_id)
if not level: if not level:
level = power_level_event.content.get("users_default", 0) level = power_level_event.content.get("users_default", 0)
if level is None:
return 0
else:
return int(level)
else: else:
key = (EventTypes.Create, "", ) key = (EventTypes.Create, "", )
create_event = auth_events.get(key) create_event = auth_events.get(key)
if (create_event is not None and if (create_event is not None and
create_event.content["creator"] == user_id): create_event.content["creator"] == user_id):
return 100 return 100
else:
return 0
return level def _get_named_level(self, auth_events, name, default):
power_level_event = self._get_power_level_event(auth_events)
def _get_ops_level_from_event_state(self, event, auth_events): if not power_level_event:
key = (EventTypes.PowerLevels, "", ) return default
power_level_event = auth_events.get(key)
if power_level_event: level = power_level_event.content.get(name, None)
return ( if level is not None:
power_level_event.content.get("ban", 50), return int(level)
power_level_event.content.get("kick", 50), else:
power_level_event.content.get("redact", 50), return default
)
return None, None, None,
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_req(self, request): def get_user_by_req(self, request):
@ -498,16 +490,7 @@ class Auth(object):
else: else:
send_level = 0 send_level = 0
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
if user_level:
user_level = int(user_level)
else:
user_level = 0
if user_level < send_level: if user_level < send_level:
raise AuthError( raise AuthError(
@ -539,16 +522,9 @@ class Auth(object):
return True return True
def _check_redaction(self, event, auth_events): def _check_redaction(self, event, auth_events):
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
_, _, redact_level = self._get_ops_level_from_event_state( redact_level = self._get_named_level(auth_events, "redact", 50)
event,
auth_events,
)
if user_level < redact_level: if user_level < redact_level:
raise AuthError( raise AuthError(
@ -576,11 +552,7 @@ class Auth(object):
if not current_state: if not current_state:
return return
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
# Check other levels: # Check other levels:
levels_to_check = [ levels_to_check = [

View File

@ -35,8 +35,8 @@ class Codes(object):
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
CAPTCHA_INVALID = "M_CAPTCHA_INVALID" CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
MISSING_PARAM = "M_MISSING_PARAM", MISSING_PARAM = "M_MISSING_PARAM"
TOO_LARGE = "M_TOO_LARGE", TOO_LARGE = "M_TOO_LARGE"
EXCLUSIVE = "M_EXCLUSIVE" EXCLUSIVE = "M_EXCLUSIVE"

View File

@ -53,6 +53,7 @@ class RegistrationConfig(Config):
@classmethod @classmethod
def generate_config(cls, args, config_dir_path): def generate_config(cls, args, config_dir_path):
super(RegistrationConfig, cls).generate_config(args, config_dir_path)
if args.enable_registration is None: if args.enable_registration is None:
args.enable_registration = False args.enable_registration = False

View File

@ -24,6 +24,8 @@ from synapse.api.errors import SynapseError, Codes
from synapse.util.retryutils import get_retry_limiter from synapse.util.retryutils import get_retry_limiter
from synapse.util.async import create_observer
from OpenSSL import crypto from OpenSSL import crypto
import logging import logging
@ -38,6 +40,8 @@ class Keyring(object):
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.hs = hs self.hs = hs
self.key_downloads = {}
@defer.inlineCallbacks @defer.inlineCallbacks
def verify_json_for_server(self, server_name, json_object): def verify_json_for_server(self, server_name, json_object):
logger.debug("Verifying for %s", server_name) logger.debug("Verifying for %s", server_name)
@ -97,6 +101,22 @@ class Keyring(object):
defer.returnValue(cached[0]) defer.returnValue(cached[0])
return return
download = self.key_downloads.get(server_name)
if download is None:
download = self._get_server_verify_key_impl(server_name, key_ids)
self.key_downloads[server_name] = download
@download.addBoth
def callback(ret):
del self.key_downloads[server_name]
return ret
r = yield create_observer(download)
defer.returnValue(r)
@defer.inlineCallbacks
def _get_server_verify_key_impl(self, server_name, key_ids):
# Try to fetch the key from the remote server. # Try to fetch the key from the remote server.
limiter = yield get_retry_limiter( limiter = yield get_retry_limiter(

View File

@ -36,6 +36,9 @@ metrics = synapse.metrics.get_metrics_for(__name__)
# Don't bother bumping "last active" time if it differs by less than 60 seconds # Don't bother bumping "last active" time if it differs by less than 60 seconds
LAST_ACTIVE_GRANULARITY = 60*1000 LAST_ACTIVE_GRANULARITY = 60*1000
# Keep no more than this number of offline serial revisions
MAX_OFFLINE_SERIALS = 1000
# TODO(paul): Maybe there's one of these I can steal from somewhere # TODO(paul): Maybe there's one of these I can steal from somewhere
def partition(l, func): def partition(l, func):
@ -135,6 +138,9 @@ class PresenceHandler(BaseHandler):
self._remote_sendmap = {} self._remote_sendmap = {}
# map remote users to sets of local users who're interested in them # map remote users to sets of local users who're interested in them
self._remote_recvmap = {} self._remote_recvmap = {}
# list of (serial, set of(userids)) tuples, ordered by serial, latest
# first
self._remote_offline_serials = []
# map any user to a UserPresenceCache # map any user to a UserPresenceCache
self._user_cachemap = {} self._user_cachemap = {}
@ -714,8 +720,24 @@ class PresenceHandler(BaseHandler):
statuscache=statuscache, statuscache=statuscache,
) )
user_id = user.to_string()
if state["presence"] == PresenceState.OFFLINE: if state["presence"] == PresenceState.OFFLINE:
self._remote_offline_serials.insert(
0,
(self._user_cachemap_latest_serial, set([user_id]))
)
while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS:
self._remote_offline_serials.pop() # remove the oldest
del self._user_cachemap[user] del self._user_cachemap[user]
else:
# Remove the user from remote_offline_serials now that they're
# no longer offline
for idx, elem in enumerate(self._remote_offline_serials):
(_, user_ids) = elem
user_ids.discard(user_id)
if not user_ids:
self._remote_offline_serials.pop(idx)
for poll in content.get("poll", []): for poll in content.get("poll", []):
user = UserID.from_string(poll) user = UserID.from_string(poll)
@ -836,6 +858,8 @@ class PresenceEventSource(object):
presence = self.hs.get_handlers().presence_handler presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap cachemap = presence._user_cachemap
clock = self.clock
latest_serial = None
updates = [] updates = []
# TODO(paul): use a DeferredList ? How to limit concurrency. # TODO(paul): use a DeferredList ? How to limit concurrency.
@ -845,18 +869,31 @@ class PresenceEventSource(object):
if cached.serial <= from_key: if cached.serial <= from_key:
continue continue
if (yield self.is_visible(observer_user, observed_user)): if not (yield self.is_visible(observer_user, observed_user)):
updates.append((observed_user, cached)) continue
if latest_serial is None or cached.serial > latest_serial:
latest_serial = cached.serial
updates.append(cached.make_event(user=observed_user, clock=clock))
# TODO(paul): limit # TODO(paul): limit
for serial, user_ids in presence._remote_offline_serials:
if serial < from_key:
break
for u in user_ids:
updates.append({
"type": "m.presence",
"content": {"user_id": u, "presence": PresenceState.OFFLINE},
})
# TODO(paul): For the v2 API we want to tell the client their from_key
# is too old if we fell off the end of the _remote_offline_serials
# list, and get them to invalidate+resync. In v1 we have no such
# concept so this is a best-effort result.
if updates: if updates:
clock = self.clock defer.returnValue((updates, latest_serial))
latest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
defer.returnValue((data, latest_serial))
else: else:
defer.returnValue(([], presence._user_cachemap_latest_serial)) defer.returnValue(([], presence._user_cachemap_latest_serial))

View File

@ -124,7 +124,7 @@ class RoomCreationHandler(BaseHandler):
msg_handler = self.hs.get_handlers().message_handler msg_handler = self.hs.get_handlers().message_handler
for event in creation_events: for event in creation_events:
yield msg_handler.create_and_send_event(event) yield msg_handler.create_and_send_event(event, ratelimit=False)
if "name" in config: if "name" in config:
name = config["name"] name = config["name"]
@ -134,7 +134,7 @@ class RoomCreationHandler(BaseHandler):
"sender": user_id, "sender": user_id,
"state_key": "", "state_key": "",
"content": {"name": name}, "content": {"name": name},
}) }, ratelimit=False)
if "topic" in config: if "topic" in config:
topic = config["topic"] topic = config["topic"]
@ -144,7 +144,7 @@ class RoomCreationHandler(BaseHandler):
"sender": user_id, "sender": user_id,
"state_key": "", "state_key": "",
"content": {"topic": topic}, "content": {"topic": topic},
}) }, ratelimit=False)
for invitee in invite_list: for invitee in invite_list:
yield msg_handler.create_and_send_event({ yield msg_handler.create_and_send_event({
@ -153,7 +153,7 @@ class RoomCreationHandler(BaseHandler):
"room_id": room_id, "room_id": room_id,
"sender": user_id, "sender": user_id,
"content": {"membership": Membership.INVITE}, "content": {"membership": Membership.INVITE},
}) }, ratelimit=False)
result = {"room_id": room_id} result = {"room_id": room_id}

View File

@ -51,6 +51,80 @@ response_timer = metrics.register_distribution(
labels=["method", "servlet"] labels=["method", "servlet"]
) )
_next_request_id = 0
def request_handler(request_handler):
"""Wraps a method that acts as a request handler with the necessary logging
and exception handling.
The method must have a signature of "handle_foo(self, request)". The
argument "self" must have "version_string" and "clock" attributes. The
argument "request" must be a twisted HTTP request.
The method must return a deferred. If the deferred succeeds we assume that
a response has been sent. If the deferred fails with a SynapseError we use
it to send a JSON response with the appropriate HTTP reponse code. If the
deferred fails with any other type of error we send a 500 reponse.
We insert a unique request-id into the logging context for this request and
log the response and duration for this request.
"""
@defer.inlineCallbacks
def wrapped_request_handler(self, request):
global _next_request_id
request_id = "%s-%s" % (request.method, _next_request_id)
_next_request_id += 1
with LoggingContext(request_id) as request_context:
request_context.request = request_id
code = None
start = self.clock.time_msec()
try:
logger.info(
"Received request: %s %s",
request.method, request.path
)
yield request_handler(self, request)
code = request.code
except CodeMessageException as e:
code = e.code
if isinstance(e, SynapseError):
logger.info(
"%s SynapseError: %s - %s", request, code, e.msg
)
else:
logger.exception(e)
outgoing_responses_counter.inc(request.method, str(code))
respond_with_json(
request, code, cs_exception(e), send_cors=True,
pretty_print=_request_user_agent_is_curl(request),
version_string=self.version_string,
)
except:
code = 500
logger.exception(
"Failed handle request %s.%s on %r: %r",
request_handler.__module__,
request_handler.__name__,
self,
request
)
respond_with_json(
request,
500,
{"error": "Internal server error"},
send_cors=True
)
finally:
code = str(code) if code else "-"
end = self.clock.time_msec()
logger.info(
"Processed request: %dms %s %s %s",
end-start, code, request.method, request.path
)
return wrapped_request_handler
class HttpServer(object): class HttpServer(object):
""" Interface for registering callbacks on a HTTP server """ Interface for registering callbacks on a HTTP server
@ -115,102 +189,56 @@ class JsonResource(HttpServer, resource.Resource):
def render(self, request): def render(self, request):
""" This get's called by twisted every time someone sends us a request. """ This get's called by twisted every time someone sends us a request.
""" """
self._async_render_with_logging_context(request) self._async_render(request)
return server.NOT_DONE_YET return server.NOT_DONE_YET
_request_id = 0 @request_handler
@defer.inlineCallbacks
def _async_render_with_logging_context(self, request):
request_id = "%s-%s" % (request.method, JsonResource._request_id)
JsonResource._request_id += 1
with LoggingContext(request_id) as request_context:
request_context.request = request_id
yield self._async_render(request)
@defer.inlineCallbacks @defer.inlineCallbacks
def _async_render(self, request): def _async_render(self, request):
""" This get's called by twisted every time someone sends us a request. """ This get's called by twisted every time someone sends us a request.
This checks if anyone has registered a callback for that method and This checks if anyone has registered a callback for that method and
path. path.
""" """
code = None
start = self.clock.time_msec() start = self.clock.time_msec()
try: if request.method == "OPTIONS":
# Just say yes to OPTIONS. self._send_response(request, 200, {})
if request.method == "OPTIONS": return
self._send_response(request, 200, {}) # Loop through all the registered callbacks to check if the method
return # and path regex match
for path_entry in self.path_regexs.get(request.method, []):
m = path_entry.pattern.match(request.path)
if not m:
continue
# Loop through all the registered callbacks to check if the method # We found a match! Trigger callback and then return the
# and path regex match # returned response. We pass both the request and any
for path_entry in self.path_regexs.get(request.method, []): # matched groups from the regex to the callback.
m = path_entry.pattern.match(request.path)
if not m:
continue
# We found a match! Trigger callback and then return the callback = path_entry.callback
# returned response. We pass both the request and any
# matched groups from the regex to the callback.
callback = path_entry.callback servlet_instance = getattr(callback, "__self__", None)
if servlet_instance is not None:
servlet_instance = getattr(callback, "__self__", None) servlet_classname = servlet_instance.__class__.__name__
if servlet_instance is not None:
servlet_classname = servlet_instance.__class__.__name__
else:
servlet_classname = "%r" % callback
incoming_requests_counter.inc(request.method, servlet_classname)
args = [
urllib.unquote(u).decode("UTF-8") for u in m.groups()
]
logger.info(
"Received request: %s %s",
request.method, request.path
)
code, response = yield callback(request, *args)
self._send_response(request, code, response)
response_timer.inc_by(
self.clock.time_msec() - start, request.method, servlet_classname
)
return
# Huh. No one wanted to handle that? Fiiiiiine. Send 400.
raise UnrecognizedRequestError()
except CodeMessageException as e:
if isinstance(e, SynapseError):
logger.info("%s SynapseError: %s - %s", request, e.code, e.msg)
else: else:
logger.exception(e) servlet_classname = "%r" % callback
incoming_requests_counter.inc(request.method, servlet_classname)
code = e.code args = [
self._send_response( urllib.unquote(u).decode("UTF-8") for u in m.groups()
request, ]
code,
cs_exception(e),
response_code_message=e.response_code_message
)
except Exception as e:
logger.exception(e)
self._send_response(
request,
500,
{"error": "Internal server error"}
)
finally:
code = str(code) if code else "-"
end = self.clock.time_msec() code, response = yield callback(request, *args)
logger.info(
"Processed request: %dms %s %s %s", self._send_response(request, code, response)
end-start, code, request.method, request.path response_timer.inc_by(
self.clock.time_msec() - start, request.method, servlet_classname
) )
return
# Huh. No one wanted to handle that? Fiiiiiine. Send 400.
raise UnrecognizedRequestError()
def _send_response(self, request, code, response_json_object, def _send_response(self, request, code, response_json_object,
response_code_message=None): response_code_message=None):
# could alternatively use request.notifyFinish() and flip a flag when # could alternatively use request.notifyFinish() and flip a flag when
@ -229,20 +257,10 @@ class JsonResource(HttpServer, resource.Resource):
request, code, response_json_object, request, code, response_json_object,
send_cors=True, send_cors=True,
response_code_message=response_code_message, response_code_message=response_code_message,
pretty_print=self._request_user_agent_is_curl, pretty_print=_request_user_agent_is_curl(request),
version_string=self.version_string, version_string=self.version_string,
) )
@staticmethod
def _request_user_agent_is_curl(request):
user_agents = request.requestHeaders.getRawHeaders(
"User-Agent", default=[]
)
for user_agent in user_agents:
if "curl" in user_agent:
return True
return False
class RootRedirect(resource.Resource): class RootRedirect(resource.Resource):
"""Redirects the root '/' path to another path.""" """Redirects the root '/' path to another path."""
@ -263,8 +281,8 @@ class RootRedirect(resource.Resource):
def respond_with_json(request, code, json_object, send_cors=False, def respond_with_json(request, code, json_object, send_cors=False,
response_code_message=None, pretty_print=False, response_code_message=None, pretty_print=False,
version_string=""): version_string=""):
if not pretty_print: if pretty_print:
json_bytes = encode_pretty_printed_json(json_object) json_bytes = encode_pretty_printed_json(json_object) + "\n"
else: else:
json_bytes = encode_canonical_json(json_object) json_bytes = encode_canonical_json(json_object)
@ -304,3 +322,13 @@ def respond_with_json_bytes(request, code, json_bytes, send_cors=False,
request.write(json_bytes) request.write(json_bytes)
request.finish() request.finish()
return NOT_DONE_YET return NOT_DONE_YET
def _request_user_agent_is_curl(request):
user_agents = request.requestHeaders.getRawHeaders(
"User-Agent", default=[]
)
for user_agent in user_agents:
if "curl" in user_agent:
return True
return False

View File

@ -23,6 +23,61 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_integer(request, name, default=None, required=False):
if name in request.args:
try:
return int(request.args[name][0])
except:
message = "Query parameter %r must be an integer" % (name,)
raise SynapseError(400, message)
else:
if required:
message = "Missing integer query parameter %r" % (name,)
raise SynapseError(400, message)
else:
return default
def parse_boolean(request, name, default=None, required=False):
if name in request.args:
try:
return {
"true": True,
"false": False,
}[request.args[name][0]]
except:
message = (
"Boolean query parameter %r must be one of"
" ['true', 'false']"
) % (name,)
raise SynapseError(400, message)
else:
if required:
message = "Missing boolean query parameter %r" % (name,)
raise SynapseError(400, message)
else:
return default
def parse_string(request, name, default=None, required=False,
allowed_values=None, param_type="string"):
if name in request.args:
value = request.args[name][0]
if allowed_values is not None and value not in allowed_values:
message = "Query parameter %r must be one of [%s]" % (
name, ", ".join(repr(v) for v in allowed_values)
)
raise SynapseError(message)
else:
return value
else:
if required:
message = "Missing %s query parameter %r" % (param_type, name)
raise SynapseError(400, message)
else:
return default
class RestServlet(object): class RestServlet(object):
""" A Synapse REST Servlet. """ A Synapse REST Servlet.
@ -56,58 +111,3 @@ class RestServlet(object):
http_server.register_path(method, pattern, method_handler) http_server.register_path(method, pattern, method_handler)
else: else:
raise NotImplementedError("RestServlet must register something.") raise NotImplementedError("RestServlet must register something.")
@staticmethod
def parse_integer(request, name, default=None, required=False):
if name in request.args:
try:
return int(request.args[name][0])
except:
message = "Query parameter %r must be an integer" % (name,)
raise SynapseError(400, message)
else:
if required:
message = "Missing integer query parameter %r" % (name,)
raise SynapseError(400, message)
else:
return default
@staticmethod
def parse_boolean(request, name, default=None, required=False):
if name in request.args:
try:
return {
"true": True,
"false": False,
}[request.args[name][0]]
except:
message = (
"Boolean query parameter %r must be one of"
" ['true', 'false']"
) % (name,)
raise SynapseError(400, message)
else:
if required:
message = "Missing boolean query parameter %r" % (name,)
raise SynapseError(400, message)
else:
return default
@staticmethod
def parse_string(request, name, default=None, required=False,
allowed_values=None, param_type="string"):
if name in request.args:
value = request.args[name][0]
if allowed_values is not None and value not in allowed_values:
message = "Query parameter %r must be one of [%s]" % (
name, ", ".join(repr(v) for v in allowed_values)
)
raise SynapseError(message)
else:
return value
else:
if required:
message = "Missing %s query parameter %r" % (param_type, name)
raise SynapseError(400, message)
else:
return default

View File

@ -1,3 +1,17 @@
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.push.rulekinds import PRIORITY_CLASS_MAP, PRIORITY_CLASS_INVERSE_MAP from synapse.push.rulekinds import PRIORITY_CLASS_MAP, PRIORITY_CLASS_INVERSE_MAP
@ -112,7 +126,25 @@ def make_base_prepend_override_rules():
def make_base_append_override_rules(): def make_base_append_override_rules():
return [ return [
{ {
'rule_id': 'global/override/.m.rule.call', 'rule_id': 'global/override/.m.rule.suppress_notices',
'conditions': [
{
'kind': 'event_match',
'key': 'content.msgtype',
'pattern': 'm.notice',
}
],
'actions': [
'dont_notify',
]
}
]
def make_base_append_underride_rules(user):
return [
{
'rule_id': 'global/underride/.m.rule.call',
'conditions': [ 'conditions': [
{ {
'kind': 'event_match', 'kind': 'event_match',
@ -131,19 +163,6 @@ def make_base_append_override_rules():
} }
] ]
}, },
{
'rule_id': 'global/override/.m.rule.suppress_notices',
'conditions': [
{
'kind': 'event_match',
'key': 'content.msgtype',
'pattern': 'm.notice',
}
],
'actions': [
'dont_notify',
]
},
{ {
'rule_id': 'global/override/.m.rule.contains_display_name', 'rule_id': 'global/override/.m.rule.contains_display_name',
'conditions': [ 'conditions': [
@ -162,7 +181,7 @@ def make_base_append_override_rules():
] ]
}, },
{ {
'rule_id': 'global/override/.m.rule.room_one_to_one', 'rule_id': 'global/underride/.m.rule.room_one_to_one',
'conditions': [ 'conditions': [
{ {
'kind': 'room_member_count', 'kind': 'room_member_count',
@ -179,12 +198,7 @@ def make_base_append_override_rules():
'value': False 'value': False
} }
] ]
} },
]
def make_base_append_underride_rules(user):
return [
{ {
'rule_id': 'global/underride/.m.rule.invite_for_me', 'rule_id': 'global/underride/.m.rule.invite_for_me',
'conditions': [ 'conditions': [

View File

@ -1,3 +1,17 @@
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
PRIORITY_CLASS_MAP = { PRIORITY_CLASS_MAP = {
'underride': 1, 'underride': 1,
'sender': 2, 'sender': 2,

View File

@ -1,10 +1,24 @@
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging import logging
from distutils.version import LooseVersion from distutils.version import LooseVersion
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REQUIREMENTS = { REQUIREMENTS = {
"syutil>=0.0.4": ["syutil"], "syutil>=0.0.5": ["syutil"],
"Twisted==14.0.2": ["twisted==14.0.2"], "Twisted==14.0.2": ["twisted==14.0.2"],
"service_identity>=1.0.0": ["service_identity>=1.0.0"], "service_identity>=1.0.0": ["service_identity>=1.0.0"],
"pyopenssl>=0.14": ["OpenSSL>=0.14"], "pyopenssl>=0.14": ["OpenSSL>=0.14"],
@ -43,8 +57,8 @@ DEPENDENCY_LINKS = [
), ),
github_link( github_link(
project="matrix-org/syutil", project="matrix-org/syutil",
version="v0.0.4", version="v0.0.5",
egg="syutil-0.0.4", egg="syutil-0.0.5",
), ),
github_link( github_link(
project="matrix-org/matrix-angular-sdk", project="matrix-org/matrix-angular-sdk",

View File

@ -15,7 +15,9 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.http.servlet import RestServlet from synapse.http.servlet import (
RestServlet, parse_string, parse_integer, parse_boolean
)
from synapse.handlers.sync import SyncConfig from synapse.handlers.sync import SyncConfig
from synapse.types import StreamToken from synapse.types import StreamToken
from synapse.events.utils import ( from synapse.events.utils import (
@ -87,20 +89,20 @@ class SyncRestServlet(RestServlet):
def on_GET(self, request): def on_GET(self, request):
user, client = yield self.auth.get_user_by_req(request) user, client = yield self.auth.get_user_by_req(request)
timeout = self.parse_integer(request, "timeout", default=0) timeout = parse_integer(request, "timeout", default=0)
limit = self.parse_integer(request, "limit", required=True) limit = parse_integer(request, "limit", required=True)
gap = self.parse_boolean(request, "gap", default=True) gap = parse_boolean(request, "gap", default=True)
sort = self.parse_string( sort = parse_string(
request, "sort", default="timeline,asc", request, "sort", default="timeline,asc",
allowed_values=self.ALLOWED_SORT allowed_values=self.ALLOWED_SORT
) )
since = self.parse_string(request, "since") since = parse_string(request, "since")
set_presence = self.parse_string( set_presence = parse_string(
request, "set_presence", default="online", request, "set_presence", default="online",
allowed_values=self.ALLOWED_PRESENCE allowed_values=self.ALLOWED_PRESENCE
) )
backfill = self.parse_boolean(request, "backfill", default=False) backfill = parse_boolean(request, "backfill", default=False)
filter_id = self.parse_string(request, "filter", default=None) filter_id = parse_string(request, "filter", default=None)
logger.info( logger.info(
"/sync: user=%r, timeout=%r, limit=%r, gap=%r, sort=%r, since=%r," "/sync: user=%r, timeout=%r, limit=%r, gap=%r, sort=%r, since=%r,"

View File

@ -18,13 +18,15 @@ from .thumbnailer import Thumbnailer
from synapse.http.server import respond_with_json from synapse.http.server import respond_with_json
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
from synapse.api.errors import ( from synapse.api.errors import (
cs_exception, CodeMessageException, cs_error, Codes, SynapseError cs_error, Codes, SynapseError
) )
from twisted.internet import defer from twisted.internet import defer
from twisted.web.resource import Resource from twisted.web.resource import Resource
from twisted.protocols.basic import FileSender from twisted.protocols.basic import FileSender
from synapse.util.async import create_observer
import os import os
import logging import logging
@ -32,6 +34,18 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_media_id(request):
try:
server_name, media_id = request.postpath
return (server_name, media_id)
except:
raise SynapseError(
404,
"Invalid media id token %r" % (request.postpath,),
Codes.UNKNOWN,
)
class BaseMediaResource(Resource): class BaseMediaResource(Resource):
isLeaf = True isLeaf = True
@ -45,74 +59,9 @@ class BaseMediaResource(Resource):
self.max_upload_size = hs.config.max_upload_size self.max_upload_size = hs.config.max_upload_size
self.max_image_pixels = hs.config.max_image_pixels self.max_image_pixels = hs.config.max_image_pixels
self.filepaths = filepaths self.filepaths = filepaths
self.version_string = hs.version_string
self.downloads = {} self.downloads = {}
@staticmethod
def catch_errors(request_handler):
@defer.inlineCallbacks
def wrapped_request_handler(self, request):
try:
yield request_handler(self, request)
except CodeMessageException as e:
logger.info("Responding with error: %r", e)
respond_with_json(
request, e.code, cs_exception(e), send_cors=True
)
except:
logger.exception(
"Failed handle request %s.%s on %r",
request_handler.__module__,
request_handler.__name__,
self,
)
respond_with_json(
request,
500,
{"error": "Internal server error"},
send_cors=True
)
return wrapped_request_handler
@staticmethod
def _parse_media_id(request):
try:
server_name, media_id = request.postpath
return (server_name, media_id)
except:
raise SynapseError(
404,
"Invalid media id token %r" % (request.postpath,),
Codes.UNKNOWN,
)
@staticmethod
def _parse_integer(request, arg_name, default=None):
try:
if default is None:
return int(request.args[arg_name][0])
else:
return int(request.args.get(arg_name, [default])[0])
except:
raise SynapseError(
400,
"Missing integer argument %r" % (arg_name,),
Codes.UNKNOWN,
)
@staticmethod
def _parse_string(request, arg_name, default=None):
try:
if default is None:
return request.args[arg_name][0]
else:
return request.args.get(arg_name, [default])[0]
except:
raise SynapseError(
400,
"Missing string argument %r" % (arg_name,),
Codes.UNKNOWN,
)
def _respond_404(self, request): def _respond_404(self, request):
respond_with_json( respond_with_json(
request, 404, request, 404,
@ -140,7 +89,7 @@ class BaseMediaResource(Resource):
def callback(media_info): def callback(media_info):
del self.downloads[key] del self.downloads[key]
return media_info return media_info
return download return create_observer(download)
@defer.inlineCallbacks @defer.inlineCallbacks
def _get_remote_media_impl(self, server_name, media_id): def _get_remote_media_impl(self, server_name, media_id):

View File

@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from .base_resource import BaseMediaResource from .base_resource import BaseMediaResource, parse_media_id
from synapse.http.server import request_handler
from twisted.web.server import NOT_DONE_YET from twisted.web.server import NOT_DONE_YET
from twisted.internet import defer from twisted.internet import defer
@ -28,15 +29,10 @@ class DownloadResource(BaseMediaResource):
self._async_render_GET(request) self._async_render_GET(request)
return NOT_DONE_YET return NOT_DONE_YET
@BaseMediaResource.catch_errors @request_handler
@defer.inlineCallbacks @defer.inlineCallbacks
def _async_render_GET(self, request): def _async_render_GET(self, request):
try: server_name, media_id = parse_media_id(request)
server_name, media_id = request.postpath
except:
self._respond_404(request)
return
if server_name == self.server_name: if server_name == self.server_name:
yield self._respond_local_file(request, media_id) yield self._respond_local_file(request, media_id)
else: else:

View File

@ -1,3 +1,17 @@
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pydenticon import Generator from pydenticon import Generator
from twisted.web.resource import Resource from twisted.web.resource import Resource

View File

@ -14,7 +14,9 @@
# limitations under the License. # limitations under the License.
from .base_resource import BaseMediaResource from .base_resource import BaseMediaResource, parse_media_id
from synapse.http.servlet import parse_string, parse_integer
from synapse.http.server import request_handler
from twisted.web.server import NOT_DONE_YET from twisted.web.server import NOT_DONE_YET
from twisted.internet import defer from twisted.internet import defer
@ -31,14 +33,14 @@ class ThumbnailResource(BaseMediaResource):
self._async_render_GET(request) self._async_render_GET(request)
return NOT_DONE_YET return NOT_DONE_YET
@BaseMediaResource.catch_errors @request_handler
@defer.inlineCallbacks @defer.inlineCallbacks
def _async_render_GET(self, request): def _async_render_GET(self, request):
server_name, media_id = self._parse_media_id(request) server_name, media_id = parse_media_id(request)
width = self._parse_integer(request, "width") width = parse_integer(request, "width")
height = self._parse_integer(request, "height") height = parse_integer(request, "height")
method = self._parse_string(request, "method", "scale") method = parse_string(request, "method", "scale")
m_type = self._parse_string(request, "type", "image/png") m_type = parse_string(request, "type", "image/png")
if server_name == self.server_name: if server_name == self.server_name:
yield self._respond_local_thumbnail( yield self._respond_local_thumbnail(

View File

@ -13,12 +13,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from synapse.http.server import respond_with_json from synapse.http.server import respond_with_json, request_handler
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
from synapse.api.errors import ( from synapse.api.errors import SynapseError
cs_exception, SynapseError, CodeMessageException
)
from twisted.web.server import NOT_DONE_YET from twisted.web.server import NOT_DONE_YET
from twisted.internet import defer from twisted.internet import defer
@ -69,53 +67,42 @@ class UploadResource(BaseMediaResource):
defer.returnValue("mxc://%s/%s" % (self.server_name, media_id)) defer.returnValue("mxc://%s/%s" % (self.server_name, media_id))
@request_handler
@defer.inlineCallbacks @defer.inlineCallbacks
def _async_render_POST(self, request): def _async_render_POST(self, request):
try: auth_user, client = yield self.auth.get_user_by_req(request)
auth_user, client = yield self.auth.get_user_by_req(request) # TODO: The checks here are a bit late. The content will have
# TODO: The checks here are a bit late. The content will have # already been uploaded to a tmp file at this point
# already been uploaded to a tmp file at this point content_length = request.getHeader("Content-Length")
content_length = request.getHeader("Content-Length") if content_length is None:
if content_length is None: raise SynapseError(
raise SynapseError( msg="Request must specify a Content-Length", code=400
msg="Request must specify a Content-Length", code=400 )
) if int(content_length) > self.max_upload_size:
if int(content_length) > self.max_upload_size: raise SynapseError(
raise SynapseError( msg="Upload request body is too large",
msg="Upload request body is too large", code=413,
code=413,
)
headers = request.requestHeaders
if headers.hasHeader("Content-Type"):
media_type = headers.getRawHeaders("Content-Type")[0]
else:
raise SynapseError(
msg="Upload request missing 'Content-Type'",
code=400,
)
# if headers.hasHeader("Content-Disposition"):
# disposition = headers.getRawHeaders("Content-Disposition")[0]
# TODO(markjh): parse content-dispostion
content_uri = yield self.create_content(
media_type, None, request.content.read(),
content_length, auth_user
) )
respond_with_json( headers = request.requestHeaders
request, 200, {"content_uri": content_uri}, send_cors=True
) if headers.hasHeader("Content-Type"):
except CodeMessageException as e: media_type = headers.getRawHeaders("Content-Type")[0]
logger.exception(e) else:
respond_with_json(request, e.code, cs_exception(e), send_cors=True) raise SynapseError(
except: msg="Upload request missing 'Content-Type'",
logger.exception("Failed to store file") code=400,
respond_with_json(
request,
500,
{"error": "Internal server error"},
send_cors=True
) )
# if headers.hasHeader("Content-Disposition"):
# disposition = headers.getRawHeaders("Content-Disposition")[0]
# TODO(markjh): parse content-dispostion
content_uri = yield self.create_content(
media_type, None, request.content.read(),
content_length, auth_user
)
respond_with_json(
request, 200, {"content_uri": content_uri}, send_cors=True
)

View File

@ -1,3 +1,17 @@
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json import json
import logging import logging

View File

@ -1,3 +1,17 @@
/* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
CREATE TABLE IF NOT EXISTS push_rules_enable ( CREATE TABLE IF NOT EXISTS push_rules_enable (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name VARCHAR(150) NOT NULL, user_name VARCHAR(150) NOT NULL,

View File

@ -32,3 +32,22 @@ def run_on_reactor():
iteration of the main loop iteration of the main loop
""" """
return sleep(0) return sleep(0)
def create_observer(deferred):
"""Creates a deferred that observes the result or failure of the given
deferred *without* affecting the given deferred.
"""
d = defer.Deferred()
def callback(r):
d.callback(r)
return r
def errback(f):
d.errback(f)
return f
deferred.addCallbacks(callback, errback)
return d

View File

@ -883,6 +883,71 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase):
state state
) )
@defer.inlineCallbacks
def test_recv_remote_offline(self):
""" Various tests relating to SYN-261 """
potato_set = self.handler._remote_recvmap.setdefault(self.u_potato,
set())
potato_set.add(self.u_apple)
self.room_members = [self.u_banana, self.u_potato]
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
{"user_id": "@potato:remote",
"presence": "offline"},
],
}
)
)
self.assertEquals(self.event_source.get_current_key(), 1)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": OFFLINE,
}}
]
)
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000001/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
{"user_id": "@potato:remote",
"presence": "online"},
],
}
)
)
self.assertEquals(self.event_source.get_current_key(), 2)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": ONLINE,
}}
]
)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_join_room_local(self): def test_join_room_local(self):
self.room_members = [self.u_apple, self.u_banana] self.room_members = [self.u_apple, self.u_banana]