test_replication.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import json
  2. from mock import Mock
  3. from sydent.threepid import ThreepidAssociation
  4. from sydent.threepid.signer import Signer
  5. from tests.utils import make_request, make_sydent
  6. from twisted.web.client import Response
  7. from twisted.internet import defer
  8. from twisted.trial import unittest
  9. class ReplicationTestCase(unittest.TestCase):
  10. """Test that a Sydent can correctly replicate data with another Sydent"""
  11. def setUp(self):
  12. # Create a new sydent
  13. config = {
  14. "crypto": {
  15. "ed25519.signingkey": "ed25519 0 FJi1Rnpj3/otydngacrwddFvwz/dTDsBv62uZDN2fZM"
  16. }
  17. }
  18. self.sydent = make_sydent(test_config=config)
  19. # Create a fake peer to replicate to.
  20. peer_public_key_base64 = "+vB8mTaooD/MA8YYZM8t9+vnGhP1937q2icrqPV9JTs"
  21. # Inject our fake peer into the database.
  22. cur = self.sydent.db.cursor()
  23. cur.execute(
  24. "INSERT INTO peers (name, port, lastSentVersion, active) VALUES (?, ?, ?, ?)",
  25. ("fake.server", 1234, 0, 1)
  26. )
  27. cur.execute(
  28. "INSERT INTO peer_pubkeys (peername, alg, key) VALUES (?, ?, ?)",
  29. ("fake.server", "ed25519", peer_public_key_base64)
  30. )
  31. self.sydent.db.commit()
  32. # Build some fake associations.
  33. self.assocs = []
  34. assoc_count = 150
  35. for i in range(assoc_count):
  36. assoc = ThreepidAssociation(
  37. medium="email",
  38. address="bob%d@example.com" % i,
  39. lookup_hash=None,
  40. mxid="@bob%d:example.com" % i,
  41. ts=(i * 10000),
  42. not_before=0,
  43. not_after=99999999999,
  44. )
  45. self.assocs.append(assoc)
  46. def test_incoming_replication(self):
  47. """Impersonate a peer that sends a replication push to Sydent, then checks that it
  48. accepts the payload and saves it correctly.
  49. """
  50. self.sydent.run()
  51. # Configure the Sydent to impersonate. We need to use "fake.server" as the
  52. # server's name because that's the name the recipient Sydent has for it. On top
  53. # of that, the replication servlet expects a TLS certificate in the request so it
  54. # can extract a common name and figure out which peer sent it from its common
  55. # name. The common name of the certificate we use for tests is fake.server.
  56. config = {
  57. "general": {
  58. "server.name": "fake.server"
  59. },
  60. "crypto": {
  61. "ed25519.signingkey": "ed25519 0 b29eXMMAYCFvFEtq9mLI42aivMtcg4Hl0wK89a+Vb6c"
  62. }
  63. }
  64. fake_sender_sydent = make_sydent(config)
  65. signer = Signer(fake_sender_sydent)
  66. # Sign the associations with the Sydent to impersonate so the recipient Sydent
  67. # can verify the signatures on them.
  68. signed_assocs = {}
  69. for assoc_id, assoc in enumerate(self.assocs):
  70. signed_assoc = signer.signedThreePidAssociation(assoc)
  71. signed_assocs[assoc_id] = signed_assoc
  72. # Send the replication push.
  73. body = json.dumps({"sgAssocs": signed_assocs})
  74. request, channel = make_request(
  75. self.sydent.reactor, "POST", "/_matrix/identity/replicate/v1/push", body
  76. )
  77. request.render(self.sydent.servlets.replicationPush)
  78. self.assertEqual(channel.code, 200)
  79. # Check that the recipient Sydent has correctly saved the associations in the
  80. # push.
  81. cur = self.sydent.db.cursor()
  82. res = cur.execute("SELECT originId, sgAssoc FROM global_threepid_associations")
  83. for row in res.fetchall():
  84. originId = row[0]
  85. signed_assoc = json.loads(row[1])
  86. self.assertDictEqual(signed_assoc, signed_assocs[originId])
  87. def test_outgoing_replication(self):
  88. """Make a fake peer and associations and make sure Sydent tries to push to it.
  89. """
  90. cur = self.sydent.db.cursor()
  91. # Insert the fake associations into the database.
  92. cur.executemany(
  93. "INSERT INTO local_threepid_associations "
  94. "(medium, address, lookup_hash, mxid, ts, notBefore, notAfter) "
  95. "VALUES (?, ?, ?, ?, ?, ?, ?)",
  96. [
  97. (
  98. assoc.medium,
  99. assoc.address,
  100. assoc.lookup_hash,
  101. assoc.mxid,
  102. assoc.ts,
  103. assoc.not_before,
  104. assoc.not_after,
  105. )
  106. for assoc in self.assocs
  107. ]
  108. )
  109. self.sydent.db.commit()
  110. # Manually sign all associations so we can check whether Sydent attempted to
  111. # push the same.
  112. signer = Signer(self.sydent)
  113. signed_assocs = {}
  114. for assoc_id, assoc in enumerate(self.assocs):
  115. signed_assoc = signer.signedThreePidAssociation(assoc)
  116. signed_assocs[assoc_id] = signed_assoc
  117. sent_assocs = {}
  118. def request(method, uri, headers, body):
  119. # Check the method and the URI.
  120. assert method == 'POST'
  121. assert uri == 'https://fake.server:1234/_matrix/identity/replicate/v1/push'
  122. # postJson calls the agent with a StringIO within a FileBodyProducer, so we
  123. # need to unpack the payload correctly.
  124. payload = json.loads(body._inputFile.buf)
  125. for assoc_id, assoc in payload['sgAssocs'].items():
  126. sent_assocs[assoc_id] = assoc
  127. # Return with a fake response wrapped in a Deferred.
  128. d = defer.Deferred()
  129. d.callback(Response((b'HTTP', 1, 1), 200, b'OK', None, None))
  130. return d
  131. # Mock the replication client's agent so it runs the custom code instead of
  132. # actually sending the requests.
  133. agent = Mock(spec=['request'])
  134. agent.request.side_effect = request
  135. self.sydent.replicationHttpsClient.agent = agent
  136. # Start Sydent and allow some time for all the necessary pushes to happen.
  137. self.sydent.run()
  138. self.sydent.reactor.advance(1000)
  139. # Check that, now that Sydent pushed all the associations it was meant to, we
  140. # have all of the associations we initially inserted.
  141. self.assertEqual(len(self.assocs), len(sent_assocs))
  142. for assoc_id, assoc in sent_assocs.items():
  143. # Replication payloads use a specific format that causes the JSON encoder to
  144. # convert the numeric indexes to string, so we need to convert them back when
  145. # looking up in signed_assocs. Also, the ID of the first association Sydent
  146. # will push will be 1, so we need to subtract 1 when figuring out which index
  147. # to lookup.
  148. self.assertDictEqual(assoc, signed_assocs[int(assoc_id)-1])