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