MetaData Sharing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

242 lines
6.3 KiB

  1. #!/usr/bin/env python
  2. # Notes:
  3. # Python requests: https://2.python-requests.org/en/master/
  4. # IRequest interface: https://twistedmatrix.com/documents/current/api/twisted.web.iweb.IRequest.html
  5. # IResource interface: https://twistedmatrix.com/documents/current/api/twisted.web.resource.IResource.html
  6. # Twisted TDD: https://twistedmatrix.com/documents/current/core/howto/trial.html
  7. # Hypothesis: https://hypothesis.readthedocs.io/en/latest/
  8. # Going Async from Flask to Twisted Klein: https://crossbario.com/blog/Going-Asynchronous-from-Flask-to-Twisted-Klein/
  9. # Klein POST docs: https://klein.readthedocs.io/en/latest/examples/handlingpost.html
  10. from contextlib import nested
  11. from klein import Klein
  12. from kleintest import *
  13. from twisted.trial import unittest
  14. from twisted.web.iweb import IRequest
  15. from cli import _asn1coder, Persona, MDBase
  16. import hashlib
  17. import mock
  18. import os.path
  19. import shutil
  20. import tempfile
  21. import uuid
  22. defaultfile = 'mediaserver.store.pasn1'
  23. class MEDAServer:
  24. def __init__(self, fname):
  25. self._fname = fname
  26. try:
  27. data = _asn1coder.loads(open(fname).read())
  28. self._trustedkeys = {}
  29. for i in data['trustedkeys']:
  30. self.addpubkey(i)
  31. if data['objstore']:
  32. self._objstore = { uuid.UUID(bytes=k):
  33. map(MDBase.create_obj, v) for k, v in
  34. data['objstore'].iteritems() }
  35. else:
  36. self._objstore = {}
  37. except IOError:
  38. self._trustedkeys = {}
  39. self._objstore = {}
  40. app = Klein()
  41. def addpubkey(self, pubkey):
  42. persona = Persona.from_pubkey(pubkey)
  43. self._trustedkeys[persona.uuid] = persona
  44. def store(self):
  45. obj = {
  46. 'trustedkeys': [ i.get_pubkey() for i in self._trustedkeys.itervalues() ],
  47. 'objstore': self._objstore,
  48. }
  49. with open(self._fname, 'w') as fp:
  50. fp.write(_asn1coder.dumps(obj))
  51. @app.route('/obj/<id>')
  52. def obj_lookup(self, request, id):
  53. try:
  54. id = uuid.UUID(id)
  55. return self._objstore[id][-1].encode()
  56. except ValueError: # invalid format for uuid
  57. request.setResponseCode(400)
  58. except KeyError: # no object
  59. request.setResponseCode(404)
  60. @app.route('/store')
  61. def storeobj(self, request):
  62. try:
  63. obj = MDBase.decode(request.content.read())
  64. #if obj.type == 'identity':
  65. keyuuid = obj.uuid
  66. #else:
  67. # keyuuid = obj.created_by_ref
  68. persona = self._trustedkeys[keyuuid]
  69. persona.verify(obj)
  70. self._objstore.setdefault(obj.uuid, []).append(obj)
  71. request.setResponseCode(201)
  72. except Exception:
  73. request.setResponseCode(401)
  74. # twistd support
  75. #medaserver = MEDAServer()
  76. #resource = medaserver.app.resource
  77. def main():
  78. from optparse import OptionParser
  79. parser = OptionParser()
  80. parser.add_option('-a', action='append', dest='addpubkey',
  81. default=[], help='Add specified public key as a trusted key.')
  82. options, args = parser.parse_args()
  83. medaserver = MEDAServer(defaultfile)
  84. try:
  85. if options.addpubkey:
  86. for i in options.addpubkey:
  87. medaserver.addpubkey(i)
  88. return
  89. medaserver.app.run()
  90. finally:
  91. medaserver.store()
  92. if __name__ == '__main__': # pragma: no cover
  93. main()
  94. class _BaseServerTest(unittest.TestCase):
  95. def setUp(self):
  96. d = os.path.realpath(tempfile.mkdtemp())
  97. self.basetempdir = d
  98. self.medaserverfile = os.path.join(self.basetempdir, 'serverstore.pasn1')
  99. self.medaserver = MEDAServer(self.medaserverfile)
  100. self.requests = FakeRequests(self.medaserver.app)
  101. def tearDown(self):
  102. shutil.rmtree(self.basetempdir)
  103. self.basetempdir = None
  104. class _TestCases(_BaseServerTest):
  105. def test_objlookup(self):
  106. # that when fetching an non-existant object
  107. r = self.requests.get('/obj/%s' % str(uuid.uuid4()))
  108. # it is 404, not found
  109. self.assertEqual(r.status_code, 404)
  110. # that passing an invalid uuid
  111. r = self.requests.get('/obj/bogusuuid')
  112. # it is 400, bad request
  113. self.assertEqual(r.status_code, 400)
  114. def test_pubkeystorage(self):
  115. # that an identity
  116. persona = Persona()
  117. persona.generate_key()
  118. # that by default, put's
  119. r = self.requests.put('/store', data=persona.get_identity().encode())
  120. # are denied
  121. self.assertEqual(r.status_code, 401)
  122. # can have it's public key added to the server
  123. self.medaserver.addpubkey(persona.get_pubkey())
  124. # that it can store the pubkey's identity
  125. r = self.requests.put('/store', data=persona.get_identity().encode())
  126. self.assertEqual(r.status_code, 201)
  127. # that an object with a bad signature
  128. badsigobj = persona.get_identity().new_version()
  129. # when stored
  130. r = self.requests.put('/store', data=badsigobj.encode())
  131. # is rejected
  132. self.assertEqual(r.status_code, 401)
  133. # that when fetching the object back
  134. r = self.requests.get('/obj/%s' % str(persona.uuid))
  135. # it is successful
  136. self.assertEqual(r.status_code, 200)
  137. # that the returned data
  138. fetchobj = MDBase.decode(r.text)
  139. # matches what was stored
  140. self.assertEqual(fetchobj, persona.get_identity())
  141. # that when stored
  142. self.medaserver.store()
  143. # and restarted
  144. self.medaserver = MEDAServer(self.medaserverfile)
  145. self.requests = FakeRequests(self.medaserver.app)
  146. # that fetching a previously successful object
  147. r = self.requests.get('/obj/%s' % str(persona.uuid))
  148. # it is successful
  149. self.assertEqual(r.status_code, 200)
  150. @mock.patch('klein.Klein.run')
  151. def test_addpubkey(self, apprun):
  152. persona = Persona()
  153. persona.generate_key()
  154. with nested(mock.patch('server.MEDAServer.addpubkey'),
  155. mock.patch('sys.argv', [ 'progname', '-a',
  156. persona.get_pubkey() ])) as (addpub, argv):
  157. main()
  158. addpub.assert_called_with(persona.get_pubkey())
  159. apprun.assert_not_called()
  160. # Note: because of this mock, it hides the actual app.run call w/
  161. # a mock
  162. @mock.patch('server.MEDAServer')
  163. def test_medaserverinstanciated(self, medaserver):
  164. # that when main is run
  165. main()
  166. # that it gets called with the default storage file
  167. medaserver.assert_called_with('mediaserver.store.pasn1')
  168. @mock.patch('server.MEDAServer.store')
  169. @mock.patch('klein.Klein.run')
  170. def test_appruns(self, kleinrun, storefun):
  171. main()
  172. kleinrun.assert_called()
  173. storefun.assert_called()
  174. class _TestPostConfig(_BaseServerTest):
  175. def setUp(self):
  176. _BaseServerTest.setUp(self)
  177. persona = Persona()
  178. persona.generate_key()
  179. self.persona = persona
  180. self.medaserver.addpubkey(persona.get_pubkey())
  181. def test_hashlookup(self):
  182. h = hashlib.sha256(open('fixtures/testfiles/test.txt').read()).hexdigest()
  183. r = self.requests.get('/lookup/%s' % h)
  184. self.assertEqual(r.status_code, 404)