From 0b9e1e7b562c3b278873060ca3c4109bc2e451e8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 17:58:06 -0700 Subject: Added a captcha config to the HS, to enable registration captcha checking and for the recaptcha private key. --- synapse/rest/register.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/rest/register.py b/synapse/rest/register.py index b8de3b250d..33a80b7a77 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -16,7 +16,7 @@ """This module contains REST servlets to do with registration: /register""" from twisted.internet import defer -from synapse.api.errors import SynapseError +from synapse.api.errors import SynapseError, Codes from base import RestServlet, client_path_pattern import json @@ -50,6 +50,10 @@ class RegisterRestServlet(RestServlet): threepidCreds = None if 'threepidCreds' in register_json: threepidCreds = register_json['threepidCreds'] + + if self.hs.config.enable_registration_captcha: + if not "challenge" in register_json or not "response" in register_json: + raise SynapseError(400, "Captcha response is required", errcode=Codes.NEEDS_CAPTCHA) handler = self.handlers.registration_handler (user_id, token) = yield handler.register( -- cgit 1.4.1 From 1829b55bb0d75d29475ac84eeb3e37cad8b334c7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 19:18:23 -0700 Subject: Captchas now work on registration. Missing x-forwarded-for config arg support. Missing reloading a new captcha on the web client / displaying a sensible error message. --- synapse/api/errors.py | 16 ++++++++++++++- synapse/handlers/register.py | 49 ++++++++++++++++++++++++++++++++++++++++++-- synapse/http/client.py | 28 ++++++++++++++++++++++++- synapse/rest/register.py | 29 +++++++++++++++++++++++--- 4 files changed, 115 insertions(+), 7 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 8e9dd2aba6..88175602c4 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -29,7 +29,8 @@ class Codes(object): NOT_FOUND = "M_NOT_FOUND" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" - NEEDS_CAPTCHA = "M_NEEDS_CAPTCHA" + CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" + CAPTCHA_INVALID = "M_CAPTCHA_INVALID" class CodeMessageException(Exception): @@ -102,6 +103,19 @@ class StoreError(SynapseError): pass +class InvalidCaptchaError(SynapseError): + def __init__(self, code=400, msg="Invalid captcha.", error_url=None, + errcode=Codes.CAPTCHA_INVALID): + super(InvalidCaptchaError, self).__init__(code, msg, errcode) + self.error_url = error_url + + def error_dict(self): + return cs_error( + self.msg, + self.errcode, + error_url=self.error_url, + ) + class LimitExceededError(SynapseError): """A client has sent too many requests and is being throttled. """ diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index bee052274f..cf20b4efd3 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -17,7 +17,7 @@ from twisted.internet import defer from synapse.types import UserID -from synapse.api.errors import SynapseError, RegistrationError +from synapse.api.errors import SynapseError, RegistrationError, InvalidCaptchaError from ._base import BaseHandler import synapse.util.stringutils as stringutils from synapse.http.client import PlainHttpClient @@ -38,7 +38,7 @@ class RegistrationHandler(BaseHandler): self.distributor.declare("registered_user") @defer.inlineCallbacks - def register(self, localpart=None, password=None, threepidCreds=None): + def register(self, localpart=None, password=None, threepidCreds=None, captcha_info={}): """Registers a new client on the server. Args: @@ -51,6 +51,19 @@ class RegistrationHandler(BaseHandler): Raises: RegistrationError if there was a problem registering. """ + if captcha_info: + captcha_response = yield self._validate_captcha( + captcha_info["ip"], + captcha_info["private_key"], + captcha_info["challenge"], + captcha_info["response"] + ) + if not captcha_response["valid"]: + raise InvalidCaptchaError( + error_url=captcha_response["error_url"] + ) + else: + logger.info("Valid captcha entered from %s", captcha_info["ip"]) if threepidCreds: for c in threepidCreds: @@ -153,5 +166,37 @@ class RegistrationHandler(BaseHandler): ) defer.returnValue(data) + @defer.inlineCallbacks + def _validate_captcha(self, ip_addr, private_key, challenge, response): + """Validates the captcha provided. + + 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): + client = PlainHttpClient(self.hs) + data = yield client.post_urlencoded_get_raw( + "www.google.com:80", + "/recaptcha/api/verify", + accept_partial=True, # twisted dislikes google's response, no content length. + args={ + 'privatekey': private_key, + 'remoteip': ip_addr, + 'challenge': challenge, + 'response': response + } + ) + defer.returnValue(data) diff --git a/synapse/http/client.py b/synapse/http/client.py index ebf1aa47c4..ece6318e00 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -16,7 +16,7 @@ from twisted.internet import defer, reactor from twisted.internet.error import DNSLookupError -from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer +from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer, PartialDownloadError from twisted.web.http_headers import Headers from synapse.http.endpoint import matrix_endpoint @@ -188,6 +188,32 @@ class TwistedHttpClient(HttpClient): body = yield readBody(response) defer.returnValue(json.loads(body)) + + # XXX FIXME : I'm so sorry. + @defer.inlineCallbacks + def post_urlencoded_get_raw(self, destination, path, accept_partial=False, args={}): + if destination in _destination_mappings: + destination = _destination_mappings[destination] + + query_bytes = urllib.urlencode(args, True) + + response = yield self._create_request( + destination.encode("ascii"), + "POST", + path.encode("ascii"), + producer=FileBodyProducer(StringIO(urllib.urlencode(args))), + headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]} + ) + + try: + body = yield readBody(response) + defer.returnValue(body) + except PartialDownloadError as e: + if accept_partial: + defer.returnValue(e.response) + else: + raise e + @defer.inlineCallbacks def _create_request(self, destination, method, path_bytes, param_bytes=b"", diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 33a80b7a77..3c8929cf9b 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -51,15 +51,38 @@ class RegisterRestServlet(RestServlet): if 'threepidCreds' in register_json: threepidCreds = register_json['threepidCreds'] + captcha = {} if self.hs.config.enable_registration_captcha: - if not "challenge" in register_json or not "response" in register_json: - raise SynapseError(400, "Captcha response is required", errcode=Codes.NEEDS_CAPTCHA) + challenge = None + user_response = None + try: + captcha_type = register_json["captcha"]["type"] + if captcha_type != "m.login.recaptcha": + raise SynapseError(400, "Sorry, only m.login.recaptcha requests are supported.") + challenge = register_json["captcha"]["challenge"] + user_response = register_json["captcha"]["response"] + except KeyError: + raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) + + # TODO determine the source IP : 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 + + captcha = { + "ip": ip_addr, + "private_key": self.hs.config.recaptcha_private_key, + "challenge": challenge, + "response": user_response + } + handler = self.handlers.registration_handler (user_id, token) = yield handler.register( localpart=desired_user_id, password=password, - threepidCreds=threepidCreds) + threepidCreds=threepidCreds, + captcha_info=captcha) result = { "user_id": user_id, -- cgit 1.4.1 From 37e53513b6789b4f9f845a26b64933f1c533ed62 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 22:51:11 -0700 Subject: Add config opion for XFF headers when performing ReCaptcha auth. --- synapse/config/captcha.py | 6 ++++++ synapse/handlers/register.py | 1 + synapse/rest/register.py | 7 +++++-- 3 files changed, 12 insertions(+), 2 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index 021da5c69b..a97a5bab1e 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -20,6 +20,7 @@ class CaptchaConfig(Config): super(CaptchaConfig, self).__init__(args) self.recaptcha_private_key = args.recaptcha_private_key self.enable_registration_captcha = args.enable_registration_captcha + self.captcha_ip_origin_is_x_forwarded = args.captcha_ip_origin_is_x_forwarded @classmethod def add_arguments(cls, parser): @@ -33,4 +34,9 @@ class CaptchaConfig(Config): "--enable-registration-captcha", type=bool, default=False, help="Enables ReCaptcha checks when registering, preventing signup "+ "unless a captcha is answered. Requires a valid ReCaptcha public/private key." + ) + group.add_argument( + "--captcha_ip_origin_is_x_forwarded", type=bool, default=False, + help="When checking captchas, use the X-Forwarded-For (XFF) header as the client IP "+ + "and not the actual client IP." ) \ No newline at end of file diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index cf20b4efd3..6b55775de0 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -59,6 +59,7 @@ class RegistrationHandler(BaseHandler): captcha_info["response"] ) if not captcha_response["valid"]: + logger.info("Invalid captcha entered from %s", captcha_info["ip"]) raise InvalidCaptchaError( error_url=captcha_response["error_url"] ) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 3c8929cf9b..5872a11d80 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -66,8 +66,11 @@ class RegisterRestServlet(RestServlet): # TODO determine the source IP : 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 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] captcha = { "ip": ip_addr, -- cgit 1.4.1 From 3ea6f01b4eff682c2770236cd7cee61a7bc61276 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 22:55:29 -0700 Subject: 80 chars please --- synapse/handlers/register.py | 28 +++++++++++++++++++--------- synapse/rest/register.py | 6 ++++-- 2 files changed, 23 insertions(+), 11 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 6b55775de0..0693112ba8 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -17,7 +17,9 @@ from twisted.internet import defer from synapse.types import UserID -from synapse.api.errors import SynapseError, RegistrationError, InvalidCaptchaError +from synapse.api.errors import ( + SynapseError, RegistrationError, InvalidCaptchaError +) from ._base import BaseHandler import synapse.util.stringutils as stringutils from synapse.http.client import PlainHttpClient @@ -38,7 +40,8 @@ class RegistrationHandler(BaseHandler): self.distributor.declare("registered_user") @defer.inlineCallbacks - def register(self, localpart=None, password=None, threepidCreds=None, captcha_info={}): + def register(self, localpart=None, password=None, threepidCreds=None, + captcha_info={}): """Registers a new client on the server. Args: @@ -59,7 +62,8 @@ class RegistrationHandler(BaseHandler): captcha_info["response"] ) if not captcha_response["valid"]: - logger.info("Invalid captcha entered from %s", captcha_info["ip"]) + logger.info("Invalid captcha entered from %s", + captcha_info["ip"]) raise InvalidCaptchaError( error_url=captcha_response["error_url"] ) @@ -68,7 +72,8 @@ class RegistrationHandler(BaseHandler): if threepidCreds: for c in threepidCreds: - logger.info("validating theeepidcred sid %s on id server %s", c['sid'], c['idServer']) + logger.info("validating theeepidcred sid %s on id server %s", + c['sid'], c['idServer']) try: threepid = yield self._threepid_from_creds(c) except: @@ -77,7 +82,8 @@ class RegistrationHandler(BaseHandler): if not threepid: raise RegistrationError(400, "Couldn't validate 3pid") - logger.info("got threepid medium %s address %s", threepid['medium'], threepid['address']) + logger.info("got threepid medium %s address %s", + threepid['medium'], threepid['address']) password_hash = None if password: @@ -145,7 +151,8 @@ class RegistrationHandler(BaseHandler): # XXX: make this configurable! trustedIdServers = [ 'matrix.org:8090' ] if not creds['idServer'] in trustedIdServers: - logger.warn('%s is not a trusted ID server: rejecting 3pid credentials', creds['idServer']) + logger.warn('%s is not a trusted ID server: rejecting 3pid '+ + 'credentials', creds['idServer']) defer.returnValue(None) data = yield httpCli.get_json( creds['idServer'], @@ -163,7 +170,8 @@ class RegistrationHandler(BaseHandler): data = yield httpCli.post_urlencoded_get_json( creds['idServer'], "/_matrix/identity/api/v1/3pid/bind", - { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'], 'mxid':mxid } + { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'], + 'mxid':mxid } ) defer.returnValue(data) @@ -175,12 +183,14 @@ class RegistrationHandler(BaseHandler): dict: Containing 'valid'(bool) and 'error_url'(str) if invalid. """ - response = yield self._submit_captcha(ip_addr, private_key, challenge, response) + 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] + "error_url": "http://www.google.com/recaptcha/api/challenge?"+ + "error=%s" % lines[1] } defer.returnValue(json) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 5872a11d80..48d3c6eca0 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -58,11 +58,13 @@ class RegisterRestServlet(RestServlet): try: captcha_type = register_json["captcha"]["type"] if captcha_type != "m.login.recaptcha": - raise SynapseError(400, "Sorry, only m.login.recaptcha requests are supported.") + raise SynapseError(400, "Sorry, only m.login.recaptcha " + + "requests are supported.") challenge = register_json["captcha"]["challenge"] user_response = register_json["captcha"]["response"] except KeyError: - raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) + raise SynapseError(400, "Captcha response is required", + errcode=Codes.CAPTCHA_NEEDED) # TODO determine the source IP : May be an X-Forwarding-For header depending on config ip_addr = request.getClientIP() -- cgit 1.4.1 From 34878bc26a2ed4b796412830a4e1bf9edddc0089 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 15 Sep 2014 10:23:20 +0100 Subject: Added LoginType constants. Created general structure for processing registrations. --- synapse/api/constants.py | 9 +++++ synapse/rest/register.py | 95 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 20 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/api/constants.py b/synapse/api/constants.py index fcef062fc9..618d3d7577 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -50,3 +50,12 @@ class JoinRules(object): KNOCK = u"knock" INVITE = u"invite" PRIVATE = u"private" + + +class LoginType(object): + PASSWORD = u"m.login.password" + OAUTH = u"m.login.oauth2" + EMAIL_CODE = u"m.login.email.code" + EMAIL_URL = u"m.login.email.url" + EMAIL_IDENTITY = u"m.login.email.identity" + RECAPTCHA = u"m.login.recaptcha" \ No newline at end of file diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 48d3c6eca0..8faa26572a 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -17,6 +17,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError, Codes +from synapse.api.constants import LoginType from base import RestServlet, client_path_pattern import json @@ -26,31 +27,64 @@ import urllib class RegisterRestServlet(RestServlet): PATTERN = client_path_pattern("/register$") + def on_GET(self, request): + return (200, { + "flows": [ + { + "type": LoginType.RECAPTCHA, + "stages": ([LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY, + LoginType.PASSWORD]) + }, + { + "type": LoginType.RECAPTCHA, + "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] + }, + ] + }) + @defer.inlineCallbacks def on_POST(self, request): - desired_user_id = None - password = None + register_json = _parse_json(request) + + session = (register_json["session"] if "session" in register_json + else None) try: - register_json = json.loads(request.content.read()) - if "password" in register_json: - password = register_json["password"].encode("utf-8") - - if type(register_json["user_id"]) == unicode: - desired_user_id = register_json["user_id"].encode("utf-8") - if urllib.quote(desired_user_id) != desired_user_id: - raise SynapseError( - 400, - "User ID must only contain characters which do not " + - "require URL encoding.") - except ValueError: - defer.returnValue((400, "No JSON object.")) + login_type = register_json["type"] + stages = { + LoginType.RECAPTCHA: self._do_recaptcha, + LoginType.PASSWORD: self._do_password, + LoginType.EMAIL_IDENTITY: self._do_email_identity + } + + session_info = None + if session: + session_info = self._get_session_info(session) + + response = yield stages[login_type](register_json, session_info) + defer.returnValue((200, response)) except KeyError: - pass # user_id is optional + raise SynapseError(400, "Bad login type.") + + + desired_user_id = None + password = None + + if "password" in register_json: + password = register_json["password"].encode("utf-8") + + if ("user_id" in register_json and + type(register_json["user_id"]) == unicode): + desired_user_id = register_json["user_id"].encode("utf-8") + if urllib.quote(desired_user_id) != desired_user_id: + raise SynapseError( + 400, + "User ID must only contain characters which do not " + + "require URL encoding.") threepidCreds = None if 'threepidCreds' in register_json: threepidCreds = register_json['threepidCreds'] - + captcha = {} if self.hs.config.enable_registration_captcha: challenge = None @@ -65,7 +99,7 @@ class RegisterRestServlet(RestServlet): except KeyError: raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) - + # TODO determine the source IP : May be an X-Forwarding-For header depending on config ip_addr = request.getClientIP() if self.hs.config.captcha_ip_origin_is_x_forwarded: @@ -73,14 +107,14 @@ class RegisterRestServlet(RestServlet): if request.requestHeaders.hasHeader("X-Forwarded-For"): ip_addr = request.requestHeaders.getRawHeaders( "X-Forwarded-For")[0] - + captcha = { "ip": ip_addr, "private_key": self.hs.config.recaptcha_private_key, "challenge": challenge, "response": user_response } - + handler = self.handlers.registration_handler (user_id, token) = yield handler.register( @@ -101,6 +135,27 @@ class RegisterRestServlet(RestServlet): def on_OPTIONS(self, request): return (200, {}) + def _get_session_info(self, session_id): + pass + + def _do_recaptcha(self, register_json, session): + pass + + def _do_email_identity(self, register_json, session): + pass + + def _do_password(self, register_json, session): + pass + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") def register_servlets(hs, http_server): RegisterRestServlet(hs).register(http_server) -- cgit 1.4.1 From 285ecaacd0308f8088e38c64d49cd2e56b514d3d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 15 Sep 2014 12:42:36 +0100 Subject: Split out password/captcha/email logic. --- synapse/handlers/register.py | 120 ++++++++++++---------- synapse/rest/register.py | 237 ++++++++++++++++++++++++++++--------------- 2 files changed, 217 insertions(+), 140 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 0b841d6d3a..a019d770d4 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -40,8 +40,7 @@ class RegistrationHandler(BaseHandler): self.distributor.declare("registered_user") @defer.inlineCallbacks - def register(self, localpart=None, password=None, threepidCreds=None, - captcha_info={}): + def register(self, localpart=None, password=None): """Registers a new client on the server. Args: @@ -54,37 +53,6 @@ class RegistrationHandler(BaseHandler): Raises: RegistrationError if there was a problem registering. """ - if captcha_info: - captcha_response = yield self._validate_captcha( - captcha_info["ip"], - captcha_info["private_key"], - captcha_info["challenge"], - captcha_info["response"] - ) - if not captcha_response["valid"]: - logger.info("Invalid captcha entered from %s. Error: %s", - captcha_info["ip"], captcha_response["error_url"]) - raise InvalidCaptchaError( - error_url=captcha_response["error_url"] - ) - else: - logger.info("Valid captcha entered from %s", captcha_info["ip"]) - - if threepidCreds: - for c in threepidCreds: - logger.info("validating theeepidcred sid %s on id server %s", - c['sid'], c['idServer']) - try: - threepid = yield self._threepid_from_creds(c) - except: - logger.err() - raise RegistrationError(400, "Couldn't validate 3pid") - - if not threepid: - raise RegistrationError(400, "Couldn't validate 3pid") - logger.info("got threepid medium %s address %s", - threepid['medium'], threepid['address']) - password_hash = None if password: password_hash = bcrypt.hashpw(password, bcrypt.gensalt()) @@ -126,15 +94,54 @@ class RegistrationHandler(BaseHandler): raise RegistrationError( 500, "Cannot generate user ID.") - # Now we have a matrix ID, bind it to the threepids we were given - if threepidCreds: - for c in threepidCreds: - # XXX: This should be a deferred list, shouldn't it? - yield self._bind_threepid(c, user_id) - - defer.returnValue((user_id, token)) + @defer.inlineCallbacks + def check_recaptcha(self, ip, private_key, challenge, response): + """Checks a recaptcha is correct.""" + + 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_email(self, threepidCreds): + """Registers emails with an identity server.""" + + for c in threepidCreds: + logger.info("validating theeepidcred sid %s on id server %s", + c['sid'], c['idServer']) + try: + threepid = yield self._threepid_from_creds(c) + except: + logger.err() + raise RegistrationError(400, "Couldn't validate 3pid") + + if not threepid: + raise RegistrationError(400, "Couldn't validate 3pid") + logger.info("got threepid medium %s 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.""" + + # Now we have a matrix ID, bind it to the threepids we were given + for c in threepidCreds: + # XXX: This should be a deferred list, shouldn't it? + yield self._bind_threepid(c, user_id) + def _generate_token(self, user_id): # urlsafe variant uses _ and - so use . as the separator and replace # all =s with .s so http clients don't quote =s when it is used as @@ -149,17 +156,17 @@ class RegistrationHandler(BaseHandler): def _threepid_from_creds(self, creds): httpCli = PlainHttpClient(self.hs) # XXX: make this configurable! - trustedIdServers = [ 'matrix.org:8090' ] + trustedIdServers = ['matrix.org:8090'] if not creds['idServer'] in trustedIdServers: - logger.warn('%s is not a trusted ID server: rejecting 3pid '+ + logger.warn('%s is not a trusted ID server: rejecting 3pid ' + 'credentials', creds['idServer']) defer.returnValue(None) data = yield httpCli.get_json( creds['idServer'], "/_matrix/identity/api/v1/3pid/getValidated3pid", - { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'] } + {'sid': creds['sid'], 'clientSecret': creds['clientSecret']} ) - + if 'medium' in data: defer.returnValue(data) defer.returnValue(None) @@ -170,44 +177,45 @@ class RegistrationHandler(BaseHandler): data = yield httpCli.post_urlencoded_get_json( creds['idServer'], "/_matrix/identity/api/v1/3pid/bind", - { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'], - 'mxid':mxid } + {'sid': creds['sid'], 'clientSecret': creds['clientSecret'], + 'mxid': mxid} ) defer.returnValue(data) - + @defer.inlineCallbacks def _validate_captcha(self, ip_addr, private_key, challenge, response): """Validates the captcha provided. - + Returns: dict: Containing 'valid'(bool) and 'error_url'(str) if invalid. - + """ - response = yield self._submit_captcha(ip_addr, private_key, challenge, + 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_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): client = PlainHttpClient(self.hs) data = yield client.post_urlencoded_get_raw( "www.google.com:80", "/recaptcha/api/verify", - accept_partial=True, # twisted dislikes google's response, no content length. - args={ - 'privatekey': private_key, + # twisted dislikes google's response, no content length. + accept_partial=True, + args={ + 'privatekey': private_key, 'remoteip': ip_addr, 'challenge': challenge, 'response': response } ) defer.returnValue(data) - + diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 8faa26572a..8036c3c406 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -19,28 +19,62 @@ from twisted.internet import defer from synapse.api.errors import SynapseError, Codes from synapse.api.constants import LoginType from base import RestServlet, client_path_pattern +import synapse.util.stringutils as stringutils import json +import logging import urllib +logger = logging.getLogger(__name__) + class RegisterRestServlet(RestServlet): + """Handles registration with the home server. + + This servlet is in control of the registration flow; the registration + handler doesn't have a concept of multi-stages or sessions. + """ + PATTERN = client_path_pattern("/register$") + def __init__(self, hs): + super(RegisterRestServlet, self).__init__(hs) + # sessions are stored as: + # self.sessions = { + # "session_id" : { __session_dict__ } + # } + # TODO: persistent storage + self.sessions = {} + def on_GET(self, request): - return (200, { - "flows": [ - { - "type": LoginType.RECAPTCHA, - "stages": ([LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY, - LoginType.PASSWORD]) - }, - { - "type": LoginType.RECAPTCHA, - "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] - }, - ] - }) + if self.hs.config.enable_registration_captcha: + return (200, { + "flows": [ + { + "type": LoginType.RECAPTCHA, + "stages": ([LoginType.RECAPTCHA, + LoginType.EMAIL_IDENTITY, + LoginType.PASSWORD]) + }, + { + "type": LoginType.RECAPTCHA, + "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] + } + ] + }) + else: + return (200, { + "flows": [ + { + "type": LoginType.EMAIL_IDENTITY, + "stages": ([LoginType.EMAIL_IDENTITY, + LoginType.PASSWORD]) + }, + { + "type": LoginType.PASSWORD + } + ] + }) @defer.inlineCallbacks def on_POST(self, request): @@ -56,96 +90,130 @@ class RegisterRestServlet(RestServlet): LoginType.EMAIL_IDENTITY: self._do_email_identity } - session_info = None - if session: - session_info = self._get_session_info(session) + session_info = self._get_session_info(request, session) + logger.debug("%s : session info %s request info %s", + login_type, session_info, register_json) + response = yield stages[login_type]( + request, + register_json, + session_info + ) + + if "access_token" not in response: + # isn't a final response + response["session"] = session_info["id"] - response = yield stages[login_type](register_json, session_info) defer.returnValue((200, response)) - except KeyError: - raise SynapseError(400, "Bad login type.") + except KeyError as e: + logger.exception(e) + raise SynapseError(400, "Missing JSON keys or bad login type.") + def on_OPTIONS(self, request): + return (200, {}) - desired_user_id = None - password = None + def _get_session_info(self, request, session_id): + if not session_id: + # create a new session + while session_id is None or session_id in self.sessions: + session_id = stringutils.random_string(24) + self.sessions[session_id] = { + "id": session_id, + LoginType.EMAIL_IDENTITY: False, + LoginType.RECAPTCHA: False + } - if "password" in register_json: - password = register_json["password"].encode("utf-8") + return self.sessions[session_id] - if ("user_id" in register_json and - type(register_json["user_id"]) == unicode): - desired_user_id = register_json["user_id"].encode("utf-8") - if urllib.quote(desired_user_id) != desired_user_id: - raise SynapseError( - 400, - "User ID must only contain characters which do not " + - "require URL encoding.") + def _save_session(self, session): + # TODO: Persistent storage + logger.debug("Saving session %s", session) + self.sessions[session["id"]] = session - threepidCreds = None - if 'threepidCreds' in register_json: - threepidCreds = register_json['threepidCreds'] + def _remove_session(self, session): + logger.debug("Removing session %s", session) + self.sessions.pop(session["id"]) - captcha = {} - if self.hs.config.enable_registration_captcha: - challenge = None - user_response = None - try: - captcha_type = register_json["captcha"]["type"] - if captcha_type != "m.login.recaptcha": - raise SynapseError(400, "Sorry, only m.login.recaptcha " + - "requests are supported.") - challenge = register_json["captcha"]["challenge"] - user_response = register_json["captcha"]["response"] - except KeyError: - raise SynapseError(400, "Captcha response is required", - errcode=Codes.CAPTCHA_NEEDED) - - # TODO determine the source IP : 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] - - captcha = { - "ip": ip_addr, - "private_key": self.hs.config.recaptcha_private_key, - "challenge": challenge, - "response": user_response - } + def _do_recaptcha(self, request, register_json, session): + if not self.hs.config.enable_registration_captcha: + raise SynapseError(400, "Captcha not required.") + challenge = None + user_response = None + try: + challenge = register_json["challenge"] + user_response = register_json["response"] + except KeyError: + 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] handler = self.handlers.registration_handler + yield handler.check_recaptcha( + ip_addr, + self.hs.config.recaptcha_private_key, + challenge, + user_response + ) + session[LoginType.RECAPTCHA] = True # mark captcha as done + self._save_session(session) + defer.returnValue({ + "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] + }) + + @defer.inlineCallbacks + def _do_email_identity(self, request, register_json, session): + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + raise SynapseError(400, "Captcha is required.") + + threepidCreds = register_json['threepidCreds'] + handler = self.handlers.registration_handler + yield handler.register_email(threepidCreds) + session["threepidCreds"] = threepidCreds # store creds for next stage + session[LoginType.EMAIL_IDENTITY] = True # mark email as done + self._save_session(session) + defer.returnValue({ + "next": LoginType.PASSWORD + }) + + @defer.inlineCallbacks + def _do_password(self, request, register_json, session): + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + # captcha should've been done by this stage! + raise SynapseError(400, "Captcha is required.") + + password = register_json["password"].encode("utf-8") + desired_user_id = (register_json["user_id"].encode("utf-8") if "user_id" + in register_json else None) + if desired_user_id and urllib.quote(desired_user_id) != desired_user_id: + raise SynapseError( + 400, + "User ID must only contain characters which do not " + + "require URL encoding.") + handler = self.handlers.registration_handler (user_id, token) = yield handler.register( localpart=desired_user_id, - password=password, - threepidCreds=threepidCreds, - captcha_info=captcha) + password=password + ) + + if session[LoginType.EMAIL_IDENTITY]: + yield handler.bind_emails(user_id, session["threepidCreds"]) result = { "user_id": user_id, "access_token": token, "home_server": self.hs.hostname, } - defer.returnValue( - (200, result) - ) - - def on_OPTIONS(self, request): - return (200, {}) - - def _get_session_info(self, session_id): - pass - - def _do_recaptcha(self, register_json, session): - pass - - def _do_email_identity(self, register_json, session): - pass - - def _do_password(self, register_json, session): - pass + self._remove_session(session) + defer.returnValue(result) def _parse_json(request): @@ -157,5 +225,6 @@ def _parse_json(request): except ValueError: raise SynapseError(400, "Content not JSON.") + def register_servlets(hs, http_server): RegisterRestServlet(hs).register(http_server) -- cgit 1.4.1 From 04fbda46ddb722f64f9b0d702b41cc22569e9b12 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 15 Sep 2014 14:52:39 +0100 Subject: Make captcha work again with the new registration logic. --- synapse/rest/register.py | 1 + webclient/components/matrix/matrix-service.js | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 8036c3c406..fe8f0ed23f 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -133,6 +133,7 @@ class RegisterRestServlet(RestServlet): logger.debug("Removing session %s", session) self.sessions.pop(session["id"]) + @defer.inlineCallbacks def _do_recaptcha(self, request, register_json, session): if not self.hs.config.enable_registration_captcha: raise SynapseError(400, "Captcha not required.") diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index d7d278a7f6..35ebca961c 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -154,6 +154,13 @@ angular.module('matrixService', []) } if (!useCaptcha && regType == "m.login.recaptcha") { console.error("Web client setup to not use captcha, but HS demands a captcha."); + deferred.reject({ + data: { + errcode: "M_CAPTCHA_NEEDED", + error: "Home server requires a captcha." + } + }); + return; } } } @@ -183,7 +190,20 @@ angular.module('matrixService', []) deferred.resolve(response); } else if (response.data.next) { - return doRegisterLogin(path, response.data.next, sessionId, user_name, password, threepidCreds).then( + var nextType = response.data.next; + if (response.data.next instanceof Array) { + for (var i=0; i Date: Mon, 15 Sep 2014 15:38:29 +0100 Subject: Be consistent when associating keys with login types for registration/login. --- cmdclient/console.py | 2 +- synapse/rest/register.py | 2 +- tests/rest/utils.py | 2 +- webclient/components/matrix/matrix-service.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'synapse/rest/register.py') diff --git a/cmdclient/console.py b/cmdclient/console.py index 5a9d4c3c4c..d9c6ec6a70 100755 --- a/cmdclient/console.py +++ b/cmdclient/console.py @@ -162,7 +162,7 @@ class SynapseCmd(cmd.Cmd): "type": "m.login.password" } if "userid" in args: - body["user_id"] = args["userid"] + body["user"] = args["userid"] if password: body["password"] = password diff --git a/synapse/rest/register.py b/synapse/rest/register.py index fe8f0ed23f..c2c80e70c7 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -192,7 +192,7 @@ class RegisterRestServlet(RestServlet): raise SynapseError(400, "Captcha is required.") password = register_json["password"].encode("utf-8") - desired_user_id = (register_json["user_id"].encode("utf-8") if "user_id" + desired_user_id = (register_json["user"].encode("utf-8") if "user" in register_json else None) if desired_user_id and urllib.quote(desired_user_id) != desired_user_id: raise SynapseError( diff --git a/tests/rest/utils.py b/tests/rest/utils.py index 25ed1388cf..579441fb4a 100644 --- a/tests/rest/utils.py +++ b/tests/rest/utils.py @@ -99,7 +99,7 @@ class RestTestCase(unittest.TestCase): "POST", "/register", json.dumps({ - "user_id": user_id, + "user": user_id, "password": "test", "type": "m.login.password" })) diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 35ebca961c..069e02e939 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -100,7 +100,7 @@ angular.module('matrixService', []) } else if (loginType === "m.login.password") { data = { - user_id: userName, + user: userName, password: password }; } -- cgit 1.4.1 From 34d7896b06ba72c4a7ea28d5c42124a35df121bd Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 15 Sep 2014 16:05:51 +0100 Subject: More helpful 400 error messages. --- synapse/rest/register.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'synapse/rest/register.py') diff --git a/synapse/rest/register.py b/synapse/rest/register.py index c2c80e70c7..af528a44f6 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -82,6 +82,10 @@ class RegisterRestServlet(RestServlet): session = (register_json["session"] if "session" in register_json else None) + login_type = None + if "type" not in register_json: + raise SynapseError(400, "Missing 'type' key.") + try: login_type = register_json["type"] stages = { @@ -106,7 +110,7 @@ class RegisterRestServlet(RestServlet): defer.returnValue((200, response)) except KeyError as e: logger.exception(e) - raise SynapseError(400, "Missing JSON keys or bad login type.") + raise SynapseError(400, "Missing JSON keys for login type %s." % login_type) def on_OPTIONS(self, request): return (200, {}) -- cgit 1.4.1