mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
A bunch of new tests and changes to support them.
This commit is contained in:
parent
9dca3ed02e
commit
88d35a83ca
@ -303,6 +303,9 @@ class Santiago(object):
|
||||
The request comes in encrypted and it's decrypted here. If I can't
|
||||
decrypt it, it's not for me. If it has no signature, I don't want it.
|
||||
|
||||
Some lists are changed to sets here. This allows for set-operations
|
||||
(union, intersection, etc) later, making things much more intuitive.
|
||||
|
||||
"""
|
||||
request = self.gpg.decrypt(request)
|
||||
|
||||
@ -310,23 +313,51 @@ class Santiago(object):
|
||||
if not (str(request) and request.fingerprint):
|
||||
return
|
||||
|
||||
# copy required keys from dictionary
|
||||
adict = ast.literal_eval(str(request))
|
||||
# copy out only required keys from request, throwing away cruft
|
||||
request_body = dict()
|
||||
source = ast.literal_eval(str(request))
|
||||
try:
|
||||
for key in ("host", "client", "service", "locations", "reply_to"):
|
||||
request_body = adict[key]
|
||||
for key in Santiago.ALL_KEYS:
|
||||
request_body[key] = source[key]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# required keys are non-null
|
||||
if None in [request_body[x] for x in Santiago.REQUIRED_KEYS]:
|
||||
return
|
||||
|
||||
# move lists to sets
|
||||
request_body = self.setify_lists(request_body)
|
||||
if not request_body:
|
||||
return
|
||||
|
||||
# set implied keys
|
||||
request_body["from"] = request.fingerprint
|
||||
reqeust_body["to"] = self.me
|
||||
request_body["to"] = self.me
|
||||
|
||||
return request_body
|
||||
|
||||
def setify_lists(self, request_body):
|
||||
"""Convert list nodes to sets."""
|
||||
|
||||
try:
|
||||
for key in ("locations", "reply_to"):
|
||||
if request_body[key] is not None:
|
||||
request_body[key] = set(request_body[key])
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
try:
|
||||
for key in ("reply_versions",):
|
||||
request_body[key] = set(request_body[key])
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
return request_body
|
||||
|
||||
|
||||
def handle_request(self, from_, to, host, client,
|
||||
service, reply_to):
|
||||
service, reply_to, request_version, reply_versions):
|
||||
"""Actually do the request processing.
|
||||
|
||||
- Verify we're willing to host for both the client and proxy. If we
|
||||
|
||||
@ -473,7 +473,7 @@ class VerifyRequest(unittest.TestCase):
|
||||
"reply_versions": [1],
|
||||
"request": None }
|
||||
|
||||
def test_pass_acceptable_request(self):
|
||||
def test_valid_message(self):
|
||||
"""A known good request passes."""
|
||||
|
||||
self.assertTrue(self.santiago.verify_request(self.request))
|
||||
@ -512,54 +512,150 @@ class VerifyRequest(unittest.TestCase):
|
||||
self.assertFalse(self.santiago.verify_request(self.request))
|
||||
|
||||
|
||||
class UnpackRequest(test_pgpprocessor.MessageWrapper):
|
||||
class UnpackRequest(unittest.TestCase):
|
||||
|
||||
"""Are requests unpacked as expected?
|
||||
|
||||
- Messages that aren't for me (that I can't decrypt) are ignored.
|
||||
- Messages with invalid signatures are rejected.
|
||||
- The request keys are unpacked correctly:
|
||||
- Only passing messages return the dictionary.
|
||||
- The message is unpacked correctly. This is a bit difficult because of the
|
||||
number of overlapping data types.
|
||||
|
||||
First, we have the keys that must be present in each message:
|
||||
|
||||
- client
|
||||
- host
|
||||
- service
|
||||
- locations
|
||||
- reply_to
|
||||
- request_version
|
||||
- reply_versions
|
||||
|
||||
- Only passing messages return the dictionary.
|
||||
Next the list-keys which must be lists (they'll later be converted
|
||||
directly to sets):
|
||||
|
||||
- reply_to
|
||||
- locations
|
||||
- reply_versions
|
||||
|
||||
Finally, we have the keys that may be empty:
|
||||
|
||||
- locations
|
||||
- reply_to
|
||||
|
||||
``locations`` is empty on an incoming (request) message, while
|
||||
``reply_to`` may be assumed if the reply destinations haven't changed
|
||||
since the previous message. If they have, and the client still doesn't
|
||||
send the reply_to, then the host will be unable to communicate with it, so
|
||||
it's in the client's best interests to send it whenever reasonable.
|
||||
|
||||
So, the structure of a message is a little weird here. We have three sets
|
||||
of overlapping requirements:
|
||||
|
||||
#. Certain keys must be present.
|
||||
#. Certain keys must be lists.
|
||||
#. Certain keys may be unset.
|
||||
|
||||
The really odd ones out are "locations" and "reply_to", which fall into
|
||||
all three categories.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
"""Create a request."""
|
||||
|
||||
self.gpg = gnupg.GPG(use_agent = True)
|
||||
|
||||
self.request = { "host": None, "client": None,
|
||||
"service": None, "reply_to": None,
|
||||
"locations": None }
|
||||
|
||||
config = configparser.ConfigParser(
|
||||
{"KEYID":
|
||||
"D95C32042EE54FFDB25EC3489F2733F40928D23A"})
|
||||
config.read(["test.cfg"])
|
||||
self.keyid = config.get("pgpprocessor", "keyid")
|
||||
self.keyid = utilities.load_config().get("pgpprocessor", "keyid")
|
||||
|
||||
self.santiago = santiago.Santiago()
|
||||
|
||||
self.request = { "host": self.keyid, "client": self.keyid,
|
||||
"service": "santiago", "reply_to": [1],
|
||||
"locations": [1],
|
||||
"request_version": 1, "reply_versions": [1], }
|
||||
|
||||
self.ALL_KEYS = ("host", "client", "service", "locations", "reply_to",
|
||||
"request_version", "reply_versions")
|
||||
self.REQUIRED_KEYS = ("client", "host", "service",
|
||||
"request_version", "reply_versions")
|
||||
self.OPTIONAL_KEYS = ("locations", "reply_to")
|
||||
self.LIST_KEYS = ("reply_to", "locations", "reply_versions")
|
||||
|
||||
def test_valid_message(self):
|
||||
"""A message that should pass does pass normally."""
|
||||
|
||||
self.fail()
|
||||
|
||||
def test_request_contains_all_keys(self):
|
||||
"""The test request needs all supported keys."""
|
||||
|
||||
for key in self.ALL_KEYS:
|
||||
self.assertTrue(key in self.request)
|
||||
|
||||
def wrap_message(self, message):
|
||||
"""The standard wrapping method for these tests."""
|
||||
|
||||
return str(self.gpg.encrypt(str(message),
|
||||
recipients=[self.keyid],
|
||||
sign=self.keyid))
|
||||
|
||||
def test_key_lists_updated(self):
|
||||
"""Are the lists of keys up-to-date?"""
|
||||
|
||||
for key in ("ALL_KEYS", "REQUIRED_KEYS", "OPTIONAL_KEYS", "LIST_KEYS"):
|
||||
self.assertEqual(getattr(self, key),
|
||||
getattr(santiago.Santiago, key))
|
||||
|
||||
def test_all_keys_accounted_for(self):
|
||||
"""All the keys in the ALL_KEYS list are either required or optional."""
|
||||
|
||||
self.assertEqual(set(self.ALL_KEYS),
|
||||
set(self.REQUIRED_KEYS) | set(self.OPTIONAL_KEYS))
|
||||
|
||||
def test_requred_keys_are_required(self):
|
||||
"""If any required keys are missing, the message is skipped."""
|
||||
|
||||
for key in ("host", "client", "service", "reply_to", "locations"):
|
||||
del self.request[key]
|
||||
|
||||
encrypted_data = self.gpg.encrypt(str(self.request),
|
||||
recipients=[self.keyid],
|
||||
sign=self.keyid)
|
||||
for key in self.ALL_KEYS:
|
||||
broken_dict = dict(self.request)
|
||||
del broken_dict[key]
|
||||
encrypted_data = self.wrap_message(str(broken_dict))
|
||||
|
||||
self.assertEqual(
|
||||
self.santiago.unpack_request(str(encrypted_data)),
|
||||
None)
|
||||
|
||||
def test_non_null_keys_are_set(self):
|
||||
"""If any keys that can't be empty are empty, the message is skipped."""
|
||||
|
||||
for key in self.REQUIRED_KEYS:
|
||||
broken_dict = dict(self.request)
|
||||
broken_dict[key] = None
|
||||
encrypted_data = self.wrap_message(str(broken_dict))
|
||||
|
||||
self.assertEqual(
|
||||
self.santiago.unpack_request(str(encrypted_data)),
|
||||
None)
|
||||
|
||||
def test_null_keys_are_null(self):
|
||||
"""If any optional keys are null, the message's still processed."""
|
||||
|
||||
for key in self.OPTIONAL_KEYS:
|
||||
broken_dict = dict(self.request)
|
||||
broken_dict[key] = None
|
||||
encrypted_data = str(self.wrap_message(str(broken_dict)))
|
||||
|
||||
# convert non-None elements to sets, like unpack does.
|
||||
broken_dict.update(dict([ (k, set(broken_dict[k])) for
|
||||
k in self.LIST_KEYS
|
||||
if broken_dict[k] is not None ]))
|
||||
broken_dict.update({ "from": self.keyid,
|
||||
"to": 0 })
|
||||
|
||||
self.assertEqual(
|
||||
self.santiago.unpack_request(encrypted_data),
|
||||
broken_dict)
|
||||
|
||||
def test_skip_undecryptable_messages(self):
|
||||
"""Mesasges that I can't decrypt (for other folks) are skipped.
|
||||
|
||||
@ -571,7 +667,7 @@ class UnpackRequest(test_pgpprocessor.MessageWrapper):
|
||||
def test_skip_invalid_signatures(self):
|
||||
"""Messages with invalid signatures are skipped."""
|
||||
|
||||
self.request = str(self.gpg.sign(str(self.request), keyid=self.keyid))
|
||||
self.request = self.wrap_message(str(self.request))
|
||||
|
||||
# delete the 7th line for the fun of it.
|
||||
mangled = self.request.splitlines(True)
|
||||
@ -580,6 +676,27 @@ class UnpackRequest(test_pgpprocessor.MessageWrapper):
|
||||
|
||||
self.assertEqual(self.santiago.unpack_request(self.request), None)
|
||||
|
||||
def test_incoming_lists_are_lists(self):
|
||||
"""Any variables that must be lists, before processing, actually are."""
|
||||
|
||||
for key in self.LIST_KEYS:
|
||||
broken_request = dict(self.request)
|
||||
broken_request[key] = 1
|
||||
broken_request = self.wrap_message(str(broken_request))
|
||||
|
||||
self.assertEqual(self.santiago.unpack_request(broken_request), None)
|
||||
|
||||
def test_sets_are_sets(self):
|
||||
"""Any variables that must be sets, after processing, actually are."""
|
||||
|
||||
self.request = self.wrap_message(str(self.request))
|
||||
|
||||
unpacked = self.santiago.unpack_request(self.request)
|
||||
|
||||
for key in self.LIST_KEYS:
|
||||
for attribute in ("union", "intersection"):
|
||||
self.assertTrue(hasattr(unpacked[key], attribute))
|
||||
|
||||
class HandleRequest(unittest.TestCase):
|
||||
"""Process an incoming request, from a client, for to host services.
|
||||
|
||||
@ -591,29 +708,75 @@ class HandleRequest(unittest.TestCase):
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.santiago = santiago.Santiago(hosting = {})
|
||||
self.santiago.outgoing_request = (lambda **x: self.call_request())
|
||||
self.santiago.requested = False
|
||||
"""Do a good bit of setup to make this a nicer test-class.
|
||||
|
||||
Successful tests will call ``Santiago.outgoing_request``, so that's
|
||||
overridden to record that the method is called.
|
||||
|
||||
"""
|
||||
self.keyid = utilities.load_config().get("pgpprocessor", "keyid")
|
||||
|
||||
self.santiago = santiago.Santiago(
|
||||
hosting = {self.keyid: {"santiago": set([1]) }},
|
||||
consuming = {"santiago": {self.keyid: set([1]) }},
|
||||
me = self.keyid)
|
||||
|
||||
self.santiago.requested = False
|
||||
self.santiago.outgoing_request = (lambda *args, **kwargs:
|
||||
self.record_success())
|
||||
|
||||
self.from_ = self.keyid
|
||||
self.to = self.keyid
|
||||
self.host = self.keyid
|
||||
self.client = self.keyid
|
||||
self.service = "santiago"
|
||||
self.reply_to = set([1])
|
||||
self.request_version = 1
|
||||
self.reply_versions = set([1])
|
||||
|
||||
def record_success(self):
|
||||
"""Record that we tried to reply to the request."""
|
||||
|
||||
def call_request(self):
|
||||
self.santiago.requested = True
|
||||
|
||||
def test_call(self):
|
||||
"""A short-hand for calling handle_request with all 8 arguments. Oy."""
|
||||
|
||||
self.santiago.handle_request(
|
||||
self.from_, self.to,
|
||||
self.host, self.client,
|
||||
self.service, self.reply_to,
|
||||
self.request_version, self.reply_versions)
|
||||
|
||||
def test_valid_message(self):
|
||||
"""Reply to valid messages."""
|
||||
|
||||
self.test_call()
|
||||
|
||||
self.assertTrue(self.santiago.requested)
|
||||
|
||||
def test_unwilling_client(self):
|
||||
"""Don't handle the request if the cilent isn't trusted."""
|
||||
|
||||
self.santiago.handle_request()
|
||||
self.client = 0
|
||||
|
||||
self.test_call()
|
||||
|
||||
self.assertFalse(self.santiago.requested)
|
||||
|
||||
def test_unwilling_proxy(self):
|
||||
"""Don't handle the request if the proxy isn't trusted."""
|
||||
|
||||
self.fail()
|
||||
|
||||
self.assertFalse(self.santiago.requested)
|
||||
|
||||
def test_learn_services(self):
|
||||
"""New reply_to locations are learned."""
|
||||
|
||||
self.fail()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user