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.
 
 
 
 

276 lines
6.9 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, MetaData
  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. self._hashes = {}
  27. try:
  28. data = _asn1coder.loads(open(fname).read())
  29. self._trustedkeys = {}
  30. for i in data['trustedkeys']:
  31. self.addpubkey(i)
  32. if data['objstore']:
  33. self._objstore = { uuid.UUID(bytes=k):
  34. map(MDBase.create_obj, v) for k, v in
  35. data['objstore'].iteritems() }
  36. else:
  37. self._objstore = {}
  38. except IOError:
  39. self._trustedkeys = {}
  40. self._objstore = {}
  41. app = Klein()
  42. def addpubkey(self, pubkey):
  43. persona = Persona.from_pubkey(pubkey)
  44. self._trustedkeys[persona.uuid] = persona
  45. def store(self):
  46. obj = {
  47. 'trustedkeys': [ i.get_pubkey() for i in self._trustedkeys.itervalues() ],
  48. 'objstore': self._objstore,
  49. }
  50. with open(self._fname, 'w') as fp:
  51. fp.write(_asn1coder.dumps(obj))
  52. @app.route('/lookup/<hash>')
  53. def lookup(self, request, hash):
  54. if hash in self._hashes:
  55. return
  56. request.setResponseCode(404)
  57. @app.route('/obj/<id>')
  58. def obj_lookup(self, request, id):
  59. try:
  60. id = uuid.UUID(id)
  61. return self._objstore[id][-1].encode()
  62. except ValueError: # invalid format for uuid
  63. request.setResponseCode(400)
  64. except KeyError: # no object
  65. request.setResponseCode(404)
  66. def _storeobj(self, obj):
  67. self._objstore.setdefault(obj.uuid, []).append(obj)
  68. try:
  69. hashes = obj.hashes
  70. for i in hashes:
  71. self._hashes.setdefault(i, []).append(obj.uuid)
  72. except AttributeError:
  73. pass
  74. @app.route('/store')
  75. def storeobj(self, request):
  76. try:
  77. obj = MDBase.decode(request.content.read())
  78. #if obj.type == 'identity':
  79. keyuuid = obj.uuid
  80. #else:
  81. # keyuuid = obj.created_by_ref
  82. persona = self._trustedkeys[keyuuid]
  83. persona.verify(obj)
  84. self._storeobj(obj)
  85. request.setResponseCode(201)
  86. except Exception:
  87. request.setResponseCode(401)
  88. # twistd support
  89. #medaserver = MEDAServer()
  90. #resource = medaserver.app.resource
  91. def main():
  92. from optparse import OptionParser
  93. parser = OptionParser()
  94. parser.add_option('-a', action='append', dest='addpubkey',
  95. default=[], help='Add specified public key as a trusted key.')
  96. options, args = parser.parse_args()
  97. medaserver = MEDAServer(defaultfile)
  98. try:
  99. if options.addpubkey:
  100. for i in options.addpubkey:
  101. medaserver.addpubkey(i)
  102. return
  103. medaserver.app.run()
  104. finally:
  105. medaserver.store()
  106. if __name__ == '__main__': # pragma: no cover
  107. main()
  108. class _BaseServerTest(unittest.TestCase):
  109. def setUp(self):
  110. d = os.path.realpath(tempfile.mkdtemp())
  111. self.basetempdir = d
  112. self.medaserverfile = os.path.join(self.basetempdir, 'serverstore.pasn1')
  113. self.medaserver = MEDAServer(self.medaserverfile)
  114. self.requests = FakeRequests(self.medaserver.app)
  115. def tearDown(self):
  116. shutil.rmtree(self.basetempdir)
  117. self.basetempdir = None
  118. class _TestCases(_BaseServerTest):
  119. def test_objlookup(self):
  120. # that when fetching an non-existant object
  121. r = self.requests.get('/obj/%s' % str(uuid.uuid4()))
  122. # it is 404, not found
  123. self.assertEqual(r.status_code, 404)
  124. # that passing an invalid uuid
  125. r = self.requests.get('/obj/bogusuuid')
  126. # it is 400, bad request
  127. self.assertEqual(r.status_code, 400)
  128. def test_pubkeystorage(self):
  129. # that an identity
  130. persona = Persona()
  131. persona.generate_key()
  132. # that by default, put's
  133. r = self.requests.put('/store', data=persona.get_identity().encode())
  134. # are denied
  135. self.assertEqual(r.status_code, 401)
  136. # can have it's public key added to the server
  137. self.medaserver.addpubkey(persona.get_pubkey())
  138. # that it can store the pubkey's identity
  139. r = self.requests.put('/store', data=persona.get_identity().encode())
  140. self.assertEqual(r.status_code, 201)
  141. # that an object with a bad signature
  142. badsigobj = persona.get_identity().new_version()
  143. # when stored
  144. r = self.requests.put('/store', data=badsigobj.encode())
  145. # is rejected
  146. self.assertEqual(r.status_code, 401)
  147. # that when fetching the object back
  148. r = self.requests.get('/obj/%s' % str(persona.uuid))
  149. # it is successful
  150. self.assertEqual(r.status_code, 200)
  151. # that the returned data
  152. fetchobj = MDBase.decode(r.text)
  153. # matches what was stored
  154. self.assertEqual(fetchobj, persona.get_identity())
  155. # that when stored
  156. self.medaserver.store()
  157. # and restarted
  158. self.medaserver = MEDAServer(self.medaserverfile)
  159. self.requests = FakeRequests(self.medaserver.app)
  160. # that fetching a previously successful object
  161. r = self.requests.get('/obj/%s' % str(persona.uuid))
  162. # it is successful
  163. self.assertEqual(r.status_code, 200)
  164. @mock.patch('klein.Klein.run')
  165. def test_addpubkey(self, apprun):
  166. persona = Persona()
  167. persona.generate_key()
  168. with nested(mock.patch('server.MEDAServer.addpubkey'),
  169. mock.patch('sys.argv', [ 'progname', '-a',
  170. persona.get_pubkey() ])) as (addpub, argv):
  171. main()
  172. addpub.assert_called_with(persona.get_pubkey())
  173. apprun.assert_not_called()
  174. # Note: because of this mock, it hides the actual app.run call w/
  175. # a mock
  176. @mock.patch('server.MEDAServer')
  177. def test_medaserverinstanciated(self, medaserver):
  178. # that when main is run
  179. main()
  180. # that it gets called with the default storage file
  181. medaserver.assert_called_with('mediaserver.store.pasn1')
  182. @mock.patch('server.MEDAServer.store')
  183. @mock.patch('klein.Klein.run')
  184. def test_appruns(self, kleinrun, storefun):
  185. main()
  186. kleinrun.assert_called()
  187. storefun.assert_called()
  188. class _TestPostConfig(_BaseServerTest):
  189. def setUp(self):
  190. _BaseServerTest.setUp(self)
  191. persona = Persona()
  192. persona.generate_key()
  193. self.persona = persona
  194. self.medaserver.addpubkey(persona.get_pubkey())
  195. def test_hashlookup(self):
  196. # that the hash of a file
  197. h = hashlib.sha256(open('fixtures/testfiles/test.txt').read()).hexdigest()
  198. # when looked up
  199. r = self.requests.get('/lookup/%s' % h)
  200. # returns a 404
  201. self.assertEqual(r.status_code, 404)
  202. # but when the metadata object
  203. mdobj = self.persona.MetaData(hashes=[ h ], mimetype=[ 'text/plain' ])
  204. # is in the server
  205. self.medaserver._storeobj(mdobj)
  206. # when looked up
  207. r = self.requests.get('/lookup/%s' % h)
  208. # it is found
  209. self.assertEqual(r.status_code, 200)