mirror of https://github.com/watcha-fr/synapse
commit
0b0b24cb82
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@ |
|||||||
Enforce the specified API for report_event |
|
@ -1 +0,0 @@ |
|||||||
Include CPU time from database threads in request/block metrics. |
|
@ -1 +0,0 @@ |
|||||||
Add CPU metrics for _fetch_event_list |
|
@ -1 +0,0 @@ |
|||||||
Reduce database consumption when processing large numbers of receipts |
|
@ -0,0 +1 @@ |
|||||||
|
Correctly announce deleted devices over federation |
@ -1 +0,0 @@ |
|||||||
Cache optimisation for /sync requests |
|
@ -1 +0,0 @@ |
|||||||
Fix queued federation requests being processed in the wrong order |
|
@ -1 +0,0 @@ |
|||||||
refactor: use parse_{string,integer} and assert's from http.servlet for deduplication |
|
@ -1 +0,0 @@ |
|||||||
check isort for each PR |
|
@ -1 +0,0 @@ |
|||||||
Optimisation to make handling incoming federation requests more efficient. |
|
@ -1 +0,0 @@ |
|||||||
Ensure that erasure requests are correctly honoured for publicly accessible rooms when accessed over federation. |
|
@ -0,0 +1 @@ |
|||||||
|
Catch failures saving metrics captured by Measure, and instead log the faulty metrics information for further analysis. |
@ -0,0 +1 @@ |
|||||||
|
Release notes are now in the Markdown format. |
@ -0,0 +1 @@ |
|||||||
|
Add metrics to track resource usage by background processes |
@ -0,0 +1 @@ |
|||||||
|
Add `code` label to `synapse_http_server_response_time_seconds` prometheus metric |
@ -0,0 +1 @@ |
|||||||
|
Add metrics to track resource usage by background processes |
@ -0,0 +1 @@ |
|||||||
|
add config for pep8 |
@ -0,0 +1 @@ |
|||||||
|
Fix potential stack overflow and deadlock under heavy load |
@ -0,0 +1 @@ |
|||||||
|
Merge Linearizer and Limiter |
@ -0,0 +1 @@ |
|||||||
|
Merge Linearizer and Limiter |
@ -0,0 +1,63 @@ |
|||||||
|
Shared-Secret Registration |
||||||
|
========================== |
||||||
|
|
||||||
|
This API allows for the creation of users in an administrative and |
||||||
|
non-interactive way. This is generally used for bootstrapping a Synapse |
||||||
|
instance with administrator accounts. |
||||||
|
|
||||||
|
To authenticate yourself to the server, you will need both the shared secret |
||||||
|
(``registration_shared_secret`` in the homeserver configuration), and a |
||||||
|
one-time nonce. If the registration shared secret is not configured, this API |
||||||
|
is not enabled. |
||||||
|
|
||||||
|
To fetch the nonce, you need to request one from the API:: |
||||||
|
|
||||||
|
> GET /_matrix/client/r0/admin/register |
||||||
|
|
||||||
|
< {"nonce": "thisisanonce"} |
||||||
|
|
||||||
|
Once you have the nonce, you can make a ``POST`` to the same URL with a JSON |
||||||
|
body containing the nonce, username, password, whether they are an admin |
||||||
|
(optional, False by default), and a HMAC digest of the content. |
||||||
|
|
||||||
|
As an example:: |
||||||
|
|
||||||
|
> POST /_matrix/client/r0/admin/register |
||||||
|
> { |
||||||
|
"nonce": "thisisanonce", |
||||||
|
"username": "pepper_roni", |
||||||
|
"password": "pizza", |
||||||
|
"admin": true, |
||||||
|
"mac": "mac_digest_here" |
||||||
|
} |
||||||
|
|
||||||
|
< { |
||||||
|
"access_token": "token_here", |
||||||
|
"user_id": "@pepper_roni@test", |
||||||
|
"home_server": "test", |
||||||
|
"device_id": "device_id_here" |
||||||
|
} |
||||||
|
|
||||||
|
The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being |
||||||
|
the shared secret and the content being the nonce, user, password, and either |
||||||
|
the string "admin" or "notadmin", each separated by NULs. For an example of |
||||||
|
generation in Python:: |
||||||
|
|
||||||
|
import hmac, hashlib |
||||||
|
|
||||||
|
def generate_mac(nonce, user, password, admin=False): |
||||||
|
|
||||||
|
mac = hmac.new( |
||||||
|
key=shared_secret, |
||||||
|
digestmod=hashlib.sha1, |
||||||
|
) |
||||||
|
|
||||||
|
mac.update(nonce.encode('utf8')) |
||||||
|
mac.update(b"\x00") |
||||||
|
mac.update(user.encode('utf8')) |
||||||
|
mac.update(b"\x00") |
||||||
|
mac.update(password.encode('utf8')) |
||||||
|
mac.update(b"\x00") |
||||||
|
mac.update(b"admin" if admin else b"notadmin") |
||||||
|
|
||||||
|
return mac.hexdigest() |
@ -1,5 +1,30 @@ |
|||||||
[tool.towncrier] |
[tool.towncrier] |
||||||
package = "synapse" |
package = "synapse" |
||||||
filename = "CHANGES.rst" |
filename = "CHANGES.md" |
||||||
directory = "changelog.d" |
directory = "changelog.d" |
||||||
issue_format = "`#{issue} <https://github.com/matrix-org/synapse/issues/{issue}>`_" |
issue_format = "[\\#{issue}](https://github.com/matrix-org/synapse/issues/{issue}>)" |
||||||
|
|
||||||
|
[[tool.towncrier.type]] |
||||||
|
directory = "feature" |
||||||
|
name = "Features" |
||||||
|
showcontent = true |
||||||
|
|
||||||
|
[[tool.towncrier.type]] |
||||||
|
directory = "bugfix" |
||||||
|
name = "Bugfixes" |
||||||
|
showcontent = true |
||||||
|
|
||||||
|
[[tool.towncrier.type]] |
||||||
|
directory = "doc" |
||||||
|
name = "Improved Documentation" |
||||||
|
showcontent = true |
||||||
|
|
||||||
|
[[tool.towncrier.type]] |
||||||
|
directory = "removal" |
||||||
|
name = "Deprecations and Removals" |
||||||
|
showcontent = true |
||||||
|
|
||||||
|
[[tool.towncrier.type]] |
||||||
|
directory = "misc" |
||||||
|
name = "Internal Changes" |
||||||
|
showcontent = true |
||||||
|
@ -0,0 +1,179 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
# Copyright 2018 New Vector Ltd |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
import six |
||||||
|
|
||||||
|
from prometheus_client.core import REGISTRY, Counter, GaugeMetricFamily |
||||||
|
|
||||||
|
from twisted.internet import defer |
||||||
|
|
||||||
|
from synapse.util.logcontext import LoggingContext, PreserveLoggingContext |
||||||
|
|
||||||
|
_background_process_start_count = Counter( |
||||||
|
"synapse_background_process_start_count", |
||||||
|
"Number of background processes started", |
||||||
|
["name"], |
||||||
|
) |
||||||
|
|
||||||
|
# we set registry=None in all of these to stop them getting registered with |
||||||
|
# the default registry. Instead we collect them all via the CustomCollector, |
||||||
|
# which ensures that we can update them before they are collected. |
||||||
|
# |
||||||
|
_background_process_ru_utime = Counter( |
||||||
|
"synapse_background_process_ru_utime_seconds", |
||||||
|
"User CPU time used by background processes, in seconds", |
||||||
|
["name"], |
||||||
|
registry=None, |
||||||
|
) |
||||||
|
|
||||||
|
_background_process_ru_stime = Counter( |
||||||
|
"synapse_background_process_ru_stime_seconds", |
||||||
|
"System CPU time used by background processes, in seconds", |
||||||
|
["name"], |
||||||
|
registry=None, |
||||||
|
) |
||||||
|
|
||||||
|
_background_process_db_txn_count = Counter( |
||||||
|
"synapse_background_process_db_txn_count", |
||||||
|
"Number of database transactions done by background processes", |
||||||
|
["name"], |
||||||
|
registry=None, |
||||||
|
) |
||||||
|
|
||||||
|
_background_process_db_txn_duration = Counter( |
||||||
|
"synapse_background_process_db_txn_duration_seconds", |
||||||
|
("Seconds spent by background processes waiting for database " |
||||||
|
"transactions, excluding scheduling time"), |
||||||
|
["name"], |
||||||
|
registry=None, |
||||||
|
) |
||||||
|
|
||||||
|
_background_process_db_sched_duration = Counter( |
||||||
|
"synapse_background_process_db_sched_duration_seconds", |
||||||
|
"Seconds spent by background processes waiting for database connections", |
||||||
|
["name"], |
||||||
|
registry=None, |
||||||
|
) |
||||||
|
|
||||||
|
# map from description to a counter, so that we can name our logcontexts |
||||||
|
# incrementally. (It actually duplicates _background_process_start_count, but |
||||||
|
# it's much simpler to do so than to try to combine them.) |
||||||
|
_background_process_counts = dict() # type: dict[str, int] |
||||||
|
|
||||||
|
# map from description to the currently running background processes. |
||||||
|
# |
||||||
|
# it's kept as a dict of sets rather than a big set so that we can keep track |
||||||
|
# of process descriptions that no longer have any active processes. |
||||||
|
_background_processes = dict() # type: dict[str, set[_BackgroundProcess]] |
||||||
|
|
||||||
|
|
||||||
|
class _Collector(object): |
||||||
|
"""A custom metrics collector for the background process metrics. |
||||||
|
|
||||||
|
Ensures that all of the metrics are up-to-date with any in-flight processes |
||||||
|
before they are returned. |
||||||
|
""" |
||||||
|
def collect(self): |
||||||
|
background_process_in_flight_count = GaugeMetricFamily( |
||||||
|
"synapse_background_process_in_flight_count", |
||||||
|
"Number of background processes in flight", |
||||||
|
labels=["name"], |
||||||
|
) |
||||||
|
|
||||||
|
for desc, processes in six.iteritems(_background_processes): |
||||||
|
background_process_in_flight_count.add_metric( |
||||||
|
(desc,), len(processes), |
||||||
|
) |
||||||
|
for process in processes: |
||||||
|
process.update_metrics() |
||||||
|
|
||||||
|
yield background_process_in_flight_count |
||||||
|
|
||||||
|
# now we need to run collect() over each of the static Counters, and |
||||||
|
# yield each metric they return. |
||||||
|
for m in ( |
||||||
|
_background_process_ru_utime, |
||||||
|
_background_process_ru_stime, |
||||||
|
_background_process_db_txn_count, |
||||||
|
_background_process_db_txn_duration, |
||||||
|
_background_process_db_sched_duration, |
||||||
|
): |
||||||
|
for r in m.collect(): |
||||||
|
yield r |
||||||
|
|
||||||
|
|
||||||
|
REGISTRY.register(_Collector()) |
||||||
|
|
||||||
|
|
||||||
|
class _BackgroundProcess(object): |
||||||
|
def __init__(self, desc, ctx): |
||||||
|
self.desc = desc |
||||||
|
self._context = ctx |
||||||
|
self._reported_stats = None |
||||||
|
|
||||||
|
def update_metrics(self): |
||||||
|
"""Updates the metrics with values from this process.""" |
||||||
|
new_stats = self._context.get_resource_usage() |
||||||
|
if self._reported_stats is None: |
||||||
|
diff = new_stats |
||||||
|
else: |
||||||
|
diff = new_stats - self._reported_stats |
||||||
|
self._reported_stats = new_stats |
||||||
|
|
||||||
|
_background_process_ru_utime.labels(self.desc).inc(diff.ru_utime) |
||||||
|
_background_process_ru_stime.labels(self.desc).inc(diff.ru_stime) |
||||||
|
_background_process_db_txn_count.labels(self.desc).inc( |
||||||
|
diff.db_txn_count, |
||||||
|
) |
||||||
|
_background_process_db_txn_duration.labels(self.desc).inc( |
||||||
|
diff.db_txn_duration_sec, |
||||||
|
) |
||||||
|
_background_process_db_sched_duration.labels(self.desc).inc( |
||||||
|
diff.db_sched_duration_sec, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def run_as_background_process(desc, func, *args, **kwargs): |
||||||
|
"""Run the given function in its own logcontext, with resource metrics |
||||||
|
|
||||||
|
This should be used to wrap processes which are fired off to run in the |
||||||
|
background, instead of being associated with a particular request. |
||||||
|
|
||||||
|
Args: |
||||||
|
desc (str): a description for this background process type |
||||||
|
func: a function, which may return a Deferred |
||||||
|
args: positional args for func |
||||||
|
kwargs: keyword args for func |
||||||
|
|
||||||
|
Returns: None |
||||||
|
""" |
||||||
|
@defer.inlineCallbacks |
||||||
|
def run(): |
||||||
|
count = _background_process_counts.get(desc, 0) |
||||||
|
_background_process_counts[desc] = count + 1 |
||||||
|
_background_process_start_count.labels(desc).inc() |
||||||
|
|
||||||
|
with LoggingContext(desc) as context: |
||||||
|
context.request = "%s-%i" % (desc, count) |
||||||
|
proc = _BackgroundProcess(desc, context) |
||||||
|
_background_processes.setdefault(desc, set()).add(proc) |
||||||
|
try: |
||||||
|
yield func(*args, **kwargs) |
||||||
|
finally: |
||||||
|
proc.update_metrics() |
||||||
|
_background_processes[desc].remove(proc) |
||||||
|
|
||||||
|
with PreserveLoggingContext(): |
||||||
|
run() |
@ -0,0 +1,3 @@ |
|||||||
|
""" |
||||||
|
REST APIs that are only used in v1 (the legacy API). |
||||||
|
""" |
@ -0,0 +1,39 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
# Copyright 2014-2016 OpenMarket Ltd |
||||||
|
# Copyright 2018 New Vector Ltd |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
"""This module contains base REST classes for constructing client v1 servlets. |
||||||
|
""" |
||||||
|
|
||||||
|
import re |
||||||
|
|
||||||
|
from synapse.api.urls import CLIENT_PREFIX |
||||||
|
|
||||||
|
|
||||||
|
def v1_only_client_path_patterns(path_regex, include_in_unstable=True): |
||||||
|
"""Creates a regex compiled client path with the correct client path |
||||||
|
prefix. |
||||||
|
|
||||||
|
Args: |
||||||
|
path_regex (str): The regex string to match. This should NOT have a ^ |
||||||
|
as this will be prefixed. |
||||||
|
Returns: |
||||||
|
list of SRE_Pattern |
||||||
|
""" |
||||||
|
patterns = [re.compile("^" + CLIENT_PREFIX + path_regex)] |
||||||
|
if include_in_unstable: |
||||||
|
unstable_prefix = CLIENT_PREFIX.replace("/api/v1", "/unstable") |
||||||
|
patterns.append(re.compile("^" + unstable_prefix + path_regex)) |
||||||
|
return patterns |
@ -0,0 +1,42 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
# Copyright 2018 New Vector Ltd |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
""" |
||||||
|
Injectable secrets module for Synapse. |
||||||
|
|
||||||
|
See https://docs.python.org/3/library/secrets.html#module-secrets for the API |
||||||
|
used in Python 3.6, and the API emulated in Python 2.7. |
||||||
|
""" |
||||||
|
|
||||||
|
import six |
||||||
|
|
||||||
|
if six.PY3: |
||||||
|
import secrets |
||||||
|
|
||||||
|
def Secrets(): |
||||||
|
return secrets |
||||||
|
|
||||||
|
|
||||||
|
else: |
||||||
|
|
||||||
|
import os |
||||||
|
import binascii |
||||||
|
|
||||||
|
class Secrets(object): |
||||||
|
def token_bytes(self, nbytes=32): |
||||||
|
return os.urandom(nbytes) |
||||||
|
|
||||||
|
def token_hex(self, nbytes=32): |
||||||
|
return binascii.hexlify(self.token_bytes(nbytes)) |
@ -0,0 +1,305 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
# Copyright 2018 New Vector Ltd |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
import hashlib |
||||||
|
import hmac |
||||||
|
import json |
||||||
|
|
||||||
|
from mock import Mock |
||||||
|
|
||||||
|
from synapse.http.server import JsonResource |
||||||
|
from synapse.rest.client.v1.admin import register_servlets |
||||||
|
from synapse.util import Clock |
||||||
|
|
||||||
|
from tests import unittest |
||||||
|
from tests.server import ( |
||||||
|
ThreadedMemoryReactorClock, |
||||||
|
make_request, |
||||||
|
render, |
||||||
|
setup_test_homeserver, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterTestCase(unittest.TestCase): |
||||||
|
def setUp(self): |
||||||
|
|
||||||
|
self.clock = ThreadedMemoryReactorClock() |
||||||
|
self.hs_clock = Clock(self.clock) |
||||||
|
self.url = "/_matrix/client/r0/admin/register" |
||||||
|
|
||||||
|
self.registration_handler = Mock() |
||||||
|
self.identity_handler = Mock() |
||||||
|
self.login_handler = Mock() |
||||||
|
self.device_handler = Mock() |
||||||
|
self.device_handler.check_device_registered = Mock(return_value="FAKE") |
||||||
|
|
||||||
|
self.datastore = Mock(return_value=Mock()) |
||||||
|
self.datastore.get_current_state_deltas = Mock(return_value=[]) |
||||||
|
|
||||||
|
self.secrets = Mock() |
||||||
|
|
||||||
|
self.hs = setup_test_homeserver( |
||||||
|
http_client=None, clock=self.hs_clock, reactor=self.clock |
||||||
|
) |
||||||
|
|
||||||
|
self.hs.config.registration_shared_secret = u"shared" |
||||||
|
|
||||||
|
self.hs.get_media_repository = Mock() |
||||||
|
self.hs.get_deactivate_account_handler = Mock() |
||||||
|
|
||||||
|
self.resource = JsonResource(self.hs) |
||||||
|
register_servlets(self.hs, self.resource) |
||||||
|
|
||||||
|
def test_disabled(self): |
||||||
|
""" |
||||||
|
If there is no shared secret, registration through this method will be |
||||||
|
prevented. |
||||||
|
""" |
||||||
|
self.hs.config.registration_shared_secret = None |
||||||
|
|
||||||
|
request, channel = make_request("POST", self.url, b'{}') |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual( |
||||||
|
'Shared secret registration is not enabled', channel.json_body["error"] |
||||||
|
) |
||||||
|
|
||||||
|
def test_get_nonce(self): |
||||||
|
""" |
||||||
|
Calling GET on the endpoint will return a randomised nonce, using the |
||||||
|
homeserver's secrets provider. |
||||||
|
""" |
||||||
|
secrets = Mock() |
||||||
|
secrets.token_hex = Mock(return_value="abcd") |
||||||
|
|
||||||
|
self.hs.get_secrets = Mock(return_value=secrets) |
||||||
|
|
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(channel.json_body, {"nonce": "abcd"}) |
||||||
|
|
||||||
|
def test_expired_nonce(self): |
||||||
|
""" |
||||||
|
Calling GET on the endpoint will return a randomised nonce, which will |
||||||
|
only last for SALT_TIMEOUT (60s). |
||||||
|
""" |
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
nonce = channel.json_body["nonce"] |
||||||
|
|
||||||
|
# 59 seconds |
||||||
|
self.clock.advance(59) |
||||||
|
|
||||||
|
body = json.dumps({"nonce": nonce}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('username must be specified', channel.json_body["error"]) |
||||||
|
|
||||||
|
# 61 seconds |
||||||
|
self.clock.advance(2) |
||||||
|
|
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('unrecognised nonce', channel.json_body["error"]) |
||||||
|
|
||||||
|
def test_register_incorrect_nonce(self): |
||||||
|
""" |
||||||
|
Only the provided nonce can be used, as it's checked in the MAC. |
||||||
|
""" |
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
nonce = channel.json_body["nonce"] |
||||||
|
|
||||||
|
want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) |
||||||
|
want_mac.update(b"notthenonce\x00bob\x00abc123\x00admin") |
||||||
|
want_mac = want_mac.hexdigest() |
||||||
|
|
||||||
|
body = json.dumps( |
||||||
|
{ |
||||||
|
"nonce": nonce, |
||||||
|
"username": "bob", |
||||||
|
"password": "abc123", |
||||||
|
"admin": True, |
||||||
|
"mac": want_mac, |
||||||
|
} |
||||||
|
).encode('utf8') |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual("HMAC incorrect", channel.json_body["error"]) |
||||||
|
|
||||||
|
def test_register_correct_nonce(self): |
||||||
|
""" |
||||||
|
When the correct nonce is provided, and the right key is provided, the |
||||||
|
user is registered. |
||||||
|
""" |
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
nonce = channel.json_body["nonce"] |
||||||
|
|
||||||
|
want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) |
||||||
|
want_mac.update(nonce.encode('ascii') + b"\x00bob\x00abc123\x00admin") |
||||||
|
want_mac = want_mac.hexdigest() |
||||||
|
|
||||||
|
body = json.dumps( |
||||||
|
{ |
||||||
|
"nonce": nonce, |
||||||
|
"username": "bob", |
||||||
|
"password": "abc123", |
||||||
|
"admin": True, |
||||||
|
"mac": want_mac, |
||||||
|
} |
||||||
|
).encode('utf8') |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual("@bob:test", channel.json_body["user_id"]) |
||||||
|
|
||||||
|
def test_nonce_reuse(self): |
||||||
|
""" |
||||||
|
A valid unrecognised nonce. |
||||||
|
""" |
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
nonce = channel.json_body["nonce"] |
||||||
|
|
||||||
|
want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1) |
||||||
|
want_mac.update(nonce.encode('ascii') + b"\x00bob\x00abc123\x00admin") |
||||||
|
want_mac = want_mac.hexdigest() |
||||||
|
|
||||||
|
body = json.dumps( |
||||||
|
{ |
||||||
|
"nonce": nonce, |
||||||
|
"username": "bob", |
||||||
|
"password": "abc123", |
||||||
|
"admin": True, |
||||||
|
"mac": want_mac, |
||||||
|
} |
||||||
|
).encode('utf8') |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual("@bob:test", channel.json_body["user_id"]) |
||||||
|
|
||||||
|
# Now, try and reuse it |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('unrecognised nonce', channel.json_body["error"]) |
||||||
|
|
||||||
|
def test_missing_parts(self): |
||||||
|
""" |
||||||
|
Synapse will complain if you don't give nonce, username, password, and |
||||||
|
mac. Admin is optional. Additional checks are done for length and |
||||||
|
type. |
||||||
|
""" |
||||||
|
def nonce(): |
||||||
|
request, channel = make_request("GET", self.url) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
return channel.json_body["nonce"] |
||||||
|
|
||||||
|
# |
||||||
|
# Nonce check |
||||||
|
# |
||||||
|
|
||||||
|
# Must be present |
||||||
|
body = json.dumps({}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('nonce must be specified', channel.json_body["error"]) |
||||||
|
|
||||||
|
# |
||||||
|
# Username checks |
||||||
|
# |
||||||
|
|
||||||
|
# Must be present |
||||||
|
body = json.dumps({"nonce": nonce()}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('username must be specified', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Must be a string |
||||||
|
body = json.dumps({"nonce": nonce(), "username": 1234}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid username', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Must not have null bytes |
||||||
|
body = json.dumps({"nonce": nonce(), "username": b"abcd\x00"}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid username', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Must not have null bytes |
||||||
|
body = json.dumps({"nonce": nonce(), "username": "a" * 1000}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid username', channel.json_body["error"]) |
||||||
|
|
||||||
|
# |
||||||
|
# Username checks |
||||||
|
# |
||||||
|
|
||||||
|
# Must be present |
||||||
|
body = json.dumps({"nonce": nonce(), "username": "a"}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('password must be specified', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Must be a string |
||||||
|
body = json.dumps({"nonce": nonce(), "username": "a", "password": 1234}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid password', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Must not have null bytes |
||||||
|
body = json.dumps({"nonce": nonce(), "username": "a", "password": b"abcd\x00"}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid password', channel.json_body["error"]) |
||||||
|
|
||||||
|
# Super long |
||||||
|
body = json.dumps({"nonce": nonce(), "username": "a", "password": "A" * 1000}) |
||||||
|
request, channel = make_request("POST", self.url, body.encode('utf8')) |
||||||
|
render(request, self.resource, self.clock) |
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) |
||||||
|
self.assertEqual('Invalid password', channel.json_body["error"]) |
@ -1,70 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
# Copyright 2016 OpenMarket Ltd |
|
||||||
# |
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
# you may not use this file except in compliance with the License. |
|
||||||
# You may obtain a copy of the License at |
|
||||||
# |
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
# |
|
||||||
# Unless required by applicable law or agreed to in writing, software |
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
# See the License for the specific language governing permissions and |
|
||||||
# limitations under the License. |
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import defer |
|
||||||
|
|
||||||
from synapse.util.async import Limiter |
|
||||||
|
|
||||||
from tests import unittest |
|
||||||
|
|
||||||
|
|
||||||
class LimiterTestCase(unittest.TestCase): |
|
||||||
|
|
||||||
@defer.inlineCallbacks |
|
||||||
def test_limiter(self): |
|
||||||
limiter = Limiter(3) |
|
||||||
|
|
||||||
key = object() |
|
||||||
|
|
||||||
d1 = limiter.queue(key) |
|
||||||
cm1 = yield d1 |
|
||||||
|
|
||||||
d2 = limiter.queue(key) |
|
||||||
cm2 = yield d2 |
|
||||||
|
|
||||||
d3 = limiter.queue(key) |
|
||||||
cm3 = yield d3 |
|
||||||
|
|
||||||
d4 = limiter.queue(key) |
|
||||||
self.assertFalse(d4.called) |
|
||||||
|
|
||||||
d5 = limiter.queue(key) |
|
||||||
self.assertFalse(d5.called) |
|
||||||
|
|
||||||
with cm1: |
|
||||||
self.assertFalse(d4.called) |
|
||||||
self.assertFalse(d5.called) |
|
||||||
|
|
||||||
self.assertTrue(d4.called) |
|
||||||
self.assertFalse(d5.called) |
|
||||||
|
|
||||||
with cm3: |
|
||||||
self.assertFalse(d5.called) |
|
||||||
|
|
||||||
self.assertTrue(d5.called) |
|
||||||
|
|
||||||
with cm2: |
|
||||||
pass |
|
||||||
|
|
||||||
with (yield d4): |
|
||||||
pass |
|
||||||
|
|
||||||
with (yield d5): |
|
||||||
pass |
|
||||||
|
|
||||||
d6 = limiter.queue(key) |
|
||||||
with (yield d6): |
|
||||||
pass |
|
Loading…
Reference in new issue