diff --git a/cfg.sample.py b/cfg.sample.py index 2c8a8adf1..a165bcb61 100644 --- a/cfg.sample.py +++ b/cfg.sample.py @@ -8,6 +8,8 @@ user_db = os.path.join(data_dir, "users") status_log_file = os.path.join(data_dir, "status.log") access_log_file = os.path.join(data_dir, "access.log") users_dir = os.path.join(data_dir, "users") +santiago = os.path.join(data_dir, "santiago.sqlite3") + product_name = "Plinth" box_name = "Freedom Plug" diff --git a/ugly_hacks/santiago/README.rst b/ugly_hacks/santiago/README.rst index 1074847ba..2272b3e3d 100644 --- a/ugly_hacks/santiago/README.rst +++ b/ugly_hacks/santiago/README.rst @@ -47,6 +47,10 @@ for the service. In a nutshell, that's the important part. There are additional details to manage, but they're implied by and built on the system above. +Each Santiago process is responsible for managing a single key and set of +friendships, so multiple Santiagi per box (each with a different purpose or +social circle) is completely possible and intended. + Our Cheats ---------- @@ -98,6 +102,8 @@ Each node contains two dictionaries/hash-tables listing (1) what they serve and who they serve it to, and (2) what services they use, who from, and where that service is located. +These are stored in the "Santiago" database, as three individual tables. + What I Host and Serve ~~~~~~~~~~~~~~~~~~~~~ @@ -108,9 +114,9 @@ These data are stored as pair of dictionaries: - The GPG ID to Service dictionary. This lists which service each user is authorized for:: - 0x0928: [ "proxy": "proxy", "wiki": "wiki", - "drinking buddy": "drinking buddy" ] - 0x7747: [ "wiki": "wiki", "proxy": "restricted_proxy" ] + 0x0928: { "proxy": "proxy", "wiki": "wiki", + "drinking buddy": "drinking buddy" } + 0x7747: { "wiki": "wiki", "proxy": "restricted_proxy" } - The Service to Location dictionary. This lists the locations each service runs on:: @@ -249,6 +255,25 @@ Note that: - Irrelevant signatures (intermediate links in the WOT) are stripped, hiding the WOT's structure from friends. +Anachronisms +~~~~~~~~~~~~ + +It's odd because this has a potential for a number of irrelevant communications. + +It's possible for A to send multiple requests to B and for B to receive multiple +requests before A acknowledges responses. + +A -> B, A -> B, B -> A, A -> B. + +Code Structure +-------------- + +Yeah, I really need to hammer this part out. Stupid MVC model. + +So, listeners receive responses and pass them up to the controller that queues +it for the responder. Lots of listeners, a single responder. Listeners have a +single method, while responders have multiple (per type of response?). + Unit Tests ========== @@ -383,6 +408,52 @@ Functional Questions messages should be queued at a file-level so that each process who needs access can have it. +:Santiago Updates: Updates are tricky things. They're when we're most + vulnerable. The question becomes: since both boxes need to know where they + are to communicate successfully, but at least one box may have changed its + location (even its Santiago), how do we handle those updates, while reducing + the vulnerability as much as possible? Let's assume that A (the requester) + changes its locations frequently, while B (the server) does not. A requests + a service from B and B then needs to reply. How does B know where to reply? + It has a few old Santiago ports left over in the database. A might also + have sent Santiago updates with the request message. How does B handle + those updates? + + Does B queue those Santiagi last in the update queue, are they checked + first, or is appending Santiagi not allowed? Each creates a different + vulnerability. + + If A's key is compromised, but his box is not, then the request is fake and + so are the new Santiagi. The old ones are still valid. + + If A's box is compromised, then his key is probably compromised too, and all + existing Santiagi are compromised. This could be A trying to transition to + a new box without changing keys, though, so the new Santiagi are valid. + + If A NAKs B's update message when A didn't ask for it, causing B to consider + that request from A (and the related Santiago) compromised, then that too + could be used by adversaries with a compromised key to deny A service. + + What a bloody circle. Both options are bad, but some worse than others? + + Well, if we prevent Santiagi updates in messages altogether, B might never + find A again, if A moved. So that sucks. But, that's also overloading + messages and implicitly allowing push-updating. If we allow pull-updating + only, then both boxes need to be accessible to one another at all times. + More secure, but a *lot* less useful. + + Is it meaningful to consider some forms of signed communication more + vulnerable than others, or are we just saying that if the communication is + successfully signed, then it must be valid, damn the consequences? I think + so, actually. Otherwise, we start jumping at shadows. There's no way to + know whether a key's been compromised until the revocation certificate is + deployed, and I can't verify anybody else's security measures. Perhaps your + definition of security is "this key I share between me, my wife, our three + kids, and the dog's neighbor." If I happen to trust the dog's neighbor + (but, oddly, not the dog itself), then I might trust the key. If I don't + trust the second of three kids, then why am I trusting the key? Trust is an + annoyingly deep subject, and one of the few good uses of the word "faith." + References ========== diff --git a/ugly_hacks/santiago/TODO b/ugly_hacks/santiago/TODO index 7c660d65c..247bc7c96 100644 --- a/ugly_hacks/santiago/TODO +++ b/ugly_hacks/santiago/TODO @@ -12,6 +12,8 @@ - Spec out the system through test harnesses. If the tests can run the system, it's complete. +- Differentiate backends and the independent system. + * Message Queuing - Process a maximum of X MB over Y requests per unit time Z per @@ -22,3 +24,16 @@ * Process Separation - Listeners aren't senders aren't controllers. + +* Actual PGP sig verification. + +* Send replies to the recipient's Santiago. + +- Queue the Santiagi and wait for a reply. If we time out before + receiving a reply, go on to the next Santiago. + +* Move Santiago data store to James's databases. + +* Implement real Santiago request/reply/response. + +- Need to move to James's database first. diff --git a/ugly_hacks/santiago/__init__.py b/ugly_hacks/santiago/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ugly_hacks/santiago/fbx/known_services.py b/ugly_hacks/santiago/fbx/known_services.py deleted file mode 100644 index 6c2725aeb..000000000 --- a/ugly_hacks/santiago/fbx/known_services.py +++ /dev/null @@ -1 +0,0 @@ -{ "plinth": { "james": [ "http://192.168.0.12:8080" ], }, } diff --git a/ugly_hacks/santiago/fbx/serving_to.py b/ugly_hacks/santiago/fbx/serving_to.py deleted file mode 100644 index a8130ae57..000000000 --- a/ugly_hacks/santiago/fbx/serving_to.py +++ /dev/null @@ -1,2 +0,0 @@ -{ "james": { "wiki": "wiki" }, - "ian": { "web": "plinth" }, } diff --git a/ugly_hacks/santiago/fbx/serving_what.py b/ugly_hacks/santiago/fbx/serving_what.py deleted file mode 100644 index e2b428d3f..000000000 --- a/ugly_hacks/santiago/fbx/serving_what.py +++ /dev/null @@ -1,2 +0,0 @@ -{ "plinth": "http://192.168.0.13:8080", - "wiki": "http://192.168.0.13/wiki", } diff --git a/ugly_hacks/santiago/fbx/settings.py b/ugly_hacks/santiago/fbx/settings.py deleted file mode 100644 index 56491a2d6..000000000 --- a/ugly_hacks/santiago/fbx/settings.py +++ /dev/null @@ -1,5 +0,0 @@ -{ "me": "nick", - "socket_port": 8080, - "max_hops": 3, - "proxy_list": ("tor") - } diff --git a/ugly_hacks/santiago/protocols/__init__.py b/ugly_hacks/santiago/protocols/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ugly_hacks/santiago/protocols/http.py b/ugly_hacks/santiago/protocols/http.py new file mode 100644 index 000000000..b9a3b7b13 --- /dev/null +++ b/ugly_hacks/santiago/protocols/http.py @@ -0,0 +1,87 @@ +import cherrypy +import santiago +from simplejson import JSONEncoder + +encoder = JSONEncoder() + + +# dirty hacks for Debian Squeeze (stable) +# ======================================= + +def fix_old_cherrypy(): + """Make Squeeze's CherryPy forward-compatible.""" + + for y in range(0,3): + for x in range(0, 7): + print "WARNING", + print "" + + print("You're using an old CherryPy version! We're faking it!") + print("Expect the unexpected! Raar!") + + def jsonify_tool_callback(*args, **kwargs): + response = cherrypy.response + response.headers['Content-Type'] = 'application/json' + response.body = encoder.iterencode(response.body) + + cherrypy.tools.json_out = cherrypy.Tool('before_finalize', jsonify_tool_callback, priority=30) + +if cherrypy.__version__ < "3.2": + fix_old_cherrypy() + + +# actual HTTP Santiago classes. +# ============================= + +class SantiagoHttpListener(santiago.SantiagoListener): + """Listens for connections on the HTTP protocol.""" + + DEFAULT_RESPONSE = """\ +Use it right. + +

Nice try, now try again with a request like:

+

http://localhost:8080/santiago/(requester)/(server)/(service)

+ +
+
requster
james, ian
+
server
nick
+
service
wiki, web
+
+ +

This'll get you good results:

+ + http://localhost:8080/santiago/james/nick/wiki + +

See the serving_to, serving_what, and + me variables.

+ +""" + + def __init__(self, instance, port=8080): + super(SantiagoHttpListener, self).__init__(instance) + self.socket_port = port + + @cherrypy.expose + @cherrypy.tools.json_out() + def serve(self, key=None, service=None, server=None, hops=3, santiagi=None): + """Handles an incoming request.""" + + return super(SantiagoHttpListener, self).serve( + key, service, server, hops, santiagi) + + @cherrypy.expose + def index(self): + """Do nothing, unless we're debugging.""" + + if santiago.DEBUG: + return self.DEFAULT_RESPONSE + + +class SantiagoHttpSender(santiago.SantiagoSender): + """Responsible for answering HTTP requests.""" + + @cherrypy.tools.json_out() + def proxy(self, key, service, server, hops=3): + """Forwards on a request to another Santiago.""" + + return super(SantiagoHttpSender, self).proxy(key, service, server, hops) diff --git a/ugly_hacks/santiago/santiago.py b/ugly_hacks/santiago/santiago.py index 4ce24280c..2049bbbb0 100644 --- a/ugly_hacks/santiago/santiago.py +++ b/ugly_hacks/santiago/santiago.py @@ -20,178 +20,149 @@ or later. A copy of GPLv3 is available [from the Free Software Foundation] """ -# If you're crazy like me, you can turn this script into its own -# documentation by running:: -# -# python pylit.py -c backup - | rst2html > backup.html -# -# You'll need PyLit_ and ReStructuredText_ for this to work correctly. -# -# .. _pylit: http://pylit.berlios.de/ -# .. _restructuredtext: http://docutils.sourceforge.net/rst.html -# -# .. contents:: +# debug hacks +# =========== -import cherrypy -import os -from simplejson import JSONEncoder - - -DEBUG = 0 -encoder = JSONEncoder() - -# random setup tools -# ================== +DEBUG = 1 if DEBUG: - for x in range(0, 3): - for y in range(0, 7): - print "WARNING", - print "" - print "You're in DEBUG MODE! You are surprisingly vulnerable! Raar!" + """A few hacks to make testing easier.""" -def fix_old_cherrypy(): - """Make Lenny's CherryPy forward-compatible.""" + def cfg_hack(): + import sys + sys.path.append("../../") + import cfg - for x in range(0,3): - for y in range(0, 7): - print "WARNING", - print "" + def ohnoes(): + for y in range(0, 3): + for x in range(0, 7): + print "WARNING", + print "" + print "You're in DEBUG MODE! You are surprisingly vulnerable! Raar!" - print("You're using an old CherryPy version! We're faking it!") - print("Expect the unexpected! Raar!") - - def jsonify_tool_callback(*args, **kwargs): - response = cherrypy.response - response.headers['Content-Type'] = 'application/json' - response.body = encoder.iterencode(response.body) - - cherrypy.tools.json_out = cherrypy.Tool('before_finalize', jsonify_tool_callback, priority=30) - -if cherrypy.__version__ < "3.2": - fix_old_cherrypy() + ohnoes() + cfg_hack() -# actual Santiago -# =============== +# normal imports +# ============== + +from collections import defaultdict as DefaultDict +import util + class Santiago(object): """Santiago's base class, containing listener and sender defaults.""" - DEFAULT_RESPONSE = """\ -Use it right. - -

Nice try, now try again with a request like:

-

http://localhost:8080/santiago/(gpgId)/(service)/(server)

- -
-
gpgId
james, ian
-
service
wiki, web
-
server
nick
-
- -

This'll get you good results:

- - http://localhost:8080/santiago/james/wiki/nick - -

See the serving_to, serving_what, and - me variables.

- -""" - def __init__(self, instance): - self.load_instance(instance) + """Initializes the Santiago service. - # TODO Does the listener need to know what services others are running? - # TODO Does the sender need to know what services I'm running? - self.load_serving_to() - self.load_serving_what() - self.load_known_services() + instance is the PGP key this Santiago service is responsible for. + + Each service contains one or more senders and listeners, primarily + divided by protocol, all pulling from and adding to the same pool of + services. + + Each Santiago keeps track of the services it hosts, and other servers' + Santiago services. A Santiago has no idea of and is not responsible for + anybody else's services. + + """ + self.instance = instance + self.hosting = self.load_dict("hosting") + self.keys = self.load_dict("keys") + self.servers = self.load_dict("servers") + + self.listeners = list() + self.senders = list() + + # load settings by name + settings = self.load_dict("settings") + for key in ("socket_port", "max_hops", "proxy_list"): + setattr(self, key, settings[key] if key in settings else None) + + def load_dict(self, name): + """Loads a dictionary from file.""" + + # FIXME: figure out the threading issue. + #return util.filedict_con("%s_%s " % (cfg.santiago, self.instance), name) + return { + "hosting": DefaultDict(list), + "keys": DefaultDict(list), + "servers": DefaultDict(lambda: DefaultDict(list)), + "settings": DefaultDict(None) + }[name] def am_i(self, server): - return str(self.me) == str(server) + """Hello? Is it me you're looking for?""" + + return self.instance == server - def load_instance(self, instance): - """Load instance settings from a file. + # Server-related tags + # ------------------- - A terrible, unforgivable hack. But it's a pretty effective prototype, - allowing us to add and remove attributes really easily. + def provide_at_location(self, service, location): + """Serve service at location. + + post:: + + location in cfg.santiago.hosting[service] """ - settings = run_file("%s%ssettings.py" % (instance, os.sep)) - for key, value in settings.iteritems(): - setattr(self, key, value) + self.hosting[service].append(location) - self.instance = instance + def provide_for_key(self, service, key): + """Serve service for user. - # FIXME I need to handle instance vs controller correctly. this is the - # wrong place. - # - # I'm putting instance data into the controller, which is nutters. Too - # tired to fix tonight, though. - # - # These data should probably be loaded in the server and listener, - # respectively, but I don't know whether one needs to know about the other's - # services. + post:: - def load_serving_to(self): - """Who can see which of my services?""" - - self.serving_to = run_file("%s%sserving_to.py" % (self.instance, - os.sep)) - def load_serving_what(self): - """What location does that service translate to?""" - - self.serving_what = run_file("%s%sserving_what.py" % (self.instance, - os.sep)) - def load_known_services(self): - """What services do I know of?""" - - self.known_services = run_file("%s%sknown_services.py" % (self.instance, - os.sep)) - @cherrypy.expose - def index(self): - """Do nothing, unless we're debugging.""" - - if DEBUG: - return DEFAULT_RESPONSE - - -class SantiagoListener(Santiago): - """Listens for requests on the santiago port.""" - - def __init__(self, instance, port=8080): - super(SantiagoListener, self).__init__(instance=instance) - - self.socket_port = port - - @cherrypy.expose - @cherrypy.tools.json_out() - def santiago(self, from_id=None, to_id=None, service=None, hops=0): #, new_santiago_id=""): - """Handles an incoming request. - - FIXME: handle new Santiago ID list. + service in cfg.santiago.keys[key] """ - message = { "requester": from_id, - "server": to_id, - "service": service, - "hops": hops, } - #"santiago": new_santiago_id } + self.keys[key].append(service) - # FIXME: this is being dumb and not working how I expect it. later. - # if not self.i_am(message["server"]): - # return self.proxy_santiago_request(message) + # client-related tags + # ------------------- - try: - return self.serving_what[ - self.serving_to[message["requester"]][message["service"]]] - except KeyError: - # TODO: handle responses. should a fail just timeout? - return None + def learn_service(self, service, key, locations): + """Learn a service to use, as a client. - @cherrypy.tools.json_out() - def proxy_santiago_request(self, message, hops=3): + post:: + + forall(locations, lambda x: x in cfg.santiago.servers[service][key]) + + """ + self.servers[service][key] += locations + + def consume_service(self, service, key): + return self.servers[service][key] + + def add_listener(self, listener): + """Registers a protocol-specific listener.""" + + self.listeners.append(listener) + + def add_sender(self, sender): + """Registers a sender.""" + + self.senders.append(sender) + + # processing related tags + # ----------------------- + + def serve(self, key, service, server, hops, santiagi): + """Provide a requested service to a client.""" + + if santiagi is not None: + self.learn_service("santiago", key, santiagi) + + if not self.am_i(server): + return self.proxy(key, service, server, hops=hops) + + if service in self.keys[key]: + return self.hosting[service] + + def proxy(self, key, service, server, hops=3): """Passes a Santiago request off to another known host. We're trying to search the friend list for the target server. @@ -203,20 +174,27 @@ class SantiagoListener(Santiago): if (hops < 1): return - # this counts as a hop. hops -= 1 - # TODO pull this off, another day. - return str(message) + # TODO pick the senders more intelligently. + return self.senders[0].proxy(key, service, server, hops) -class SantiagoSender(Santiago): - """Sendss the Santiago request to a Santiago service.""" +class SantiagoListener(object): + """Listens for requests on the santiago port.""" - def __init__(self, instance, proxy): - super(SantiagoSender, self).__init__(instance=instance) + def __init__(self, santiago): + self.santiago = santiago - self.proxy = proxy if proxy in self.proxy_list else None + def serve(self, key, service, server, hops, santiagi): + return self.santiago.serve(key, service, server, hops, santiagi) + + +class SantiagoSender(object): + """Sends the Santiago request to a Santiago service.""" + + def __init__(self, santiago): + self.santiago = santiago def request(self, destination, resource): """Sends a request for a resource to a known Santiago. @@ -231,7 +209,8 @@ class SantiagoSender(Santiago): - Other Santiago listeners. - An action. - post: + post:: + not (__return__["destination"] is None) not (__return__["service"] is None) # TODO my request is signed with my GPG key, recipient encrypted. @@ -239,24 +218,64 @@ class SantiagoSender(Santiago): """ pass # TODO: do. + def nak(self): + """Denies a requested resource to a Santiago. -# utility functions -# ================= + No reason is given. All the recipient knows is that the host did not + have that resource for that client. -def run_file(filename): - """Returns the result of executing the Python file. Terrible idea. Effective - hack. + """ + pass - TODO: Replace this with James's database stuff! + def ack(self): + """A successful reply to a Santiago request. - If you try to use this in the wild, I will hunt you down and cut you. + The response must include: - """ - with open(filename) as in_file: - return eval("".join(in_file.readlines())) + - A server. + + The response may include: + + - The Santiago listener that received and accepted the request. + + """ + pass + + def end(self): + """Sent by the original requester, when it receives the server's + response, telling the server it needs to send no more responses. + + Sent to the Santiago that first received the request. + + """ + pass + + def proxy(self, key, service, server, hops): + """Sends the request to another server.""" + + # TODO pull this off, another day. + return (self.santiago.instance +" is not %(server)s. proxying request. " + + "%(key)s is requesting the %(service)s from %(server)s. " + + "%(hops)d hops remain.") % locals() if __name__ == "__main__": - santiago = SantiagoListener("fbx") + import cherrypy + import sys + sys.path.append(".") + from protocols.http import SantiagoHttpListener, SantiagoHttpSender - cherrypy.quickstart(santiago) + # build the Santiago + santiago = Santiago("nick") + http_listener = SantiagoHttpListener(santiago) + http_sender = SantiagoHttpSender(santiago) + santiago.add_listener(http_listener) + santiago.add_sender(http_sender) + + # TODO move this into the table loading. + santiago.provide_at_location("wiki", "192.168.0.13") + santiago.provide_for_key("wiki", "james") + santiago.max_hops = 3 + santiago.proxy_list = ("tor") + + cherrypy.quickstart(http_listener)