diff options
Diffstat (limited to 'synapse')
-rw-r--r-- | synapse/api/auth.py | 42 | ||||
-rwxr-xr-x | synapse/app/homeserver.py | 4 | ||||
-rw-r--r-- | synapse/config/repository.py | 1 | ||||
-rw-r--r-- | synapse/handlers/__init__.py | 2 | ||||
-rw-r--r-- | synapse/handlers/admin.py | 62 | ||||
-rw-r--r-- | synapse/handlers/directory.py | 49 | ||||
-rw-r--r-- | synapse/handlers/login.py | 6 | ||||
-rw-r--r-- | synapse/handlers/register.py | 11 | ||||
-rw-r--r-- | synapse/http/client.py | 297 | ||||
-rw-r--r-- | synapse/rest/__init__.py | 4 | ||||
-rw-r--r-- | synapse/rest/admin.py | 47 | ||||
-rw-r--r-- | synapse/rest/directory.py | 20 | ||||
-rw-r--r-- | synapse/rest/register.py | 8 | ||||
-rw-r--r-- | synapse/server.py | 12 | ||||
-rw-r--r-- | synapse/storage/__init__.py | 26 | ||||
-rw-r--r-- | synapse/storage/directory.py | 30 | ||||
-rw-r--r-- | synapse/storage/registration.py | 35 | ||||
-rw-r--r-- | synapse/storage/roommember.py | 1 | ||||
-rw-r--r-- | synapse/storage/schema/delta/v5.sql | 16 | ||||
-rw-r--r-- | synapse/storage/schema/users.sql | 14 |
20 files changed, 480 insertions, 207 deletions
diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 9bfd25c86e..e1b1823cd7 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -206,6 +206,7 @@ class Auth(object): defer.returnValue(True) + @defer.inlineCallbacks def get_user_by_req(self, request): """ Get a registered user's ID. @@ -218,7 +219,25 @@ class Auth(object): """ # Can optionally look elsewhere in the request (e.g. headers) try: - return self.get_user_by_token(request.args["access_token"][0]) + access_token = request.args["access_token"][0] + user_info = yield self.get_user_by_token(access_token) + user = user_info["user"] + + ip_addr = self.hs.get_ip_from_request(request) + user_agent = request.requestHeaders.getRawHeaders( + "User-Agent", + default=[""] + )[0] + if user and access_token and ip_addr: + self.store.insert_client_ip( + user=user, + access_token=access_token, + device_id=user_info["device_id"], + ip=ip_addr, + user_agent=user_agent + ) + + defer.returnValue(user) except KeyError: raise AuthError(403, "Missing access token.") @@ -227,21 +246,32 @@ class Auth(object): """ Get a registered user's ID. Args: - token (str)- The access token to get the user by. + token (str): The access token to get the user by. Returns: - UserID : User ID object of the user who has that access token. + dict : dict that includes the user, device_id, and whether the + user is a server admin. Raises: AuthError if no user by that token exists or the token is invalid. """ try: - user_id = yield self.store.get_user_by_token(token=token) - if not user_id: + ret = yield self.store.get_user_by_token(token=token) + if not ret: raise StoreError() - defer.returnValue(self.hs.parse_userid(user_id)) + + user_info = { + "admin": bool(ret.get("admin", False)), + "device_id": ret.get("device_id"), + "user": self.hs.parse_userid(ret.get("name")), + } + + defer.returnValue(user_info) except StoreError: raise AuthError(403, "Unrecognised access token.", errcode=Codes.UNKNOWN_TOKEN) + def is_server_admin(self, user): + return self.store.is_server_admin(user) + @defer.inlineCallbacks @log_function def _can_send_event(self, event): diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 2f1b954902..61d574a00f 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -25,7 +25,7 @@ from twisted.web.static import File from twisted.web.server import Site from synapse.http.server import JsonResource, RootRedirect from synapse.http.content_repository import ContentRepoResource -from synapse.http.client import TwistedHttpClient +from synapse.http.client import MatrixHttpClient from synapse.api.urls import ( CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX ) @@ -47,7 +47,7 @@ logger = logging.getLogger(__name__) class SynapseHomeServer(HomeServer): def build_http_client(self): - return TwistedHttpClient(self) + return MatrixHttpClient(self) def build_resource_for_client(self): return JsonResource() diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 407c8d6c24..b71d30227c 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -14,7 +14,6 @@ # limitations under the License. from ._base import Config -import os class ContentRepositoryConfig(Config): def __init__(self, args): diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 5308e2c8e1..d5df3c630b 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -25,6 +25,7 @@ from .profile import ProfileHandler from .presence import PresenceHandler from .directory import DirectoryHandler from .typing import TypingNotificationHandler +from .admin import AdminHandler class Handlers(object): @@ -49,3 +50,4 @@ class Handlers(object): self.login_handler = LoginHandler(hs) self.directory_handler = DirectoryHandler(hs) self.typing_notification_handler = TypingNotificationHandler(hs) + self.admin_handler = AdminHandler(hs) diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py new file mode 100644 index 0000000000..687b343a1d --- /dev/null +++ b/synapse/handlers/admin.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 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 ._base import BaseHandler + +import logging + + +logger = logging.getLogger(__name__) + + +class AdminHandler(BaseHandler): + + def __init__(self, hs): + super(AdminHandler, self).__init__(hs) + + @defer.inlineCallbacks + def get_whois(self, user): + res = yield self.store.get_user_ip_and_agents(user) + + d = {} + for r in res: + device = d.setdefault(r["device_id"], {}) + session = device.setdefault(r["access_token"], []) + session.append({ + "ip": r["ip"], + "user_agent": r["user_agent"], + "last_seen": r["last_seen"], + }) + + ret = { + "user_id": user.to_string(), + "devices": [ + { + "device_id": k, + "sessions": [ + { + # "access_token": x, TODO (erikj) + "connections": y, + } + for x, y in v.items() + ] + } + for k, v in d.items() + ], + } + + defer.returnValue(ret) diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 4ab00a761a..cec7737e09 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -18,7 +18,7 @@ from twisted.internet import defer from ._base import BaseHandler from synapse.api.errors import SynapseError -from synapse.http.client import HttpClient +from synapse.http.client import MatrixHttpClient from synapse.api.events.room import RoomAliasesEvent import logging @@ -57,7 +57,6 @@ class DirectoryHandler(BaseHandler): if not servers: raise SynapseError(400, "Failed to get server list") - try: yield self.store.create_room_alias_association( room_alias, @@ -68,25 +67,19 @@ class DirectoryHandler(BaseHandler): defer.returnValue("Already exists") # TODO: Send the room event. + yield self._update_room_alias_events(user_id, room_id) - aliases = yield self.store.get_aliases_for_room(room_id) - - event = self.event_factory.create_event( - etype=RoomAliasesEvent.TYPE, - state_key=self.hs.hostname, - room_id=room_id, - user_id=user_id, - content={"aliases": aliases}, - ) + @defer.inlineCallbacks + def delete_association(self, user_id, room_alias): + # TODO Check if server admin - snapshot = yield self.store.snapshot_room( - room_id=room_id, - user_id=user_id, - ) + if not room_alias.is_mine: + raise SynapseError(400, "Room alias must be local") - yield self.state_handler.handle_new_event(event, snapshot) - yield self._on_new_room_event(event, snapshot, extra_users=[user_id]) + room_id = yield self.store.delete_room_alias(room_alias) + if room_id: + yield self._update_room_alias_events(user_id, room_id) @defer.inlineCallbacks def get_association(self, room_alias): @@ -105,7 +98,7 @@ class DirectoryHandler(BaseHandler): query_type="directory", args={ "room_alias": room_alias.to_string(), - HttpClient.RETRY_DNS_LOOKUP_FAILURES: False + MatrixHttpClient.RETRY_DNS_LOOKUP_FAILURES: False } ) @@ -142,3 +135,23 @@ class DirectoryHandler(BaseHandler): "room_id": result.room_id, "servers": result.servers, }) + + @defer.inlineCallbacks + def _update_room_alias_events(self, user_id, room_id): + aliases = yield self.store.get_aliases_for_room(room_id) + + event = self.event_factory.create_event( + etype=RoomAliasesEvent.TYPE, + state_key=self.hs.hostname, + room_id=room_id, + user_id=user_id, + content={"aliases": aliases}, + ) + + snapshot = yield self.store.snapshot_room( + room_id=room_id, + user_id=user_id, + ) + + yield self.state_handler.handle_new_event(event, snapshot) + yield self._on_new_room_event(event, snapshot, extra_users=[user_id]) diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 80ffdd2726..3f152e18f0 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -17,7 +17,7 @@ from twisted.internet import defer from ._base import BaseHandler from synapse.api.errors import LoginError, Codes -from synapse.http.client import PlainHttpClient +from synapse.http.client import IdentityServerHttpClient from synapse.util.emailutils import EmailException import synapse.util.emailutils as emailutils @@ -97,10 +97,10 @@ class LoginHandler(BaseHandler): @defer.inlineCallbacks def _query_email(self, email): - httpCli = PlainHttpClient(self.hs) + httpCli = IdentityServerHttpClient(self.hs) data = yield httpCli.get_json( 'matrix.org:8090', # TODO FIXME This should be configurable. "/_matrix/identity/api/v1/lookup?medium=email&address=" + "%s" % urllib.quote(email) ) - defer.returnValue(data) \ No newline at end of file + defer.returnValue(data) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index a019d770d4..266495056e 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -22,7 +22,8 @@ from synapse.api.errors import ( ) from ._base import BaseHandler import synapse.util.stringutils as stringutils -from synapse.http.client import PlainHttpClient +from synapse.http.client import IdentityServerHttpClient +from synapse.http.client import CaptchaServerHttpClient import base64 import bcrypt @@ -154,7 +155,9 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def _threepid_from_creds(self, creds): - httpCli = PlainHttpClient(self.hs) + # TODO: get this from the homeserver rather than creating a new one for + # each request + httpCli = IdentityServerHttpClient(self.hs) # XXX: make this configurable! trustedIdServers = ['matrix.org:8090'] if not creds['idServer'] in trustedIdServers: @@ -203,7 +206,9 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def _submit_captcha(self, ip_addr, private_key, challenge, response): - client = PlainHttpClient(self.hs) + # TODO: get this from the homeserver rather than creating a new one for + # each request + client = CaptchaServerHttpClient(self.hs) data = yield client.post_urlencoded_get_raw( "www.google.com:80", "/recaptcha/api/verify", diff --git a/synapse/http/client.py b/synapse/http/client.py index eb11bfd4d5..e02cce5642 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -35,56 +35,6 @@ import urllib logger = logging.getLogger(__name__) -# FIXME: SURELY these should be killed?! -_destination_mappings = { - "red": "localhost:8080", - "blue": "localhost:8081", - "green": "localhost:8082", -} - - -class HttpClient(object): - """ Interface for talking json over http - """ - RETRY_DNS_LOOKUP_FAILURES = "__retry_dns" - - def put_json(self, destination, path, data): - """ Sends the specifed json data using PUT - - Args: - destination (str): The remote server to send the HTTP request - to. - path (str): The HTTP path. - data (dict): A dict containing the data that will be used as - the request body. This will be encoded as JSON. - - Returns: - Deferred: Succeeds when we get a 2xx HTTP response. The result - will be the decoded JSON body. On a 4xx or 5xx error response a - CodeMessageException is raised. - """ - pass - - def get_json(self, destination, path, args=None): - """ Get's some json from the given host homeserver and path - - Args: - destination (str): The remote server to send the HTTP request - to. - path (str): The HTTP path. - args (dict): A dictionary used to create query strings, defaults to - None. - **Note**: The value of each key is assumed to be an iterable - and *not* a string. - - Returns: - Deferred: Succeeds when we get *any* HTTP response. - - The result of the deferred is a tuple of `(code, response)`, - where `response` is a dict representing the decoded JSON body. - """ - pass - class MatrixHttpAgent(_AgentBase): @@ -109,23 +59,114 @@ class MatrixHttpAgent(_AgentBase): parsed_URI.originForm) -class TwistedHttpClient(HttpClient): - """ Wrapper around the twisted HTTP client api. +class BaseHttpClient(object): + """Base class for HTTP clients using twisted. + """ + + def __init__(self, hs): + self.agent = MatrixHttpAgent(reactor) + self.hs = hs + + @defer.inlineCallbacks + def _create_request(self, destination, method, path_bytes, param_bytes=b"", + query_bytes=b"", producer=None, headers_dict={}, + retry_on_dns_fail=True, on_send_callback=None): + """ Creates and sends a request to the given url + """ + headers_dict[b"User-Agent"] = [b"Synapse"] + headers_dict[b"Host"] = [destination] + + logger.debug("Sending request to %s: %s %s;%s?%s", + destination, method, path_bytes, param_bytes, query_bytes) + + logger.debug( + "Types: %s", + [ + type(destination), type(method), type(path_bytes), + type(param_bytes), + type(query_bytes) + ] + ) + + retries_left = 5 + + endpoint = self._getEndpoint(reactor, destination); + + while True: + if on_send_callback: + on_send_callback(destination, method, path_bytes, producer) + + try: + response = yield self.agent.request( + destination, + endpoint, + method, + path_bytes, + param_bytes, + query_bytes, + Headers(headers_dict), + producer + ) + + logger.debug("Got response to %s", method) + break + except Exception as e: + if not retry_on_dns_fail and isinstance(e, DNSLookupError): + logger.warn("DNS Lookup failed to %s with %s", destination, + e) + raise SynapseError(400, "Domain specified not found.") + + logger.exception("Got error in _create_request") + _print_ex(e) + + if retries_left: + yield sleep(2 ** (5 - retries_left)) + retries_left -= 1 + else: + raise + + if 200 <= response.code < 300: + # We need to update the transactions table to say it was sent? + pass + else: + # :'( + # Update transactions table? + logger.error( + "Got response %d %s", response.code, response.phrase + ) + raise CodeMessageException( + response.code, response.phrase + ) + + defer.returnValue(response) + + +class MatrixHttpClient(BaseHttpClient): + """ Wrapper around the twisted HTTP client api. Implements Attributes: agent (twisted.web.client.Agent): The twisted Agent used to send the requests. """ - def __init__(self, hs): - self.agent = MatrixHttpAgent(reactor) - self.hs = hs + RETRY_DNS_LOOKUP_FAILURES = "__retry_dns" @defer.inlineCallbacks def put_json(self, destination, path, data, on_send_callback=None): - if destination in _destination_mappings: - destination = _destination_mappings[destination] + """ Sends the specifed json data using PUT + + Args: + destination (str): The remote server to send the HTTP request + to. + path (str): The HTTP path. + data (dict): A dict containing the data that will be used as + the request body. This will be encoded as JSON. + Returns: + Deferred: Succeeds when we get a 2xx HTTP response. The result + will be the decoded JSON body. On a 4xx or 5xx error response a + CodeMessageException is raised. + """ response = yield self._create_request( destination.encode("ascii"), "PUT", @@ -143,9 +184,23 @@ class TwistedHttpClient(HttpClient): @defer.inlineCallbacks def get_json(self, destination, path, args={}): - if destination in _destination_mappings: - destination = _destination_mappings[destination] + """ Get's some json from the given host homeserver and path + Args: + destination (str): The remote server to send the HTTP request + to. + path (str): The HTTP path. + args (dict): A dictionary used to create query strings, defaults to + None. + **Note**: The value of each key is assumed to be an iterable + and *not* a string. + + Returns: + Deferred: Succeeds when we get *any* HTTP response. + + The result of the deferred is a tuple of `(code, response)`, + where `response` is a dict representing the decoded JSON body. + """ logger.debug("get_json args: %s", args) retry_on_dns_fail = True @@ -170,6 +225,22 @@ class TwistedHttpClient(HttpClient): defer.returnValue(json.loads(body)) + + def _getEndpoint(self, reactor, destination): + return matrix_endpoint( + reactor, destination, timeout=10, + ssl_context_factory=self.hs.tls_context_factory + ) + + +class IdentityServerHttpClient(BaseHttpClient): + """Separate HTTP client for talking to the Identity servers since they + don't use SRV records and talk x-www-form-urlencoded rather than JSON. + """ + def _getEndpoint(self, reactor, destination): + #TODO: This should be talking TLS + return matrix_endpoint(reactor, destination, timeout=10) + @defer.inlineCallbacks def post_urlencoded_get_json(self, destination, path, args={}): if destination in _destination_mappings: @@ -183,16 +254,25 @@ class TwistedHttpClient(HttpClient): "POST", path.encode("ascii"), producer=FileBodyProducer(StringIO(urllib.urlencode(args))), - headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]} + headers_dict={ + "Content-Type": ["application/x-www-form-urlencoded"] + } ) body = yield readBody(response) defer.returnValue(json.loads(body)) - - # XXX FIXME : I'm so sorry. + + +class CaptchaServerHttpClient(MatrixHttpClient): + """Separate HTTP client for talking to google's captcha servers""" + + def _getEndpoint(self, reactor, destination): + return matrix_endpoint(reactor, destination, timeout=10) + @defer.inlineCallbacks - def post_urlencoded_get_raw(self, destination, path, accept_partial=False, args={}): + def post_urlencoded_get_raw(self, destination, path, accept_partial=False, + args={}): if destination in _destination_mappings: destination = _destination_mappings[destination] @@ -203,7 +283,9 @@ class TwistedHttpClient(HttpClient): "POST", path.encode("ascii"), producer=FileBodyProducer(StringIO(urllib.urlencode(args))), - headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]} + headers_dict={ + "Content-Type": ["application/x-www-form-urlencoded"] + } ) try: @@ -214,93 +296,6 @@ class TwistedHttpClient(HttpClient): defer.returnValue(e.response) else: raise e - - - @defer.inlineCallbacks - def _create_request(self, destination, method, path_bytes, param_bytes=b"", - query_bytes=b"", producer=None, headers_dict={}, - retry_on_dns_fail=True, on_send_callback=None): - """ Creates and sends a request to the given url - """ - headers_dict[b"User-Agent"] = [b"Synapse"] - headers_dict[b"Host"] = [destination] - - logger.debug("Sending request to %s: %s %s;%s?%s", - destination, method, path_bytes, param_bytes, query_bytes) - - logger.debug( - "Types: %s", - [ - type(destination), type(method), type(path_bytes), - type(param_bytes), - type(query_bytes) - ] - ) - - retries_left = 5 - - # TODO: setup and pass in an ssl_context to enable TLS - endpoint = self._getEndpoint(reactor, destination); - - while True: - if on_send_callback: - on_send_callback(destination, method, path_bytes, producer) - - try: - response = yield self.agent.request( - destination, - endpoint, - method, - path_bytes, - param_bytes, - query_bytes, - Headers(headers_dict), - producer - ) - - logger.debug("Got response to %s", method) - break - except Exception as e: - if not retry_on_dns_fail and isinstance(e, DNSLookupError): - logger.warn("DNS Lookup failed to %s with %s", destination, - e) - raise SynapseError(400, "Domain specified not found.") - - logger.exception("Got error in _create_request") - _print_ex(e) - - if retries_left: - yield sleep(2 ** (5 - retries_left)) - retries_left -= 1 - else: - raise - - if 200 <= response.code < 300: - # We need to update the transactions table to say it was sent? - pass - else: - # :'( - # Update transactions table? - logger.error( - "Got response %d %s", response.code, response.phrase - ) - raise CodeMessageException( - response.code, response.phrase - ) - - defer.returnValue(response) - - def _getEndpoint(self, reactor, destination): - return matrix_endpoint( - reactor, destination, timeout=10, - ssl_context_factory=self.hs.tls_context_factory - ) - - -class PlainHttpClient(TwistedHttpClient): - def _getEndpoint(self, reactor, destination): - return matrix_endpoint(reactor, destination, timeout=10) - def _print_ex(e): if hasattr(e, "reasons") and e.reasons: diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 3b9aa59733..e391e5678d 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -15,7 +15,8 @@ from . import ( - room, events, register, login, profile, presence, initial_sync, directory, voip + room, events, register, login, profile, presence, initial_sync, directory, + voip, admin, ) @@ -43,3 +44,4 @@ class RestServletFactory(object): initial_sync.register_servlets(hs, client_resource) directory.register_servlets(hs, client_resource) voip.register_servlets(hs, client_resource) + admin.register_servlets(hs, client_resource) diff --git a/synapse/rest/admin.py b/synapse/rest/admin.py new file mode 100644 index 0000000000..ed9b484623 --- /dev/null +++ b/synapse/rest/admin.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 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.api.errors import AuthError, SynapseError +from base import RestServlet, client_path_pattern + +import logging + +logger = logging.getLogger(__name__) + + +class WhoisRestServlet(RestServlet): + PATTERN = client_path_pattern("/admin/whois/(?P<user_id>[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + target_user = self.hs.parse_userid(user_id) + auth_user = yield self.auth.get_user_by_req(request) + is_admin = yield self.auth.is_server_admin(auth_user) + + if not is_admin and target_user != auth_user: + raise AuthError(403, "You are not a server admin") + + if not target_user.is_mine: + raise SynapseError(400, "Can only whois a local user") + + ret = yield self.handlers.admin_handler.get_whois(target_user) + + defer.returnValue((200, ret)) + + +def register_servlets(hs, http_server): + WhoisRestServlet(hs).register(http_server) diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py index 31849246a1..6c260e7102 100644 --- a/synapse/rest/directory.py +++ b/synapse/rest/directory.py @@ -16,7 +16,7 @@ from twisted.internet import defer -from synapse.api.errors import SynapseError, Codes +from synapse.api.errors import AuthError, SynapseError, Codes from base import RestServlet, client_path_pattern import json @@ -81,6 +81,24 @@ class ClientDirectoryServer(RestServlet): defer.returnValue((200, {})) + @defer.inlineCallbacks + def on_DELETE(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + is_admin = yield self.auth.is_server_admin(user) + if not is_admin: + raise AuthError(403, "You need to be a server admin") + + dir_handler = self.handlers.directory_handler + + room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias)) + + yield dir_handler.delete_association( + user.to_string(), room_alias + ) + + defer.returnValue((200, {})) + def _parse_json(request): try: diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 4935e323d9..804117ee09 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -195,13 +195,7 @@ class RegisterRestServlet(RestServlet): raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) - # May be an X-Forwarding-For header depending on config - ip_addr = request.getClientIP() - if self.hs.config.captcha_ip_origin_is_x_forwarded: - # use the header - if request.requestHeaders.hasHeader("X-Forwarded-For"): - ip_addr = request.requestHeaders.getRawHeaders( - "X-Forwarded-For")[0] + ip_addr = self.hs.get_ip_from_request(request) handler = self.handlers.registration_handler yield handler.check_recaptcha( diff --git a/synapse/server.py b/synapse/server.py index cdea49e6ab..e5b048ede0 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -143,6 +143,18 @@ class BaseHomeServer(object): def serialize_event(self, e): return serialize_event(self, e) + def get_ip_from_request(self, request): + # May be an X-Forwarding-For header depending on config + ip_addr = request.getClientIP() + if self.config.captcha_ip_origin_is_x_forwarded: + # use the header + if request.requestHeaders.hasHeader("X-Forwarded-For"): + ip_addr = request.requestHeaders.getRawHeaders( + "X-Forwarded-For" + )[0] + + return ip_addr + # Build magic accessors for every dependency for depname in BaseHomeServer.DEPENDENCIES: BaseHomeServer._make_dependency_method(depname) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 15919eb580..32d9c1392b 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -63,7 +63,7 @@ SCHEMAS = [ # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 4 +SCHEMA_VERSION = 5 class _RollbackButIsFineException(Exception): @@ -105,7 +105,7 @@ class DataStore(RoomMemberStore, RoomStore, stream_ordering=stream_ordering, is_new_state=is_new_state, ) - except _RollbackButIsFineException as e: + except _RollbackButIsFineException: pass @defer.inlineCallbacks @@ -294,6 +294,28 @@ class DataStore(RoomMemberStore, RoomStore, defer.returnValue(self.min_token) + def insert_client_ip(self, user, access_token, device_id, ip, user_agent): + return self._simple_insert( + "user_ips", + { + "user": user.to_string(), + "access_token": access_token, + "device_id": device_id, + "ip": ip, + "user_agent": user_agent, + "last_seen": int(self._clock.time_msec()), + } + ) + + def get_user_ip_and_agents(self, user): + return self._simple_select_list( + table="user_ips", + keyvalues={"user": user.to_string()}, + retcols=[ + "device_id", "access_token", "ip", "user_agent", "last_seen" + ], + ) + def snapshot_room(self, room_id, user_id, state_type=None, state_key=None): """Snapshot the room for an update by a user Args: diff --git a/synapse/storage/directory.py b/synapse/storage/directory.py index 540eb4c2c4..52373a28a6 100644 --- a/synapse/storage/directory.py +++ b/synapse/storage/directory.py @@ -93,6 +93,36 @@ class DirectoryStore(SQLBaseStore): } ) + def delete_room_alias(self, room_alias): + return self.runInteraction( + self._delete_room_alias_txn, + room_alias, + ) + + def _delete_room_alias_txn(self, txn, room_alias): + cursor = txn.execute( + "SELECT room_id FROM room_aliases WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + res = cursor.fetchone() + if res: + room_id = res[0] + else: + return None + + txn.execute( + "DELETE FROM room_aliases WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + txn.execute( + "DELETE FROM room_alias_servers WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + return room_id + def get_aliases_for_room(self, room_id): return self._simple_select_onecol( "room_aliases", diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index db20b1daa0..719806f82b 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -88,27 +88,40 @@ class RegistrationStore(SQLBaseStore): query, user_id ) - @defer.inlineCallbacks def get_user_by_token(self, token): """Get a user from the given access token. Args: token (str): The access token of a user. Returns: - str: The user ID of the user. + dict: Including the name (user_id), device_id and whether they are + an admin. Raises: StoreError if no user was found. """ - user_id = yield self.runInteraction(self._query_for_auth, - token) - defer.returnValue(user_id) + return self.runInteraction( + self._query_for_auth, + token + ) + + def is_server_admin(self, user): + return self._simple_select_one_onecol( + table="users", + keyvalues={"name": user.to_string()}, + retcol="admin", + ) def _query_for_auth(self, txn, token): - txn.execute("SELECT users.name FROM access_tokens LEFT JOIN users" + - " ON users.id = access_tokens.user_id WHERE token = ?", - [token]) - row = txn.fetchone() - if row: - return row[0] + sql = ( + "SELECT users.name, users.admin, access_tokens.device_id " + "FROM users " + "INNER JOIN access_tokens on users.id = access_tokens.user_id " + "WHERE token = ?" + ) + + cursor = txn.execute(sql, (token,)) + rows = self.cursor_to_dict(cursor) + if rows: + return rows[0] raise StoreError(404, "Token not found.") diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 958e730591..ceeef5880e 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -18,7 +18,6 @@ from twisted.internet import defer from ._base import SQLBaseStore from synapse.api.constants import Membership -from synapse.util.logutils import log_function import logging diff --git a/synapse/storage/schema/delta/v5.sql b/synapse/storage/schema/delta/v5.sql new file mode 100644 index 0000000000..af9df11aa9 --- /dev/null +++ b/synapse/storage/schema/delta/v5.sql @@ -0,0 +1,16 @@ + +CREATE TABLE IF NOT EXISTS user_ips ( + user TEXT NOT NULL, + access_token TEXT NOT NULL, + device_id TEXT, + ip TEXT NOT NULL, + user_agent TEXT NOT NULL, + last_seen INTEGER NOT NULL, + CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE +); + +CREATE INDEX IF NOT EXISTS user_ips_user ON user_ips(user); + +ALTER TABLE users ADD COLUMN admin BOOL DEFAULT 0 NOT NULL; + +PRAGMA user_version = 5; diff --git a/synapse/storage/schema/users.sql b/synapse/storage/schema/users.sql index 2519702971..8244f733bd 100644 --- a/synapse/storage/schema/users.sql +++ b/synapse/storage/schema/users.sql @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS users( name TEXT, password_hash TEXT, creation_ts INTEGER, + admin BOOL DEFAULT 0 NOT NULL, UNIQUE(name) ON CONFLICT ROLLBACK ); @@ -29,3 +30,16 @@ CREATE TABLE IF NOT EXISTS access_tokens( FOREIGN KEY(user_id) REFERENCES users(id), UNIQUE(token) ON CONFLICT ROLLBACK ); + +CREATE TABLE IF NOT EXISTS user_ips ( + user TEXT NOT NULL, + access_token TEXT NOT NULL, + device_id TEXT, + ip TEXT NOT NULL, + user_agent TEXT NOT NULL, + last_seen INTEGER NOT NULL, + CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE +); + +CREATE INDEX IF NOT EXISTS user_ips_user ON user_ips(user); + |