#!/usr/bin/env python # Notes: # Python requests: https://2.python-requests.org/en/master/ # IRequest interface: https://twistedmatrix.com/documents/current/api/twisted.web.iweb.IRequest.html # IResource interface: https://twistedmatrix.com/documents/current/api/twisted.web.resource.IResource.html # Twisted TDD: https://twistedmatrix.com/documents/current/core/howto/trial.html # Hypothesis: https://hypothesis.readthedocs.io/en/latest/ # Going Async from Flask to Twisted Klein: https://crossbario.com/blog/Going-Asynchronous-from-Flask-to-Twisted-Klein/ # Klein POST docs: https://klein.readthedocs.io/en/latest/examples/handlingpost.html from contextlib import nested from klein import Klein from kleintest import * from twisted.trial import unittest from twisted.web.iweb import IRequest from cli import _asn1coder, Persona, MDBase, MetaData import hashlib import mock import os.path import shutil import tempfile import uuid defaultfile = 'mediaserver.store.pasn1' class MEDAServer: def __init__(self, fname): self._fname = fname self._hashes = {} try: data = _asn1coder.loads(open(fname).read()) self._trustedkeys = {} for i in data['trustedkeys']: self.addpubkey(i) if data['objstore']: self._objstore = { uuid.UUID(bytes=k): map(MDBase.create_obj, v) for k, v in data['objstore'].iteritems() } else: self._objstore = {} except IOError: self._trustedkeys = {} self._objstore = {} app = Klein() def addpubkey(self, pubkey): persona = Persona.from_pubkey(pubkey) self._trustedkeys[persona.uuid] = persona def store(self): obj = { 'trustedkeys': [ i.get_pubkey() for i in self._trustedkeys.itervalues() ], 'objstore': self._objstore, } with open(self._fname, 'w') as fp: fp.write(_asn1coder.dumps(obj)) @app.route('/lookup/') def lookup(self, request, hash): if hash in self._hashes: return request.setResponseCode(404) @app.route('/obj/') def obj_lookup(self, request, id): try: id = uuid.UUID(id) return self._objstore[id][-1].encode() except ValueError: # invalid format for uuid request.setResponseCode(400) except KeyError: # no object request.setResponseCode(404) def _storeobj(self, obj): self._objstore.setdefault(obj.uuid, []).append(obj) try: hashes = obj.hashes for i in hashes: self._hashes.setdefault(i, []).append(obj.uuid) except AttributeError: pass @app.route('/store') def storeobj(self, request): try: obj = MDBase.decode(request.content.read()) #if obj.type == 'identity': keyuuid = obj.uuid #else: # keyuuid = obj.created_by_ref persona = self._trustedkeys[keyuuid] persona.verify(obj) self._storeobj(obj) request.setResponseCode(201) except Exception: request.setResponseCode(401) # twistd support #medaserver = MEDAServer() #resource = medaserver.app.resource def main(): from optparse import OptionParser parser = OptionParser() parser.add_option('-a', action='append', dest='addpubkey', default=[], help='Add specified public key as a trusted key.') options, args = parser.parse_args() medaserver = MEDAServer(defaultfile) try: if options.addpubkey: for i in options.addpubkey: medaserver.addpubkey(i) return medaserver.app.run() finally: medaserver.store() if __name__ == '__main__': # pragma: no cover main() class _BaseServerTest(unittest.TestCase): def setUp(self): d = os.path.realpath(tempfile.mkdtemp()) self.basetempdir = d self.medaserverfile = os.path.join(self.basetempdir, 'serverstore.pasn1') self.medaserver = MEDAServer(self.medaserverfile) self.requests = FakeRequests(self.medaserver.app) def tearDown(self): shutil.rmtree(self.basetempdir) self.basetempdir = None class _TestCases(_BaseServerTest): def test_objlookup(self): # that when fetching an non-existant object r = self.requests.get('/obj/%s' % str(uuid.uuid4())) # it is 404, not found self.assertEqual(r.status_code, 404) # that passing an invalid uuid r = self.requests.get('/obj/bogusuuid') # it is 400, bad request self.assertEqual(r.status_code, 400) def test_pubkeystorage(self): # that an identity persona = Persona() persona.generate_key() # that by default, put's r = self.requests.put('/store', data=persona.get_identity().encode()) # are denied self.assertEqual(r.status_code, 401) # can have it's public key added to the server self.medaserver.addpubkey(persona.get_pubkey()) # that it can store the pubkey's identity r = self.requests.put('/store', data=persona.get_identity().encode()) self.assertEqual(r.status_code, 201) # that an object with a bad signature badsigobj = persona.get_identity().new_version() # when stored r = self.requests.put('/store', data=badsigobj.encode()) # is rejected self.assertEqual(r.status_code, 401) # that when fetching the object back r = self.requests.get('/obj/%s' % str(persona.uuid)) # it is successful self.assertEqual(r.status_code, 200) # that the returned data fetchobj = MDBase.decode(r.text) # matches what was stored self.assertEqual(fetchobj, persona.get_identity()) # that when stored self.medaserver.store() # and restarted self.medaserver = MEDAServer(self.medaserverfile) self.requests = FakeRequests(self.medaserver.app) # that fetching a previously successful object r = self.requests.get('/obj/%s' % str(persona.uuid)) # it is successful self.assertEqual(r.status_code, 200) @mock.patch('klein.Klein.run') def test_addpubkey(self, apprun): persona = Persona() persona.generate_key() with nested(mock.patch('server.MEDAServer.addpubkey'), mock.patch('sys.argv', [ 'progname', '-a', persona.get_pubkey() ])) as (addpub, argv): main() addpub.assert_called_with(persona.get_pubkey()) apprun.assert_not_called() # Note: because of this mock, it hides the actual app.run call w/ # a mock @mock.patch('server.MEDAServer') def test_medaserverinstanciated(self, medaserver): # that when main is run main() # that it gets called with the default storage file medaserver.assert_called_with('mediaserver.store.pasn1') @mock.patch('server.MEDAServer.store') @mock.patch('klein.Klein.run') def test_appruns(self, kleinrun, storefun): main() kleinrun.assert_called() storefun.assert_called() class _TestPostConfig(_BaseServerTest): def setUp(self): _BaseServerTest.setUp(self) persona = Persona() persona.generate_key() self.persona = persona self.medaserver.addpubkey(persona.get_pubkey()) def test_hashlookup(self): # that the hash of a file h = hashlib.sha256(open('fixtures/testfiles/test.txt').read()).hexdigest() # when looked up r = self.requests.get('/lookup/%s' % h) # returns a 404 self.assertEqual(r.status_code, 404) # but when the metadata object mdobj = self.persona.MetaData(hashes=[ h ], mimetype=[ 'text/plain' ]) # is in the server self.medaserver._storeobj(mdobj) # when looked up r = self.requests.get('/lookup/%s' % h) # it is found self.assertEqual(r.status_code, 200)