mirror of
https://mau.dev/maunium/synapse.git
synced 2024-08-27 12:42:39 +00:00
![Daniel Wagner-Hall](/assets/img/avatar_default.png)
I prefer the auth handler to worry about all auth, and register to call into it as needed, than to smatter auth logic between the two.
320 lines
11 KiB
Python
320 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2014, 2015 OpenMarket Ltd
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Contains functions for registering clients."""
|
|
from twisted.internet import defer
|
|
|
|
from synapse.types import UserID
|
|
from synapse.api.errors import (
|
|
AuthError, Codes, SynapseError, RegistrationError, InvalidCaptchaError
|
|
)
|
|
from ._base import BaseHandler
|
|
import synapse.util.stringutils as stringutils
|
|
from synapse.util.async import run_on_reactor
|
|
from synapse.http.client import CaptchaServerHttpClient
|
|
|
|
import bcrypt
|
|
import logging
|
|
import urllib
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RegistrationHandler(BaseHandler):
|
|
|
|
def __init__(self, hs):
|
|
super(RegistrationHandler, self).__init__(hs)
|
|
|
|
self.distributor = hs.get_distributor()
|
|
self.distributor.declare("registered_user")
|
|
|
|
@defer.inlineCallbacks
|
|
def check_username(self, localpart):
|
|
yield run_on_reactor()
|
|
|
|
if urllib.quote(localpart) != localpart:
|
|
raise SynapseError(
|
|
400,
|
|
"User ID must only contain characters which do not"
|
|
" require URL encoding."
|
|
)
|
|
|
|
user = UserID(localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
|
|
yield self.check_user_id_is_valid(user_id)
|
|
|
|
u = yield self.store.get_user_by_id(user_id)
|
|
if u:
|
|
raise SynapseError(
|
|
400,
|
|
"User ID already taken.",
|
|
errcode=Codes.USER_IN_USE,
|
|
)
|
|
|
|
@defer.inlineCallbacks
|
|
def register(self, localpart=None, password=None):
|
|
"""Registers a new client on the server.
|
|
|
|
Args:
|
|
localpart : The local part of the user ID to register. If None,
|
|
one will be randomly generated.
|
|
password (str) : The password to assign to this user so they can
|
|
login again. This can be None which means they cannot login again
|
|
via a password (e.g. the user is an application service user).
|
|
Returns:
|
|
A tuple of (user_id, access_token).
|
|
Raises:
|
|
RegistrationError if there was a problem registering.
|
|
"""
|
|
yield run_on_reactor()
|
|
password_hash = None
|
|
if password:
|
|
password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
|
|
|
|
if localpart:
|
|
yield self.check_username(localpart)
|
|
|
|
user = UserID(localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
|
|
token = self.auth_handler().generate_access_token(user_id)
|
|
yield self.store.register(
|
|
user_id=user_id,
|
|
token=token,
|
|
password_hash=password_hash
|
|
)
|
|
|
|
yield self.distributor.fire("registered_user", user)
|
|
else:
|
|
# autogen a random user ID
|
|
attempts = 0
|
|
user_id = None
|
|
token = None
|
|
while not user_id and not token:
|
|
try:
|
|
localpart = self._generate_user_id()
|
|
user = UserID(localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
yield self.check_user_id_is_valid(user_id)
|
|
|
|
token = self.auth_handler().generate_access_token(user_id)
|
|
yield self.store.register(
|
|
user_id=user_id,
|
|
token=token,
|
|
password_hash=password_hash)
|
|
|
|
self.distributor.fire("registered_user", user)
|
|
except SynapseError:
|
|
# if user id is taken, just generate another
|
|
user_id = None
|
|
token = None
|
|
attempts += 1
|
|
if attempts > 5:
|
|
raise RegistrationError(
|
|
500, "Cannot generate user ID.")
|
|
|
|
# create a default avatar for the user
|
|
# XXX: ideally clients would explicitly specify one, but given they don't
|
|
# and we want consistent and pretty identicons for random users, we'll
|
|
# do it here.
|
|
try:
|
|
auth_user = UserID.from_string(user_id)
|
|
media_repository = self.hs.get_resource_for_media_repository()
|
|
identicon_resource = media_repository.getChildWithDefault("identicon", None)
|
|
upload_resource = media_repository.getChildWithDefault("upload", None)
|
|
identicon_bytes = identicon_resource.generate_identicon(user_id, 320, 320)
|
|
content_uri = yield upload_resource.create_content(
|
|
"image/png", None, identicon_bytes, len(identicon_bytes), auth_user
|
|
)
|
|
profile_handler = self.hs.get_handlers().profile_handler
|
|
profile_handler.set_avatar_url(
|
|
auth_user, auth_user, ("%s#auto" % (content_uri,))
|
|
)
|
|
except NotImplementedError:
|
|
pass # make tests pass without messing around creating default avatars
|
|
|
|
defer.returnValue((user_id, token))
|
|
|
|
@defer.inlineCallbacks
|
|
def appservice_register(self, user_localpart, as_token):
|
|
user = UserID(user_localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
service = yield self.store.get_app_service_by_token(as_token)
|
|
if not service:
|
|
raise AuthError(403, "Invalid application service token.")
|
|
if not service.is_interested_in_user(user_id):
|
|
raise SynapseError(
|
|
400, "Invalid user localpart for this application service.",
|
|
errcode=Codes.EXCLUSIVE
|
|
)
|
|
token = self.auth_handler().generate_access_token(user_id)
|
|
yield self.store.register(
|
|
user_id=user_id,
|
|
token=token,
|
|
password_hash=""
|
|
)
|
|
self.distributor.fire("registered_user", user)
|
|
defer.returnValue((user_id, token))
|
|
|
|
@defer.inlineCallbacks
|
|
def check_recaptcha(self, ip, private_key, challenge, response):
|
|
"""
|
|
Checks a recaptcha is correct.
|
|
|
|
Used only by c/s api v1
|
|
"""
|
|
|
|
captcha_response = yield self._validate_captcha(
|
|
ip,
|
|
private_key,
|
|
challenge,
|
|
response
|
|
)
|
|
if not captcha_response["valid"]:
|
|
logger.info("Invalid captcha entered from %s. Error: %s",
|
|
ip, captcha_response["error_url"])
|
|
raise InvalidCaptchaError(
|
|
error_url=captcha_response["error_url"]
|
|
)
|
|
else:
|
|
logger.info("Valid captcha entered from %s", ip)
|
|
|
|
@defer.inlineCallbacks
|
|
def register_saml2(self, localpart):
|
|
"""
|
|
Registers email_id as SAML2 Based Auth.
|
|
"""
|
|
if urllib.quote(localpart) != localpart:
|
|
raise SynapseError(
|
|
400,
|
|
"User ID must only contain characters which do not"
|
|
" require URL encoding."
|
|
)
|
|
user = UserID(localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
|
|
yield self.check_user_id_is_valid(user_id)
|
|
token = self.auth_handler().generate_access_token(user_id)
|
|
try:
|
|
yield self.store.register(
|
|
user_id=user_id,
|
|
token=token,
|
|
password_hash=None
|
|
)
|
|
yield self.distributor.fire("registered_user", user)
|
|
except Exception, e:
|
|
yield self.store.add_access_token_to_user(user_id, token)
|
|
# Ignore Registration errors
|
|
logger.exception(e)
|
|
defer.returnValue((user_id, token))
|
|
|
|
@defer.inlineCallbacks
|
|
def register_email(self, threepidCreds):
|
|
"""
|
|
Registers emails with an identity server.
|
|
|
|
Used only by c/s api v1
|
|
"""
|
|
|
|
for c in threepidCreds:
|
|
logger.info("validating theeepidcred sid %s on id server %s",
|
|
c['sid'], c['idServer'])
|
|
try:
|
|
identity_handler = self.hs.get_handlers().identity_handler
|
|
threepid = yield identity_handler.threepid_from_creds(c)
|
|
except:
|
|
logger.exception("Couldn't validate 3pid")
|
|
raise RegistrationError(400, "Couldn't validate 3pid")
|
|
|
|
if not threepid:
|
|
raise RegistrationError(400, "Couldn't validate 3pid")
|
|
logger.info("got threepid with medium '%s' and address '%s'",
|
|
threepid['medium'], threepid['address'])
|
|
|
|
@defer.inlineCallbacks
|
|
def bind_emails(self, user_id, threepidCreds):
|
|
"""Links emails with a user ID and informs an identity server.
|
|
|
|
Used only by c/s api v1
|
|
"""
|
|
|
|
# Now we have a matrix ID, bind it to the threepids we were given
|
|
for c in threepidCreds:
|
|
identity_handler = self.hs.get_handlers().identity_handler
|
|
# XXX: This should be a deferred list, shouldn't it?
|
|
yield identity_handler.bind_threepid(c, user_id)
|
|
|
|
@defer.inlineCallbacks
|
|
def check_user_id_is_valid(self, user_id):
|
|
# valid user IDs must not clash with any user ID namespaces claimed by
|
|
# application services.
|
|
services = yield self.store.get_app_services()
|
|
interested_services = [
|
|
s for s in services if s.is_interested_in_user(user_id)
|
|
]
|
|
for service in interested_services:
|
|
if service.is_exclusive_user(user_id):
|
|
raise SynapseError(
|
|
400, "This user ID is reserved by an application service.",
|
|
errcode=Codes.EXCLUSIVE
|
|
)
|
|
|
|
def _generate_user_id(self):
|
|
return "-" + stringutils.random_string(18)
|
|
|
|
@defer.inlineCallbacks
|
|
def _validate_captcha(self, ip_addr, private_key, challenge, response):
|
|
"""Validates the captcha provided.
|
|
|
|
Used only by c/s api v1
|
|
|
|
Returns:
|
|
dict: Containing 'valid'(bool) and 'error_url'(str) if invalid.
|
|
|
|
"""
|
|
response = yield self._submit_captcha(ip_addr, private_key, challenge,
|
|
response)
|
|
# parse Google's response. Lovely format..
|
|
lines = response.split('\n')
|
|
json = {
|
|
"valid": lines[0] == 'true',
|
|
"error_url": "http://www.google.com/recaptcha/api/challenge?" +
|
|
"error=%s" % lines[1]
|
|
}
|
|
defer.returnValue(json)
|
|
|
|
@defer.inlineCallbacks
|
|
def _submit_captcha(self, ip_addr, private_key, challenge, response):
|
|
"""
|
|
Used only by c/s api v1
|
|
"""
|
|
# 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(
|
|
"http://www.google.com:80/recaptcha/api/verify",
|
|
args={
|
|
'privatekey': private_key,
|
|
'remoteip': ip_addr,
|
|
'challenge': challenge,
|
|
'response': response
|
|
}
|
|
)
|
|
defer.returnValue(data)
|
|
|
|
def auth_handler(self):
|
|
return self.hs.get_handlers().auth_handler
|