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.
parent
0b9e1e7b56
commit
1829b55bb0
|
@ -29,7 +29,8 @@ class Codes(object):
|
||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||||
NEEDS_CAPTCHA = "M_NEEDS_CAPTCHA"
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(Exception):
|
class CodeMessageException(Exception):
|
||||||
|
@ -102,6 +103,19 @@ class StoreError(SynapseError):
|
||||||
pass
|
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):
|
class LimitExceededError(SynapseError):
|
||||||
"""A client has sent too many requests and is being throttled.
|
"""A client has sent too many requests and is being throttled.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.api.errors import SynapseError, RegistrationError
|
from synapse.api.errors import SynapseError, RegistrationError, InvalidCaptchaError
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
import synapse.util.stringutils as stringutils
|
import synapse.util.stringutils as stringutils
|
||||||
from synapse.http.client import PlainHttpClient
|
from synapse.http.client import PlainHttpClient
|
||||||
|
@ -38,7 +38,7 @@ class RegistrationHandler(BaseHandler):
|
||||||
self.distributor.declare("registered_user")
|
self.distributor.declare("registered_user")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@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.
|
"""Registers a new client on the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -51,6 +51,19 @@ class RegistrationHandler(BaseHandler):
|
||||||
Raises:
|
Raises:
|
||||||
RegistrationError if there was a problem registering.
|
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:
|
if threepidCreds:
|
||||||
for c in threepidCreds:
|
for c in threepidCreds:
|
||||||
|
@ -153,5 +166,37 @@ class RegistrationHandler(BaseHandler):
|
||||||
)
|
)
|
||||||
defer.returnValue(data)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.internet.error import DNSLookupError
|
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 twisted.web.http_headers import Headers
|
||||||
|
|
||||||
from synapse.http.endpoint import matrix_endpoint
|
from synapse.http.endpoint import matrix_endpoint
|
||||||
|
@ -188,6 +188,32 @@ class TwistedHttpClient(HttpClient):
|
||||||
body = yield readBody(response)
|
body = yield readBody(response)
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
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
|
@defer.inlineCallbacks
|
||||||
def _create_request(self, destination, method, path_bytes, param_bytes=b"",
|
def _create_request(self, destination, method, path_bytes, param_bytes=b"",
|
||||||
|
|
|
@ -51,15 +51,38 @@ class RegisterRestServlet(RestServlet):
|
||||||
if 'threepidCreds' in register_json:
|
if 'threepidCreds' in register_json:
|
||||||
threepidCreds = register_json['threepidCreds']
|
threepidCreds = register_json['threepidCreds']
|
||||||
|
|
||||||
|
captcha = {}
|
||||||
if self.hs.config.enable_registration_captcha:
|
if self.hs.config.enable_registration_captcha:
|
||||||
if not "challenge" in register_json or not "response" in register_json:
|
challenge = None
|
||||||
raise SynapseError(400, "Captcha response is required", errcode=Codes.NEEDS_CAPTCHA)
|
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
|
handler = self.handlers.registration_handler
|
||||||
(user_id, token) = yield handler.register(
|
(user_id, token) = yield handler.register(
|
||||||
localpart=desired_user_id,
|
localpart=desired_user_id,
|
||||||
password=password,
|
password=password,
|
||||||
threepidCreds=threepidCreds)
|
threepidCreds=threepidCreds,
|
||||||
|
captcha_info=captcha)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
|
|
Loading…
Reference in New Issue