summary refs log tree commit diff
path: root/synapse
diff options
context:
space:
mode:
Diffstat (limited to 'synapse')
-rw-r--r--synapse/api/auth.py42
-rwxr-xr-xsynapse/app/homeserver.py4
-rw-r--r--synapse/config/repository.py1
-rw-r--r--synapse/handlers/__init__.py2
-rw-r--r--synapse/handlers/admin.py62
-rw-r--r--synapse/handlers/directory.py49
-rw-r--r--synapse/handlers/login.py6
-rw-r--r--synapse/handlers/register.py11
-rw-r--r--synapse/http/client.py297
-rw-r--r--synapse/rest/__init__.py4
-rw-r--r--synapse/rest/admin.py47
-rw-r--r--synapse/rest/directory.py20
-rw-r--r--synapse/rest/register.py8
-rw-r--r--synapse/server.py12
-rw-r--r--synapse/storage/__init__.py26
-rw-r--r--synapse/storage/directory.py30
-rw-r--r--synapse/storage/registration.py35
-rw-r--r--synapse/storage/roommember.py1
-rw-r--r--synapse/storage/schema/delta/v5.sql16
-rw-r--r--synapse/storage/schema/users.sql14
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);
+