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/api/events/__init__.py b/synapse/api/events/__init__.py
index 4fe0608016..0cee196851 100644
--- a/synapse/api/events/__init__.py
+++ b/synapse/api/events/__init__.py
@@ -25,6 +25,7 @@ def serialize_event(hs, e):
d = e.get_dict()
if "age_ts" in d:
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
+ del d["age_ts"]
return d
@@ -56,6 +57,7 @@ class SynapseEvent(JsonEncodedObject):
"state_key",
"required_power_level",
"age_ts",
+ "prev_content",
]
internal_keys = [
@@ -172,10 +174,6 @@ class SynapseEvent(JsonEncodedObject):
class SynapseStateEvent(SynapseEvent):
- valid_keys = SynapseEvent.valid_keys + [
- "prev_content",
- ]
-
def __init__(self, **kwargs):
if "state_key" not in kwargs:
kwargs["state_key"] = ""
diff --git a/synapse/config/email.py b/synapse/config/email.py
new file mode 100644
index 0000000000..9bcc5a8fea
--- /dev/null
+++ b/synapse/config/email.py
@@ -0,0 +1,39 @@
+# -*- 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 ._base import Config
+
+
+class EmailConfig(Config):
+
+ def __init__(self, args):
+ super(EmailConfig, self).__init__(args)
+ self.email_from_address = args.email_from_address
+ self.email_smtp_server = args.email_smtp_server
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(EmailConfig, cls).add_arguments(parser)
+ email_group = parser.add_argument_group("email")
+ email_group.add_argument(
+ "--email-from-address",
+ default="FROM@EXAMPLE.COM",
+ help="The address to send emails from (e.g. for password resets)."
+ )
+ email_group.add_argument(
+ "--email-smtp-server",
+ default="",
+ help="The SMTP server to send emails from (e.g. for password resets)."
+ )
\ No newline at end of file
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index e16f2c733b..4b810a2302 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -20,11 +20,15 @@ from .database import DatabaseConfig
from .ratelimiting import RatelimitConfig
from .repository import ContentRepositoryConfig
from .captcha import CaptchaConfig
+from .email import EmailConfig
+
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
- RatelimitConfig, ContentRepositoryConfig, CaptchaConfig):
+ RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
+ EmailConfig):
pass
-if __name__=='__main__':
+
+if __name__ == '__main__':
import sys
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index c79ce44688..96b82f00cb 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -293,7 +293,8 @@ class ReplicationLayer(object):
for p in transaction.pdus:
if "age" in p:
- p["age_ts"] = int(self.clock.time_msec()) - int(p["age"])
+ p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
+ del p["age"]
pdu_list = [Pdu(**p) for p in transaction.pdus]
@@ -602,8 +603,21 @@ class _TransactionQueue(object):
logger.debug("TX [%s] Sending transaction...", destination)
# Actually send the transaction
+
+ # FIXME (erikj): This is a bit of a hack to make the Pdu age
+ # keys work
+ def cb(transaction):
+ now = int(self._clock.time_msec())
+ if "pdus" in transaction:
+ for p in transaction["pdus"]:
+ if "age_ts" in p:
+ p["age"] = now - int(p["age_ts"])
+
+ return transaction
+
code, response = yield self.transport_layer.send_transaction(
- transaction
+ transaction,
+ on_send_callback=cb,
)
logger.debug("TX [%s] Sent transaction", destination)
diff --git a/synapse/federation/transport.py b/synapse/federation/transport.py
index 6e62ae7c74..afc777ec9e 100644
--- a/synapse/federation/transport.py
+++ b/synapse/federation/transport.py
@@ -144,7 +144,7 @@ class TransportLayer(object):
@defer.inlineCallbacks
@log_function
- def send_transaction(self, transaction):
+ def send_transaction(self, transaction, on_send_callback=None):
""" Sends the given Transaction to it's destination
Args:
@@ -165,10 +165,23 @@ class TransportLayer(object):
data = transaction.get_dict()
+ # FIXME (erikj): This is a bit of a hack to make the Pdu age
+ # keys work
+ def cb(destination, method, path_bytes, producer):
+ if not on_send_callback:
+ return
+
+ transaction = json.loads(producer.body)
+
+ new_transaction = on_send_callback(transaction)
+
+ producer.reset(new_transaction)
+
code, response = yield self.client.put_json(
transaction.destination,
path=PREFIX + "/send/%s/" % transaction.transaction_id,
- data=data
+ data=data,
+ on_send_callback=cb,
)
logger.debug(
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 59cbf71d78..001c6c110c 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -93,22 +93,18 @@ class FederationHandler(BaseHandler):
"""
event = self.pdu_codec.event_from_pdu(pdu)
+ logger.debug("Got event: %s", event.event_id)
+
with (yield self.lock_manager.lock(pdu.context)):
if event.is_state and not backfilled:
is_new_state = yield self.state_handler.handle_new_state(
pdu
)
- if not is_new_state:
- return
else:
is_new_state = False
# TODO: Implement something in federation that allows us to
# respond to PDU.
- if hasattr(event, "state_key") and not is_new_state:
- logger.debug("Ignoring old state.")
- return
-
target_is_mine = False
if hasattr(event, "target_host"):
target_is_mine = event.target_host == self.hs.hostname
@@ -139,7 +135,11 @@ class FederationHandler(BaseHandler):
else:
with (yield self.room_lock.lock(event.room_id)):
- yield self.store.persist_event(event, backfilled)
+ yield self.store.persist_event(
+ event,
+ backfilled,
+ is_new_state=is_new_state
+ )
room = yield self.store.get_room(event.room_id)
diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py
index 6ee7ce5a2d..80ffdd2726 100644
--- a/synapse/handlers/login.py
+++ b/synapse/handlers/login.py
@@ -17,9 +17,13 @@ from twisted.internet import defer
from ._base import BaseHandler
from synapse.api.errors import LoginError, Codes
+from synapse.http.client import PlainHttpClient
+from synapse.util.emailutils import EmailException
+import synapse.util.emailutils as emailutils
import bcrypt
import logging
+import urllib
logger = logging.getLogger(__name__)
@@ -62,4 +66,41 @@ class LoginHandler(BaseHandler):
defer.returnValue(token)
else:
logger.warn("Failed password login for user %s", user)
- raise LoginError(403, "", errcode=Codes.FORBIDDEN)
\ No newline at end of file
+ raise LoginError(403, "", errcode=Codes.FORBIDDEN)
+
+ @defer.inlineCallbacks
+ def reset_password(self, user_id, email):
+ is_valid = yield self._check_valid_association(user_id, email)
+ logger.info("reset_password user=%s email=%s valid=%s", user_id, email,
+ is_valid)
+ if is_valid:
+ try:
+ # send an email out
+ emailutils.send_email(
+ smtp_server=self.hs.config.email_smtp_server,
+ from_addr=self.hs.config.email_from_address,
+ to_addr=email,
+ subject="Password Reset",
+ body="TODO."
+ )
+ except EmailException as e:
+ logger.exception(e)
+
+ @defer.inlineCallbacks
+ def _check_valid_association(self, user_id, email):
+ identity = yield self._query_email(email)
+ if identity and "mxid" in identity:
+ if identity["mxid"] == user_id:
+ defer.returnValue(True)
+ return
+ defer.returnValue(False)
+
+ @defer.inlineCallbacks
+ def _query_email(self, email):
+ httpCli = PlainHttpClient(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
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/http/client.py b/synapse/http/client.py
index ece6318e00..eb11bfd4d5 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -122,7 +122,7 @@ class TwistedHttpClient(HttpClient):
self.hs = hs
@defer.inlineCallbacks
- def put_json(self, destination, path, data):
+ def put_json(self, destination, path, data, on_send_callback=None):
if destination in _destination_mappings:
destination = _destination_mappings[destination]
@@ -131,7 +131,8 @@ class TwistedHttpClient(HttpClient):
"PUT",
path.encode("ascii"),
producer=_JsonProducer(data),
- headers_dict={"Content-Type": ["application/json"]}
+ headers_dict={"Content-Type": ["application/json"]},
+ on_send_callback=on_send_callback,
)
logger.debug("Getting resp body")
@@ -218,7 +219,7 @@ class TwistedHttpClient(HttpClient):
@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):
+ 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"]
@@ -242,6 +243,9 @@ class TwistedHttpClient(HttpClient):
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,
@@ -310,6 +314,9 @@ class _JsonProducer(object):
""" Used by the twisted http client to create the HTTP body from json
"""
def __init__(self, jsn):
+ self.reset(jsn)
+
+ def reset(self, jsn):
self.body = encode_canonical_json(jsn)
self.length = len(self.body)
diff --git a/synapse/rest/login.py b/synapse/rest/login.py
index ba49afcaa7..ad71f6c61d 100644
--- a/synapse/rest/login.py
+++ b/synapse/rest/login.py
@@ -73,6 +73,27 @@ class LoginFallbackRestServlet(RestServlet):
return (200, {})
+class PasswordResetRestServlet(RestServlet):
+ PATTERN = client_path_pattern("/login/reset")
+
+ @defer.inlineCallbacks
+ def on_POST(self, request):
+ reset_info = _parse_json(request)
+ try:
+ email = reset_info["email"]
+ user_id = reset_info["user_id"]
+ handler = self.handlers.login_handler
+ yield handler.reset_password(user_id, email)
+ # purposefully give no feedback to avoid people hammering different
+ # combinations.
+ defer.returnValue((200, {}))
+ except KeyError:
+ raise SynapseError(
+ 400,
+ "Missing keys. Requires 'email' and 'user_id'."
+ )
+
+
def _parse_json(request):
try:
content = json.loads(request.content.read())
@@ -85,3 +106,4 @@ def _parse_json(request):
def register_servlets(hs, http_server):
LoginRestServlet(hs).register(http_server)
+ # TODO PasswordResetRestServlet(hs).register(http_server)
diff --git a/synapse/rest/register.py b/synapse/rest/register.py
index 48d3c6eca0..af528a44f6 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/register.py
@@ -17,89 +17,218 @@
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):
+ 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):
- desired_user_id = None
- password = None
+ register_json = _parse_json(request)
+
+ 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:
- 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."))
- except KeyError:
- pass # user_id is optional
+ login_type = register_json["type"]
+ stages = {
+ LoginType.RECAPTCHA: self._do_recaptcha,
+ LoginType.PASSWORD: self._do_password,
+ LoginType.EMAIL_IDENTITY: self._do_email_identity
+ }
- threepidCreds = None
- if 'threepidCreds' in register_json:
- threepidCreds = register_json['threepidCreds']
-
- 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
+ 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"]
+
+ defer.returnValue((200, response))
+ except KeyError as e:
+ logger.exception(e)
+ raise SynapseError(400, "Missing JSON keys for login type %s." % login_type)
+
+ def on_OPTIONS(self, request):
+ return (200, {})
+
+ 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
}
-
+ return self.sessions[session_id]
+
+ def _save_session(self, session):
+ # TODO: Persistent storage
+ logger.debug("Saving session %s", session)
+ self.sessions[session["id"]] = session
+
+ def _remove_session(self, session):
+ 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.")
+
+ 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"].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(
+ 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)
- )
+ self._remove_session(session)
+ defer.returnValue(result)
- def on_OPTIONS(self, request):
- return (200, {})
+
+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):
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 88c9a5b88c..66658f6721 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -85,7 +85,8 @@ class DataStore(RoomMemberStore, RoomStore,
@defer.inlineCallbacks
@log_function
- def persist_event(self, event=None, backfilled=False, pdu=None):
+ def persist_event(self, event=None, backfilled=False, pdu=None,
+ is_new_state=True):
stream_ordering = None
if backfilled:
if not self.min_token_deferred.called:
@@ -100,6 +101,7 @@ class DataStore(RoomMemberStore, RoomStore,
event=event,
backfilled=backfilled,
stream_ordering=stream_ordering,
+ is_new_state=is_new_state,
)
except _RollbackButIsFineException as e:
pass
@@ -126,12 +128,14 @@ class DataStore(RoomMemberStore, RoomStore,
defer.returnValue(event)
def _persist_pdu_event_txn(self, txn, pdu=None, event=None,
- backfilled=False, stream_ordering=None):
+ backfilled=False, stream_ordering=None,
+ is_new_state=True):
if pdu is not None:
self._persist_event_pdu_txn(txn, pdu)
if event is not None:
return self._persist_event_txn(
- txn, event, backfilled, stream_ordering
+ txn, event, backfilled, stream_ordering,
+ is_new_state=is_new_state,
)
def _persist_event_pdu_txn(self, txn, pdu):
@@ -158,7 +162,8 @@ class DataStore(RoomMemberStore, RoomStore,
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
@log_function
- def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None):
+ def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None,
+ is_new_state=True):
if event.type == RoomMemberEvent.TYPE:
self._store_room_member_txn(txn, event)
elif event.type == FeedbackEvent.TYPE:
@@ -212,7 +217,7 @@ class DataStore(RoomMemberStore, RoomStore,
)
raise _RollbackButIsFineException("_persist_event")
- if not backfilled and hasattr(event, "state_key"):
+ if is_new_state and hasattr(event, "state_key"):
vals = {
"event_id": event.event_id,
"room_id": event.room_id,
diff --git a/synapse/util/emailutils.py b/synapse/util/emailutils.py
new file mode 100644
index 0000000000..cdb0abd7ea
--- /dev/null
+++ b/synapse/util/emailutils.py
@@ -0,0 +1,71 @@
+# -*- 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.
+""" This module allows you to send out emails.
+"""
+import email.utils
+import smtplib
+import twisted.python.log
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class EmailException(Exception):
+ pass
+
+
+def send_email(smtp_server, from_addr, to_addr, subject, body):
+ """Sends an email.
+
+ Args:
+ smtp_server(str): The SMTP server to use.
+ from_addr(str): The address to send from.
+ to_addr(str): The address to send to.
+ subject(str): The subject of the email.
+ body(str): The plain text body of the email.
+ Raises:
+ EmailException if there was a problem sending the mail.
+ """
+ if not smtp_server or not from_addr or not to_addr:
+ raise EmailException("Need SMTP server, from and to addresses. Check " +
+ "the config to set these.")
+
+ msg = MIMEMultipart('alternative')
+ msg['Subject'] = subject
+ msg['From'] = from_addr
+ msg['To'] = to_addr
+ plain_part = MIMEText(body)
+ msg.attach(plain_part)
+
+ raw_from = email.utils.parseaddr(from_addr)[1]
+ raw_to = email.utils.parseaddr(to_addr)[1]
+ if not raw_from or not raw_to:
+ raise EmailException("Couldn't parse from/to address.")
+
+ logger.info("Sending email to %s on server %s with subject %s",
+ to_addr, smtp_server, subject)
+
+ try:
+ smtp = smtplib.SMTP(smtp_server)
+ smtp.sendmail(raw_from, raw_to, msg.as_string())
+ smtp.quit()
+ except Exception as origException:
+ twisted.python.log.err()
+ ese = EmailException()
+ ese.cause = origException
+ raise ese
\ No newline at end of file
|