Merge remote-tracking branch 'origin/develop' into webclient_data_centralisation

This commit is contained in:
Emmanuel ROHEE 2014-09-16 08:50:53 +02:00
commit faee41c303
28 changed files with 713 additions and 237 deletions

View file

@ -1,3 +1,14 @@
Latest
======
Registration API:
* The registration API has been overhauled to function like the login API. In
practice, this means registration requests must now include the following:
'type':'m.login.password'. See UPGRADE for more information on this.
* The 'user_id' key has been renamed to 'user' to better match the login API.
* There is an additional login type: 'm.login.email.identity'.
* The command client and web client have been updated to reflect these changes.
Changes in synapse 0.2.3 (2014-09-12) Changes in synapse 0.2.3 (2014-09-12)
===================================== =====================================

View file

@ -1,3 +1,26 @@
Upgrading to Latest
===================
This registration API now closely matches the login API. This introduces a bit
more backwards and forwards between the HS and the client, but this improves
the overall flexibility of the API. You can now GET on /register to retrieve a list
of valid registration flows. Upon choosing one, they are submitted in the same
way as login, e.g::
{
type: m.login.password,
user: foo,
password: bar
}
The default HS supports 2 flows, with and without Identity Server email
authentication. Enabling captcha on the HS will add in an extra step to all
flows: ``m.login.recaptcha`` which must be completed before you can transition
to the next stage. There is a new login type: ``m.login.email.identity`` which
contains the ``threepidCreds`` key which were previously sent in the original
register request. For more information on this, see the specification.
Upgrading to v0.2.0 Upgrading to v0.2.0
=================== ===================

View file

@ -145,35 +145,50 @@ class SynapseCmd(cmd.Cmd):
<noupdate> : Do not automatically clobber config values. <noupdate> : Do not automatically clobber config values.
""" """
args = self._parse(line, ["userid", "noupdate"]) args = self._parse(line, ["userid", "noupdate"])
path = "/register"
password = None password = None
pwd = None pwd = None
pwd2 = "_" pwd2 = "_"
while pwd != pwd2: while pwd != pwd2:
pwd = getpass.getpass("(Optional) Type a password for this user: ") pwd = getpass.getpass("Type a password for this user: ")
if len(pwd) == 0:
print "Not using a password for this user."
break
pwd2 = getpass.getpass("Retype the password: ") pwd2 = getpass.getpass("Retype the password: ")
if pwd != pwd2: if pwd != pwd2 or len(pwd) == 0:
print "Password mismatch." print "Password mismatch."
pwd = None
else: else:
password = pwd password = pwd
body = {} body = {
"type": "m.login.password"
}
if "userid" in args: if "userid" in args:
body["user_id"] = args["userid"] body["user"] = args["userid"]
if password: if password:
body["password"] = password body["password"] = password
reactor.callFromThread(self._do_register, "POST", path, body, reactor.callFromThread(self._do_register, body,
"noupdate" not in args) "noupdate" not in args)
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_register(self, method, path, data, update_config): def _do_register(self, data, update_config):
url = self._url() + path # check the registration flows
json_res = yield self.http_client.do_request(method, url, data=data) url = self._url() + "/register"
json_res = yield self.http_client.do_request("GET", url)
print json.dumps(json_res, indent=4)
passwordFlow = None
for flow in json_res["flows"]:
if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]):
print "Unable to register: Home server requires captcha."
return
if flow["type"] == "m.login.password" and "stages" not in flow:
passwordFlow = flow
break
if not passwordFlow:
return
json_res = yield self.http_client.do_request("POST", url, data=data)
print json.dumps(json_res, indent=4) print json.dumps(json_res, indent=4)
if update_config and "user_id" in json_res: if update_config and "user_id" in json_res:
self.config["user"] = json_res["user_id"] self.config["user"] = json_res["user_id"]

View file

@ -3,35 +3,38 @@
"apis": [ "apis": [
{ {
"operations": [ "operations": [
{
"method": "GET",
"nickname": "get_registration_info",
"notes": "All login stages MUST be mentioned if there is >1 login type.",
"summary": "Get the login mechanism to use when registering.",
"type": "RegistrationFlows"
},
{ {
"method": "POST", "method": "POST",
"nickname": "register", "nickname": "submit_registration",
"notes": "Volatile: This API is likely to change.", "notes": "If this is part of a multi-stage registration, there MUST be a 'session' key.",
"parameters": [ "parameters": [
{ {
"description": "A registration request", "description": "A registration submission",
"name": "body", "name": "body",
"paramType": "body", "paramType": "body",
"required": true, "required": true,
"type": "RegistrationRequest" "type": "RegistrationSubmission"
} }
], ],
"responseMessages": [ "responseMessages": [
{ {
"code": 400, "code": 400,
"message": "No JSON object." "message": "Bad login type"
}, },
{ {
"code": 400, "code": 400,
"message": "User ID must only contain characters which do not require url encoding." "message": "Missing JSON keys"
},
{
"code": 400,
"message": "User ID already taken."
} }
], ],
"summary": "Register with the home server.", "summary": "Submit a registration action.",
"type": "RegistrationResponse" "type": "RegistrationResult"
} }
], ],
"path": "/register" "path": "/register"
@ -42,30 +45,68 @@
"application/json" "application/json"
], ],
"models": { "models": {
"RegistrationResponse": { "RegistrationFlows": {
"id": "RegistrationResponse", "id": "RegistrationFlows",
"properties": { "properties": {
"access_token": { "flows": {
"description": "The access token for this user.", "description": "A list of valid registration flows.",
"type": "string" "type": "array",
"items": {
"$ref": "RegistrationInfo"
}
}
}
},
"RegistrationInfo": {
"id": "RegistrationInfo",
"properties": {
"stages": {
"description": "Multi-stage registration only: An array of all the login types required to registration.",
"items": {
"$ref": "string"
},
"type": "array"
}, },
"user_id": { "type": {
"description": "The fully-qualified user ID.", "description": "The first login type that must be used when logging in.",
"type": "string"
},
"home_server": {
"description": "The name of the home server.",
"type": "string" "type": "string"
} }
} }
}, },
"RegistrationRequest": { "RegistrationResult": {
"id": "RegistrationRequest", "id": "RegistrationResult",
"properties": { "properties": {
"access_token": {
"description": "The access token for this user's registration if this is the final stage of the registration process.",
"type": "string"
},
"user_id": { "user_id": {
"description": "The desired user ID. If not specified, a random user ID will be allocated.", "description": "The user's fully-qualified user ID.",
"type": "string", "type": "string"
"required": false },
"next": {
"description": "Multi-stage registration only: The next registration type to submit.",
"type": "string"
},
"session": {
"description": "Multi-stage registration only: The session token to send when submitting the next registration type.",
"type": "string"
}
}
},
"RegistrationSubmission": {
"id": "RegistrationSubmission",
"properties": {
"type": {
"description": "The type of registration being submitted.",
"type": "string"
},
"session": {
"description": "Multi-stage registration only: The session token from an earlier registration stage.",
"type": "string"
},
"_registration_type_defined_keys_": {
"description": "Keys as defined by the specified registration type, e.g. \"user\", \"password\""
} }
} }
} }

View file

@ -1305,12 +1305,6 @@ display name other than it being a valid unicode string.
Registration and login Registration and login
====================== ======================
.. WARNING::
The registration API is likely to change.
.. TODO
- TODO Kegan : Make registration like login (just omit the "user" key on the
initial request?)
Clients must register with a home server in order to use Matrix. After Clients must register with a home server in order to use Matrix. After
registering, the client will be given an access token which must be used in ALL registering, the client will be given an access token which must be used in ALL
@ -1323,9 +1317,11 @@ a token sent to their email address, etc. This specification does not define how
home servers should authorise their users who want to login to their existing home servers should authorise their users who want to login to their existing
accounts, but instead defines the standard interface which implementations accounts, but instead defines the standard interface which implementations
should follow so that ANY client can login to ANY home server. Clients login should follow so that ANY client can login to ANY home server. Clients login
using the |login|_ API. using the |login|_ API. Clients register using the |register|_ API. Registration
follows the same procedure as login, but the path requests are sent to are
different.
The login process breaks down into the following: The registration/login process breaks down into the following:
1. Determine the requirements for logging in. 1. Determine the requirements for logging in.
2. Submit the login stage credentials. 2. Submit the login stage credentials.
3. Get credentials or be told the next stage in the login process and repeat 3. Get credentials or be told the next stage in the login process and repeat
@ -1383,7 +1379,7 @@ This specification defines the following login types:
- ``m.login.oauth2`` - ``m.login.oauth2``
- ``m.login.email.code`` - ``m.login.email.code``
- ``m.login.email.url`` - ``m.login.email.url``
- ``m.login.email.identity``
Password-based Password-based
-------------- --------------
@ -1531,6 +1527,31 @@ If the link has not been visited yet, a standard error response with an errcode
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned. ``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
Email-based (identity server)
-----------------------------
:Type:
``m.login.email.identity``
:Description:
Login is supported by authorising an email address with an identity server.
Prior to submitting this, the client should authenticate with an identity server.
After authenticating, the session information should be submitted to the home server.
To respond to this type, reply with::
{
"type": "m.login.email.identity",
"threepidCreds": [
{
"sid": "<identity server session id>",
"clientSecret": "<identity server client secret>",
"idServer": "<url of identity server authed with, e.g. 'matrix.org:8090'>"
}
]
}
N-Factor Authentication N-Factor Authentication
----------------------- -----------------------
Multiple login stages can be combined to create N-factor authentication during login. Multiple login stages can be combined to create N-factor authentication during login.
@ -2242,6 +2263,9 @@ Transaction:
.. |login| replace:: ``/login`` .. |login| replace:: ``/login``
.. _login: /docs/api/client-server/#!/-login .. _login: /docs/api/client-server/#!/-login
.. |register| replace:: ``/register``
.. _register: /docs/api/client-server/#!/-registration
.. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages`` .. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages``
.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages .. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages

View file

@ -50,3 +50,12 @@ class JoinRules(object):
KNOCK = u"knock" KNOCK = u"knock"
INVITE = u"invite" INVITE = u"invite"
PRIVATE = u"private" 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"

View file

@ -17,6 +17,19 @@ from synapse.api.errors import SynapseError, Codes
from synapse.util.jsonobject import JsonEncodedObject from synapse.util.jsonobject import JsonEncodedObject
def serialize_event(hs, e):
# FIXME(erikj): To handle the case of presence events and the like
if not isinstance(e, SynapseEvent):
return 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
class SynapseEvent(JsonEncodedObject): class SynapseEvent(JsonEncodedObject):
"""Base class for Synapse events. These are JSON objects which must abide """Base class for Synapse events. These are JSON objects which must abide
@ -43,6 +56,8 @@ class SynapseEvent(JsonEncodedObject):
"content", # HTTP body, JSON "content", # HTTP body, JSON
"state_key", "state_key",
"required_power_level", "required_power_level",
"age_ts",
"prev_content",
] ]
internal_keys = [ internal_keys = [
@ -158,10 +173,6 @@ class SynapseEvent(JsonEncodedObject):
class SynapseStateEvent(SynapseEvent): class SynapseStateEvent(SynapseEvent):
valid_keys = SynapseEvent.valid_keys + [
"prev_content",
]
def __init__(self, **kwargs): def __init__(self, **kwargs):
if "state_key" not in kwargs: if "state_key" not in kwargs:
kwargs["state_key"] = "" kwargs["state_key"] = ""

View file

@ -59,6 +59,14 @@ class EventFactory(object):
if "ts" not in kwargs: if "ts" not in kwargs:
kwargs["ts"] = int(self.clock.time_msec()) kwargs["ts"] = int(self.clock.time_msec())
# The "age" key is a delta timestamp that should be converted into an
# absolute timestamp the minute we see it.
if "age" in kwargs:
kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"])
del kwargs["age"]
elif "age_ts" not in kwargs:
kwargs["age_ts"] = int(self.clock.time_msec())
if etype in self._event_list: if etype in self._event_list:
handler = self._event_list[etype] handler = self._event_list[etype]
else: else:

View file

@ -291,6 +291,13 @@ class ReplicationLayer(object):
def on_incoming_transaction(self, transaction_data): def on_incoming_transaction(self, transaction_data):
transaction = Transaction(**transaction_data) transaction = Transaction(**transaction_data)
for p in transaction.pdus:
if "age" in p:
p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
del p["age"]
pdu_list = [Pdu(**p) for p in transaction.pdus]
logger.debug("[%s] Got transaction", transaction.transaction_id) logger.debug("[%s] Got transaction", transaction.transaction_id)
response = yield self.transaction_actions.have_responded(transaction) response = yield self.transaction_actions.have_responded(transaction)
@ -303,8 +310,6 @@ class ReplicationLayer(object):
logger.debug("[%s] Transacition is new", transaction.transaction_id) logger.debug("[%s] Transacition is new", transaction.transaction_id)
pdu_list = [Pdu(**p) for p in transaction.pdus]
dl = [] dl = []
for pdu in pdu_list: for pdu in pdu_list:
dl.append(self._handle_new_pdu(pdu)) dl.append(self._handle_new_pdu(pdu))
@ -405,9 +410,14 @@ class ReplicationLayer(object):
"""Returns a new Transaction containing the given PDUs suitable for """Returns a new Transaction containing the given PDUs suitable for
transmission. transmission.
""" """
pdus = [p.get_dict() for p in pdu_list]
for p in pdus:
if "age_ts" in pdus:
p["age"] = int(self.clock.time_msec()) - p["age_ts"]
return Transaction( return Transaction(
pdus=[p.get_dict() for p in pdu_list],
origin=self.server_name, origin=self.server_name,
pdus=pdus,
ts=int(self._clock.time_msec()), ts=int(self._clock.time_msec()),
destination=None, destination=None,
) )
@ -593,8 +603,21 @@ class _TransactionQueue(object):
logger.debug("TX [%s] Sending transaction...", destination) logger.debug("TX [%s] Sending transaction...", destination)
# Actually send the transaction # 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( code, response = yield self.transport_layer.send_transaction(
transaction transaction,
on_send_callback=cb,
) )
logger.debug("TX [%s] Sent transaction", destination) logger.debug("TX [%s] Sent transaction", destination)

View file

@ -144,7 +144,7 @@ class TransportLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def send_transaction(self, transaction): def send_transaction(self, transaction, on_send_callback=None):
""" Sends the given Transaction to it's destination """ Sends the given Transaction to it's destination
Args: Args:
@ -165,10 +165,23 @@ class TransportLayer(object):
data = transaction.get_dict() 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( code, response = yield self.client.put_json(
transaction.destination, transaction.destination,
path=PREFIX + "/send/%s/" % transaction.transaction_id, path=PREFIX + "/send/%s/" % transaction.transaction_id,
data=data data=data,
on_send_callback=cb,
) )
logger.debug( logger.debug(

View file

@ -15,7 +15,6 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.events import SynapseEvent
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
from ._base import BaseHandler from ._base import BaseHandler
@ -71,10 +70,7 @@ class EventStreamHandler(BaseHandler):
auth_user, room_ids, pagin_config, timeout auth_user, room_ids, pagin_config, timeout
) )
chunks = [ chunks = [self.hs.serialize_event(e) for e in events]
e.get_dict() if isinstance(e, SynapseEvent) else e
for e in events
]
chunk = { chunk = {
"chunk": chunks, "chunk": chunks,
@ -92,7 +88,9 @@ class EventStreamHandler(BaseHandler):
# 10 seconds of grace to allow the client to reconnect again # 10 seconds of grace to allow the client to reconnect again
# before we think they're gone # before we think they're gone
def _later(): def _later():
logger.debug("_later stopped_user_eventstream %s", auth_user) logger.debug(
"_later stopped_user_eventstream %s", auth_user
)
self.distributor.fire( self.distributor.fire(
"stopped_user_eventstream", auth_user "stopped_user_eventstream", auth_user
) )

View file

@ -93,22 +93,18 @@ class FederationHandler(BaseHandler):
""" """
event = self.pdu_codec.event_from_pdu(pdu) event = self.pdu_codec.event_from_pdu(pdu)
logger.debug("Got event: %s", event.event_id)
with (yield self.lock_manager.lock(pdu.context)): with (yield self.lock_manager.lock(pdu.context)):
if event.is_state and not backfilled: if event.is_state and not backfilled:
is_new_state = yield self.state_handler.handle_new_state( is_new_state = yield self.state_handler.handle_new_state(
pdu pdu
) )
if not is_new_state:
return
else: else:
is_new_state = False is_new_state = False
# TODO: Implement something in federation that allows us to # TODO: Implement something in federation that allows us to
# respond to PDU. # respond to PDU.
if hasattr(event, "state_key") and not is_new_state:
logger.debug("Ignoring old state.")
return
target_is_mine = False target_is_mine = False
if hasattr(event, "target_host"): if hasattr(event, "target_host"):
target_is_mine = event.target_host == self.hs.hostname target_is_mine = event.target_host == self.hs.hostname
@ -139,7 +135,11 @@ class FederationHandler(BaseHandler):
else: else:
with (yield self.room_lock.lock(event.room_id)): 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) room = yield self.store.get_room(event.room_id)

View file

@ -124,7 +124,7 @@ class MessageHandler(BaseHandler):
) )
chunk = { chunk = {
"chunk": [e.get_dict() for e in events], "chunk": [self.hs.serialize_event(e) for e in events],
"start": pagin_config.from_token.to_string(), "start": pagin_config.from_token.to_string(),
"end": next_token.to_string(), "end": next_token.to_string(),
} }
@ -296,7 +296,7 @@ class MessageHandler(BaseHandler):
end_token = now_token.copy_and_replace("room_key", token[1]) end_token = now_token.copy_and_replace("room_key", token[1])
d["messages"] = { d["messages"] = {
"chunk": [m.get_dict() for m in messages], "chunk": [self.hs.serialize_event(m) for m in messages],
"start": start_token.to_string(), "start": start_token.to_string(),
"end": end_token.to_string(), "end": end_token.to_string(),
} }
@ -304,7 +304,7 @@ class MessageHandler(BaseHandler):
current_state = yield self.store.get_current_state( current_state = yield self.store.get_current_state(
event.room_id event.room_id
) )
d["state"] = [c.get_dict() for c in current_state] d["state"] = [self.hs.serialize_event(c) for c in current_state]
except: except:
logger.exception("Failed to get snapshot") logger.exception("Failed to get snapshot")

View file

@ -40,8 +40,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):
captcha_info={}):
"""Registers a new client on the server. """Registers a new client on the server.
Args: Args:
@ -54,37 +53,6 @@ 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"]:
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 password_hash = None
if password: if password:
password_hash = bcrypt.hashpw(password, bcrypt.gensalt()) password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
@ -126,15 +94,54 @@ class RegistrationHandler(BaseHandler):
raise RegistrationError( raise RegistrationError(
500, "Cannot generate user ID.") 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.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): def _generate_token(self, user_id):
# urlsafe variant uses _ and - so use . as the separator and replace # 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 # 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): def _threepid_from_creds(self, creds):
httpCli = PlainHttpClient(self.hs) httpCli = PlainHttpClient(self.hs)
# XXX: make this configurable! # XXX: make this configurable!
trustedIdServers = [ 'matrix.org:8090' ] trustedIdServers = ['matrix.org:8090']
if not creds['idServer'] in trustedIdServers: 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']) 'credentials', creds['idServer'])
defer.returnValue(None) defer.returnValue(None)
data = yield httpCli.get_json( data = yield httpCli.get_json(
creds['idServer'], creds['idServer'],
"/_matrix/identity/api/v1/3pid/getValidated3pid", "/_matrix/identity/api/v1/3pid/getValidated3pid",
{ 'sid': creds['sid'], 'clientSecret': creds['clientSecret'] } {'sid': creds['sid'], 'clientSecret': creds['clientSecret']}
) )
if 'medium' in data: if 'medium' in data:
defer.returnValue(data) defer.returnValue(data)
defer.returnValue(None) defer.returnValue(None)
@ -170,44 +177,45 @@ class RegistrationHandler(BaseHandler):
data = yield httpCli.post_urlencoded_get_json( data = yield httpCli.post_urlencoded_get_json(
creds['idServer'], creds['idServer'],
"/_matrix/identity/api/v1/3pid/bind", "/_matrix/identity/api/v1/3pid/bind",
{ 'sid': creds['sid'], 'clientSecret': creds['clientSecret'], {'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
'mxid':mxid } 'mxid': mxid}
) )
defer.returnValue(data) defer.returnValue(data)
@defer.inlineCallbacks @defer.inlineCallbacks
def _validate_captcha(self, ip_addr, private_key, challenge, response): def _validate_captcha(self, ip_addr, private_key, challenge, response):
"""Validates the captcha provided. """Validates the captcha provided.
Returns: Returns:
dict: Containing 'valid'(bool) and 'error_url'(str) if invalid. 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) response)
# parse Google's response. Lovely format.. # parse Google's response. Lovely format..
lines = response.split('\n') lines = response.split('\n')
json = { json = {
"valid": lines[0] == 'true', "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] "error=%s" % lines[1]
} }
defer.returnValue(json) defer.returnValue(json)
@defer.inlineCallbacks @defer.inlineCallbacks
def _submit_captcha(self, ip_addr, private_key, challenge, response): def _submit_captcha(self, ip_addr, private_key, challenge, response):
client = PlainHttpClient(self.hs) client = PlainHttpClient(self.hs)
data = yield client.post_urlencoded_get_raw( data = yield client.post_urlencoded_get_raw(
"www.google.com:80", "www.google.com:80",
"/recaptcha/api/verify", "/recaptcha/api/verify",
accept_partial=True, # twisted dislikes google's response, no content length. # twisted dislikes google's response, no content length.
args={ accept_partial=True,
'privatekey': private_key, args={
'privatekey': private_key,
'remoteip': ip_addr, 'remoteip': ip_addr,
'challenge': challenge, 'challenge': challenge,
'response': response 'response': response
} }
) )
defer.returnValue(data) defer.returnValue(data)

View file

@ -335,7 +335,7 @@ class RoomMemberHandler(BaseHandler):
member_list = yield self.store.get_room_members(room_id=room_id) member_list = yield self.store.get_room_members(room_id=room_id)
event_list = [ event_list = [
entry.get_dict() self.hs.serialize_event(entry)
for entry in member_list for entry in member_list
] ]
chunk_data = { chunk_data = {

View file

@ -122,7 +122,7 @@ class TwistedHttpClient(HttpClient):
self.hs = hs self.hs = hs
@defer.inlineCallbacks @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: if destination in _destination_mappings:
destination = _destination_mappings[destination] destination = _destination_mappings[destination]
@ -131,7 +131,8 @@ class TwistedHttpClient(HttpClient):
"PUT", "PUT",
path.encode("ascii"), path.encode("ascii"),
producer=_JsonProducer(data), 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") logger.debug("Getting resp body")
@ -218,7 +219,7 @@ class TwistedHttpClient(HttpClient):
@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"",
query_bytes=b"", producer=None, headers_dict={}, 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 """ Creates and sends a request to the given url
""" """
headers_dict[b"User-Agent"] = [b"Synapse"] headers_dict[b"User-Agent"] = [b"Synapse"]
@ -242,6 +243,9 @@ class TwistedHttpClient(HttpClient):
endpoint = self._getEndpoint(reactor, destination); endpoint = self._getEndpoint(reactor, destination);
while True: while True:
if on_send_callback:
on_send_callback(destination, method, path_bytes, producer)
try: try:
response = yield self.agent.request( response = yield self.agent.request(
destination, destination,
@ -310,6 +314,9 @@ class _JsonProducer(object):
""" Used by the twisted http client to create the HTTP body from json """ Used by the twisted http client to create the HTTP body from json
""" """
def __init__(self, jsn): def __init__(self, jsn):
self.reset(jsn)
def reset(self, jsn):
self.body = encode_canonical_json(jsn) self.body = encode_canonical_json(jsn)
self.length = len(self.body) self.length = len(self.body)

View file

@ -59,7 +59,7 @@ class EventRestServlet(RestServlet):
event = yield handler.get_event(auth_user, event_id) event = yield handler.get_event(auth_user, event_id)
if event: if event:
defer.returnValue((200, event.get_dict())) defer.returnValue((200, self.hs.serialize_event(event)))
else: else:
defer.returnValue((404, "Event not found.")) defer.returnValue((404, "Event not found."))

View file

@ -17,89 +17,218 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import SynapseError, Codes from synapse.api.errors import SynapseError, Codes
from synapse.api.constants import LoginType
from base import RestServlet, client_path_pattern from base import RestServlet, client_path_pattern
import synapse.util.stringutils as stringutils
import json import json
import logging
import urllib import urllib
logger = logging.getLogger(__name__)
class RegisterRestServlet(RestServlet): 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$") 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 @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
desired_user_id = None register_json = _parse_json(request)
password = None
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: try:
register_json = json.loads(request.content.read()) login_type = register_json["type"]
if "password" in register_json: stages = {
password = register_json["password"].encode("utf-8") LoginType.RECAPTCHA: self._do_recaptcha,
LoginType.PASSWORD: self._do_password,
if type(register_json["user_id"]) == unicode: LoginType.EMAIL_IDENTITY: self._do_email_identity
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
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 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, )
captcha_info=captcha)
if session[LoginType.EMAIL_IDENTITY]:
yield handler.bind_emails(user_id, session["threepidCreds"])
result = { result = {
"user_id": user_id, "user_id": user_id,
"access_token": token, "access_token": token,
"home_server": self.hs.hostname, "home_server": self.hs.hostname,
} }
defer.returnValue( self._remove_session(session)
(200, result) 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): def register_servlets(hs, http_server):

View file

@ -378,7 +378,7 @@ class RoomTriggerBackfill(RestServlet):
handler = self.handlers.federation_handler handler = self.handlers.federation_handler
events = yield handler.backfill(remote_server, room_id, limit) events = yield handler.backfill(remote_server, room_id, limit)
res = [event.get_dict() for event in events] res = [self.hs.serialize_event(event) for event in events]
defer.returnValue((200, res)) defer.returnValue((200, res))

View file

@ -20,6 +20,7 @@
# Imports required for the default HomeServer() implementation # Imports required for the default HomeServer() implementation
from synapse.federation import initialize_http_replication from synapse.federation import initialize_http_replication
from synapse.api.events import serialize_event
from synapse.api.events.factory import EventFactory from synapse.api.events.factory import EventFactory
from synapse.notifier import Notifier from synapse.notifier import Notifier
from synapse.api.auth import Auth from synapse.api.auth import Auth
@ -138,6 +139,9 @@ class BaseHomeServer(object):
object.""" object."""
return RoomID.from_string(s, hs=self) return RoomID.from_string(s, hs=self)
def serialize_event(self, e):
return serialize_event(self, e)
# Build magic accessors for every dependency # Build magic accessors for every dependency
for depname in BaseHomeServer.DEPENDENCIES: for depname in BaseHomeServer.DEPENDENCIES:
BaseHomeServer._make_dependency_method(depname) BaseHomeServer._make_dependency_method(depname)

View file

@ -68,7 +68,8 @@ class DataStore(RoomMemberStore, RoomStore,
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @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 stream_ordering = None
if backfilled: if backfilled:
if not self.min_token_deferred.called: if not self.min_token_deferred.called:
@ -83,6 +84,7 @@ class DataStore(RoomMemberStore, RoomStore,
event=event, event=event,
backfilled=backfilled, backfilled=backfilled,
stream_ordering=stream_ordering, stream_ordering=stream_ordering,
is_new_state=is_new_state,
) )
except _RollbackButIsFineException as e: except _RollbackButIsFineException as e:
pass pass
@ -109,12 +111,14 @@ class DataStore(RoomMemberStore, RoomStore,
defer.returnValue(event) defer.returnValue(event)
def _persist_pdu_event_txn(self, txn, pdu=None, event=None, 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: if pdu is not None:
self._persist_event_pdu_txn(txn, pdu) self._persist_event_pdu_txn(txn, pdu)
if event is not None: if event is not None:
return self._persist_event_txn( 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): def _persist_event_pdu_txn(self, txn, pdu):
@ -141,7 +145,8 @@ class DataStore(RoomMemberStore, RoomStore,
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth) self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
@log_function @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: if event.type == RoomMemberEvent.TYPE:
self._store_room_member_txn(txn, event) self._store_room_member_txn(txn, event)
elif event.type == FeedbackEvent.TYPE: elif event.type == FeedbackEvent.TYPE:
@ -195,7 +200,7 @@ class DataStore(RoomMemberStore, RoomStore,
) )
raise _RollbackButIsFineException("_persist_event") raise _RollbackButIsFineException("_persist_event")
if not backfilled and hasattr(event, "state_key"): if is_new_state and hasattr(event, "state_key"):
vals = { vals = {
"event_id": event.event_id, "event_id": event.event_id,
"room_id": event.room_id, "room_id": event.room_id,

View file

@ -315,6 +315,10 @@ class SQLBaseStore(object):
d["content"] = json.loads(d["content"]) d["content"] = json.loads(d["content"])
del d["unrecognized_keys"] del d["unrecognized_keys"]
if "age_ts" not in d:
# For compatibility
d["age_ts"] = d["ts"] if "ts" in d else 0
return self.event_factory.create_event( return self.event_factory.create_event(
etype=d["type"], etype=d["type"],
**d **d

View file

@ -17,7 +17,7 @@ from twisted.internet import defer
from tests import unittest from tests import unittest
# python imports # python imports
from mock import Mock from mock import Mock, ANY
from ..utils import MockHttpResource, MockClock from ..utils import MockHttpResource, MockClock
@ -181,7 +181,8 @@ class FederationTestCase(unittest.TestCase):
"depth": 1, "depth": 1,
}, },
] ]
} },
on_send_callback=ANY,
) )
@defer.inlineCallbacks @defer.inlineCallbacks
@ -212,7 +213,9 @@ class FederationTestCase(unittest.TestCase):
"content": {"testing": "content here"}, "content": {"testing": "content here"},
} }
], ],
}) },
on_send_callback=ANY,
)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_recv_edu(self): def test_recv_edu(self):

View file

@ -74,7 +74,9 @@ class FederationTestCase(unittest.TestCase):
yield self.handlers.federation_handler.on_receive_pdu(pdu, False) yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
self.datastore.persist_event.assert_called_once_with(ANY, False) self.datastore.persist_event.assert_called_once_with(
ANY, False, is_new_state=False
)
self.notifier.on_new_room_event.assert_called_once_with(ANY) self.notifier.on_new_room_event.assert_called_once_with(ANY)
@defer.inlineCallbacks @defer.inlineCallbacks

View file

@ -319,7 +319,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
"observer_user": "@apple:test", "observer_user": "@apple:test",
"observed_user": "@cabbage:elsewhere", "observed_user": "@cabbage:elsewhere",
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -345,7 +346,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
"observed_user": "@apple:test", "observed_user": "@apple:test",
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -376,7 +378,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
"observed_user": "@durian:test", "observed_user": "@durian:test",
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -765,7 +768,8 @@ class PresencePushTestCase(unittest.TestCase):
"last_active_ago": 0}, "last_active_ago": 0},
], ],
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -780,7 +784,8 @@ class PresencePushTestCase(unittest.TestCase):
"last_active_ago": 0}, "last_active_ago": 0},
], ],
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -906,6 +911,7 @@ class PresencePushTestCase(unittest.TestCase):
], ],
} }
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -920,6 +926,7 @@ class PresencePushTestCase(unittest.TestCase):
], ],
} }
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -949,6 +956,7 @@ class PresencePushTestCase(unittest.TestCase):
], ],
} }
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -1145,6 +1153,7 @@ class PresencePollingTestCase(unittest.TestCase):
"poll": [ "@potato:remote" ], "poll": [ "@potato:remote" ],
}, },
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -1157,6 +1166,7 @@ class PresencePollingTestCase(unittest.TestCase):
"push": [ {"user_id": "@clementine:test" }], "push": [ {"user_id": "@clementine:test" }],
}, },
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -1185,6 +1195,7 @@ class PresencePollingTestCase(unittest.TestCase):
"push": [ {"user_id": "@fig:test" }], "push": [ {"user_id": "@fig:test" }],
}, },
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -1217,6 +1228,7 @@ class PresencePollingTestCase(unittest.TestCase):
"unpoll": [ "@potato:remote" ], "unpoll": [ "@potato:remote" ],
}, },
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -1248,6 +1260,7 @@ class PresencePollingTestCase(unittest.TestCase):
], ],
}, },
), ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )

View file

@ -169,7 +169,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
"user_id": self.u_apple.to_string(), "user_id": self.u_apple.to_string(),
"typing": True, "typing": True,
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )
@ -219,7 +220,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
"user_id": self.u_apple.to_string(), "user_id": self.u_apple.to_string(),
"typing": False, "typing": False,
} }
) ),
on_send_callback=ANY,
), ),
defer.succeed((200, "OK")) defer.succeed((200, "OK"))
) )

View file

@ -95,8 +95,14 @@ class RestTestCase(unittest.TestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def register(self, user_id): def register(self, user_id):
(code, response) = yield self.mock_resource.trigger("POST", "/register", (code, response) = yield self.mock_resource.trigger(
'{"user_id":"%s"}' % user_id) "POST",
"/register",
json.dumps({
"user": user_id,
"password": "test",
"type": "m.login.password"
}))
self.assertEquals(200, code) self.assertEquals(200, code)
defer.returnValue(response) defer.returnValue(response)

View file

@ -81,38 +81,155 @@ angular.module('matrixService', [])
return $http(request); return $http(request);
}; };
var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) {
var data = {};
if (loginType === "m.login.recaptcha") {
var challengeToken = Recaptcha.get_challenge();
var captchaEntry = Recaptcha.get_response();
data = {
type: "m.login.recaptcha",
challenge: challengeToken,
response: captchaEntry
};
}
else if (loginType === "m.login.email.identity") {
data = {
threepidCreds: threepidCreds
};
}
else if (loginType === "m.login.password") {
data = {
user: userName,
password: password
};
}
if (sessionId) {
data.session = sessionId;
}
data.type = loginType;
console.log("doRegisterLogin >>> " + loginType);
return doRequest("POST", path, undefined, data);
};
return { return {
/****** Home server API ******/ /****** Home server API ******/
prefix: prefixPath, prefix: prefixPath,
// Register an user // Register an user
register: function(user_name, password, threepidCreds, useCaptcha) { register: function(user_name, password, threepidCreds, useCaptcha) {
// The REST path spec // registration is composed of multiple requests, to check you can
// register, then to actually register. This deferred will fire when
// all the requests are done, along with the final response.
var deferred = $q.defer();
var path = "/register"; var path = "/register";
var data = { // check we can actually register with this HS.
user_id: user_name, doRequest("GET", path, undefined, undefined).then(
password: password, function(response) {
threepidCreds: threepidCreds console.log("/register [1] : "+JSON.stringify(response));
}; var flows = response.data.flows;
var knownTypes = [
"m.login.password",
"m.login.recaptcha",
"m.login.email.identity"
];
// if they entered 3pid creds, we want to use a flow which uses it.
var useThreePidFlow = threepidCreds != undefined;
var flowIndex = 0;
var firstRegType = undefined;
for (var i=0; i<flows.length; i++) {
var isThreePidFlow = false;
if (flows[i].stages) {
for (var j=0; j<flows[i].stages.length; j++) {
var regType = flows[i].stages[j];
if (knownTypes.indexOf(regType) === -1) {
deferred.reject("Unknown type: "+regType);
return;
}
if (regType == "m.login.email.identity") {
isThreePidFlow = true;
}
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;
}
}
}
if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) {
flowIndex = i;
}
if (knownTypes.indexOf(flows[i].type) == -1) {
deferred.reject("Unknown type: "+flows[i].type);
return;
}
}
// looks like we can register fine, go ahead and do it.
console.log("Using flow " + JSON.stringify(flows[flowIndex]));
firstRegType = flows[flowIndex].type;
var sessionId = undefined;
// generic response processor so it can loop as many times as required
var loginResponseFunc = function(response) {
if (response.data.session) {
sessionId = response.data.session;
}
console.log("login response: " + JSON.stringify(response.data));
if (response.data.access_token) {
deferred.resolve(response);
}
else if (response.data.next) {
var nextType = response.data.next;
if (response.data.next instanceof Array) {
for (var i=0; i<response.data.next.length; i++) {
if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") {
nextType = response.data.next[i];
break;
}
else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") {
nextType = response.data.next[i];
break;
}
}
}
return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then(
loginResponseFunc,
function(err) {
deferred.reject(err);
}
);
}
else {
deferred.reject("Unknown continuation: "+JSON.stringify(response));
}
};
// set the ball rolling
doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then(
loginResponseFunc,
function(err) {
deferred.reject(err);
}
);
},
function(err) {
deferred.reject(err);
}
);
if (useCaptcha) { return deferred.promise;
// 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
};
}
return doRequest("POST", path, undefined, data);
}, },
// Create a room // Create a room