From 0280176ccddb9a1f142ad14a9a8a6e97686b0a4d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 13:31:47 -0700 Subject: [PATCH 1/9] Added basic captcha, not hooked up --- webclient/index.html | 3 ++- webclient/login/register-controller.js | 14 ++++++++++++++ webclient/login/register.html | 6 ++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/webclient/index.html b/webclient/index.html index 91b6bf27be..fe62d95bb8 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -10,7 +10,8 @@ - + + diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index 5a14964248..1c1f4c42f3 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -142,6 +142,20 @@ angular.module('RegisterController', ['matrixService']) } ); }; + + var setupCaptcha = function() { + console.log("Setting up ReCaptcha") + Recaptcha.create("6Le31_kSAAAAAK-54VKccKamtr-MFA_3WS1d_fGV", + "regcaptcha", + { + theme: "red", + callback: Recaptcha.focus_response_field + }); + }; + $scope.init = function() { + setupCaptcha(); + }; + }]); diff --git a/webclient/login/register.html b/webclient/login/register.html index 06a6526b70..a27f9ad4e8 100644 --- a/webclient/login/register.html +++ b/webclient/login/register.html @@ -12,7 +12,6 @@

-
Specifying an email address lets other users find you on Matrix more easily,
and will give you a way to reset your password in the future
@@ -26,7 +25,10 @@

- + + +
+
From 130458385e919d886fcdfc4203354e93e9e8f1b1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 13:56:36 -0700 Subject: [PATCH 2/9] Modified matrixService.register to specify if captcha results should be sent with the registration request. This is toggleable via useCaptcha in register-controller. --- webclient/components/matrix/matrix-service.js | 26 ++++++++++++++++--- webclient/login/register-controller.js | 8 ++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 8a0223979c..4754dc87da 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -84,15 +84,33 @@ angular.module('matrixService', []) prefix: prefixPath, // Register an user - register: function(user_name, password, threepidCreds) { + register: function(user_name, password, threepidCreds, useCaptcha) { // The REST path spec var path = "/register"; - - return doRequest("POST", path, undefined, { + + var data = { user_id: user_name, password: password, threepidCreds: threepidCreds - }); + }; + + if (useCaptcha) { + // Not all home servers will require captcha on signup, but if this flag is checked, + // send captcha information. + // TODO: Might be nice to make this a bit more flexible.. + var challengeToken = Recaptcha.get_challenge(); + var captchaEntry = Recaptcha.get_response(); + var captchaType = "m.login.recaptcha"; + + data.captcha = { + type: captchaType, + challenge: challengeToken, + response: captchaEntry + }; + console.log("Sending Captcha info: " + JSON.stringify(data.captcha)); + } + + return doRequest("POST", path, undefined, data); }, // Create a room diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index 1c1f4c42f3..9d02f274df 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -19,6 +19,8 @@ angular.module('RegisterController', ['matrixService']) function($scope, $rootScope, $location, matrixService, eventStreamService) { 'use strict'; + var useCaptcha = false; + // FIXME: factor out duplication with login-controller.js // Assume that this is hosted on the home server, in which case the URL @@ -87,7 +89,7 @@ angular.module('RegisterController', ['matrixService']) }; $scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) { - matrixService.register(mxid, password, threepidCreds).then( + matrixService.register(mxid, password, threepidCreds, useCaptcha).then( function(response) { $scope.feedback = "Success"; // Update the current config @@ -154,7 +156,9 @@ angular.module('RegisterController', ['matrixService']) }; $scope.init = function() { - setupCaptcha(); + if (useCaptcha) { + setupCaptcha(); + } }; }]); From c80f7394617dcc44c3a608a3a51acde0f255f623 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 17:36:09 -0700 Subject: [PATCH 3/9] Added webclient config.js for storing recaptcha public key. --- .gitignore | 2 ++ webclient/README | 18 +++++++++++++++--- webclient/index.html | 1 + webclient/login/register-controller.js | 15 +++++++++++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d2b93ef61f..dfe8dfedbf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ graph/*.svg graph/*.png graph/*.dot +webclient/config.js + uploads diff --git a/webclient/README b/webclient/README index 0f893b1712..9750d2706a 100644 --- a/webclient/README +++ b/webclient/README @@ -1,12 +1,24 @@ Basic Usage ----------- -The Synapse web client needs to be hosted by a basic HTTP server. - -You can use the Python simple HTTP server:: +The web client should automatically run when running the home server. Alternatively, you can run +it stand-alone: $ python -m SimpleHTTPServer Then, open this URL in a WEB browser:: http://127.0.0.1:8000/ + + +ReCaptcha Keys +-------------- +The web client will look for the global variable webClientConfig for config options. You should +put your ReCaptcha public key there like so: + +webClientConfig = { + recaptcha_public_key: "YOUR_PUBLIC_KEY" +} + +This should be put in webclient/config.js which is already .gitignored, rather than in the web +client source files. diff --git a/webclient/index.html b/webclient/index.html index fe62d95bb8..0981373134 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -17,6 +17,7 @@ + diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index 9d02f274df..96fffb364d 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -19,7 +19,7 @@ angular.module('RegisterController', ['matrixService']) function($scope, $rootScope, $location, matrixService, eventStreamService) { 'use strict'; - var useCaptcha = false; + var useCaptcha = true; // FIXME: factor out duplication with login-controller.js @@ -147,7 +147,18 @@ angular.module('RegisterController', ['matrixService']) var setupCaptcha = function() { console.log("Setting up ReCaptcha") - Recaptcha.create("6Le31_kSAAAAAK-54VKccKamtr-MFA_3WS1d_fGV", + var config = window.webClientConfig; + var public_key = undefined; + if (config === undefined) { + console.error("Couldn't find webClientConfig. Cannot get public key for captcha."); + } + else { + public_key = webClientConfig.recaptcha_public_key; + if (public_key === undefined) { + console.error("No public key defined for captcha!") + } + } + Recaptcha.create(public_key, "regcaptcha", { theme: "red", From 0b9e1e7b562c3b278873060ca3c4109bc2e451e8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 17:58:06 -0700 Subject: [PATCH 4/9] Added a captcha config to the HS, to enable registration captcha checking and for the recaptcha private key. --- synapse/api/errors.py | 1 + synapse/config/captcha.py | 36 +++++++++++++++++++ synapse/config/homeserver.py | 3 +- synapse/rest/register.py | 6 +++- webclient/components/matrix/matrix-service.js | 1 - 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 synapse/config/captcha.py diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 84afe4fa37..8e9dd2aba6 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -29,6 +29,7 @@ class Codes(object): NOT_FOUND = "M_NOT_FOUND" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" + NEEDS_CAPTCHA = "M_NEEDS_CAPTCHA" class CodeMessageException(Exception): diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py new file mode 100644 index 0000000000..021da5c69b --- /dev/null +++ b/synapse/config/captcha.py @@ -0,0 +1,36 @@ +# 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 ._base import Config + +class CaptchaConfig(Config): + + def __init__(self, args): + super(CaptchaConfig, self).__init__(args) + self.recaptcha_private_key = args.recaptcha_private_key + self.enable_registration_captcha = args.enable_registration_captcha + + @classmethod + def add_arguments(cls, parser): + super(CaptchaConfig, cls).add_arguments(parser) + group = parser.add_argument_group("recaptcha") + group.add_argument( + "--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY", + help="The matching private key for the web client's public key." + ) + group.add_argument( + "--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." + ) \ No newline at end of file diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 76e2cdeddd..e16f2c733b 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -19,9 +19,10 @@ from .logger import LoggingConfig from .database import DatabaseConfig from .ratelimiting import RatelimitConfig from .repository import ContentRepositoryConfig +from .captcha import CaptchaConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, - RatelimitConfig, ContentRepositoryConfig): + RatelimitConfig, ContentRepositoryConfig, CaptchaConfig): pass if __name__=='__main__': 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( diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 4754dc87da..cc785269a1 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -107,7 +107,6 @@ angular.module('matrixService', []) challenge: challengeToken, response: captchaEntry }; - console.log("Sending Captcha info: " + JSON.stringify(data.captcha)); } return doRequest("POST", path, undefined, data); From 1829b55bb0d75d29475ac84eeb3e37cad8b334c7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 19:18:23 -0700 Subject: [PATCH 5/9] 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(-) 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, From 37e53513b6789b4f9f845a26b64933f1c533ed62 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 22:51:11 -0700 Subject: [PATCH 6/9] 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(-) 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, From 3ea6f01b4eff682c2770236cd7cee61a7bc61276 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 22:55:29 -0700 Subject: [PATCH 7/9] 80 chars please --- synapse/handlers/register.py | 28 +++++++++++++++++++--------- synapse/rest/register.py | 6 ++++-- 2 files changed, 23 insertions(+), 11 deletions(-) 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() From b5749c75d90247ff2f7960fad909b7b4fb694b67 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 23:08:39 -0700 Subject: [PATCH 8/9] Reload captchas when they fail. Cleanup on success. --- synapse/handlers/register.py | 4 ++-- webclient/login/register-controller.js | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 0693112ba8..0b841d6d3a 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -62,8 +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. Error: %s", + captcha_info["ip"], captcha_response["error_url"]) raise InvalidCaptchaError( error_url=captcha_response["error_url"] ) diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index 96fffb364d..1ab50888df 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -92,6 +92,9 @@ angular.module('RegisterController', ['matrixService']) matrixService.register(mxid, password, threepidCreds, useCaptcha).then( function(response) { $scope.feedback = "Success"; + if (useCaptcha) { + Recaptcha.destroy(); + } // Update the current config var config = matrixService.config(); angular.extend(config, { @@ -118,11 +121,17 @@ angular.module('RegisterController', ['matrixService']) }, function(error) { console.trace("Registration error: "+error); + if (useCaptcha) { + Recaptcha.reload(); + } if (error.data) { if (error.data.errcode === "M_USER_IN_USE") { $scope.feedback = "Username already taken."; $scope.reenter_username = true; } + else if (error.data.errcode == "M_CAPTCHA_INVALID") { + $scope.feedback = "Failed captcha."; + } } else if (error.status === 0) { $scope.feedback = "Unable to talk to the server."; From a342867d3f86096d53a59b0e09d6ac6121bfaa6f Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 5 Sep 2014 23:32:07 -0700 Subject: [PATCH 9/9] Added instructions for setting up captcha in an obviously named file. --- webclient/CAPTCHA_SETUP | 46 ++++++++++++++++++++++++++ webclient/README | 11 ------ webclient/login/register-controller.js | 8 +++++ 3 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 webclient/CAPTCHA_SETUP diff --git a/webclient/CAPTCHA_SETUP b/webclient/CAPTCHA_SETUP new file mode 100644 index 0000000000..ebc8a5f3b0 --- /dev/null +++ b/webclient/CAPTCHA_SETUP @@ -0,0 +1,46 @@ +Captcha can be enabled for this web client / home server. This file explains how to do that. +The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. + +Getting keys +------------ +Requires a public/private key pair from: + +https://developers.google.com/recaptcha/ + + +Setting Private ReCaptcha Key +----------------------------- +The private key is a config option on the home server config. If it is not +visible, you can generate it via --generate-config. Set the following value: + + recaptcha_private_key: YOUR_PRIVATE_KEY + +In addition, you MUST enable captchas via: + + enable_registration_captcha: true + +Setting Public ReCaptcha Key +---------------------------- +The web client will look for the global variable webClientConfig for config +options. You should put your ReCaptcha public key there like so: + +webClientConfig = { + useCaptcha: true, + recaptcha_public_key: "YOUR_PUBLIC_KEY" +} + +This should be put in webclient/config.js which is already .gitignored, rather +than in the web client source files. You MUST set useCaptcha to true else a +ReCaptcha widget will not be generated. + +Configuring IP used for auth +---------------------------- +The ReCaptcha API requires that the IP address of the user who solved the +captcha is sent. If the client is connecting through a proxy or load balancer, +it may be required to use the X-Forwarded-For (XFF) header instead of the origin +IP address. This can be configured as an option on the home server like so: + + captcha_ip_origin_is_x_forwarded: true + + + diff --git a/webclient/README b/webclient/README index 9750d2706a..13224c3d07 100644 --- a/webclient/README +++ b/webclient/README @@ -11,14 +11,3 @@ Then, open this URL in a WEB browser:: http://127.0.0.1:8000/ -ReCaptcha Keys --------------- -The web client will look for the global variable webClientConfig for config options. You should -put your ReCaptcha public key there like so: - -webClientConfig = { - recaptcha_public_key: "YOUR_PUBLIC_KEY" -} - -This should be put in webclient/config.js which is already .gitignored, rather than in the web -client source files. diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index 1ab50888df..b3c0c21335 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -19,7 +19,11 @@ angular.module('RegisterController', ['matrixService']) function($scope, $rootScope, $location, matrixService, eventStreamService) { 'use strict'; + var config = window.webClientConfig; var useCaptcha = true; + if (config !== undefined) { + useCaptcha = config.useCaptcha; + } // FIXME: factor out duplication with login-controller.js @@ -132,6 +136,10 @@ angular.module('RegisterController', ['matrixService']) else if (error.data.errcode == "M_CAPTCHA_INVALID") { $scope.feedback = "Failed captcha."; } + else if (error.data.errcode == "M_CAPTCHA_NEEDED") { + $scope.feedback = "Captcha is required on this home " + + "server."; + } } else if (error.status === 0) { $scope.feedback = "Unable to talk to the server.";