diff --git a/ugly_hacks/santiago/README.rst b/ugly_hacks/santiago/README.rst index 923b73cf5..a2ace4040 100644 --- a/ugly_hacks/santiago/README.rst +++ b/ugly_hacks/santiago/README.rst @@ -473,6 +473,52 @@ Functional Questions 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." +Using Santiago +============== + +The Santiago service is mostly working now. I still need to add PGP encryption, +signing, decryption, and verification (and handle all the ``FIXME``\s and +``TODO``\s in the code). + +First, you'll need to create a certificate to serve Santiago over HTTPS:: + + # make-ssl-cert generate-default-snakeoil + # make-ssl-cert /usr/share/ssl-cert/ssleay.cnf santiago.crt + # chgrp 1000 santiago.crt + # chmod g+r santiago.crt + +Next, you'll need to open up "start.py" and update the system path to locate the +"gnupg_" and "cfg" modules. + +.. _gnupg: https://code.google.com/p/python-gnupg/ + +Finally, start the Santiago process in a console with ``start.py``. Test it out +by navigating to: + + https://localhost:8080/query?to=b&service=santiago + +You should see three requests appear in the console. + +Tasks +----- + +- |TODO| TODOs and FIXMEs. + +- |TODO| PGP Signing and Verification. + +- |TODO| Verify I successfully split into protocol-specific listeners and + senders. + +- |TODO| add unit tests and doctests + +- |TODO| Create startup script that adds all necessary things to the PYTHONPATH. + +- |TODO| allow multiple listeners and senders per protocol (with different + proxies?) + +.. |TODO| unicode:: U+2610 +.. |DONE| unicode:: U+2611 + References ========== diff --git a/ugly_hacks/santiago/protocols/https.py b/ugly_hacks/santiago/protocols/https.py new file mode 100644 index 000000000..37e99cba3 --- /dev/null +++ b/ugly_hacks/santiago/protocols/https.py @@ -0,0 +1,83 @@ +"""The HTTPS Santiago listener and sender.""" + +from simple_santiago import SantiagoListener, SantiagoSender + +import cherrypy +import httplib, urllib +import sys + + +class Listener(SantiagoListener): + + def __init__(self, santiago, socket_port=0, + ssl_certificate="", ssl_private_key=""): + + super(SantiagoListener, self).__init__(santiago) + + cherrypy.server.socket_port = socket_port + cherrypy.server.ssl_certificate = ssl_certificate + cherrypy.server.ssl_private_key = ssl_private_key + cherrypy.Application(self, "/") + + def start(self): + """Starts the listener.""" + + # TODO: integrate multiple servers: + # http://docs.cherrypy.org/dev/refman/process/servers.html + # cherrypy.engine.start() + cherrypy.quickstart(self) + + @cherrypy.expose + def index(self, **kwargs): + """Receive an incoming Santiago request from another Santiago client.""" + + self.incoming_request(**kwargs) + + @cherrypy.expose + def query(self, host, service): + """Request a resource from another Santiago client. + + TODO: add request whitelisting. + + """ + if not cherrypy.request.remote.ip.startswith("127.0.0"): + return + + self.santiago.query(host, service) + + @cherrypy.expose + def save_server(self): + if not cherrypy.request.remote.ip.startswith("127.0.0"): + return + + self.santiago.save_server() + +class Sender(SantiagoSender): + + def __init__(self, santiago, proxy_host, proxy_port): + + super(SantiagoSender, self).__init__(santiago) + self.proxy_host = proxy_host + self.proxy_port = proxy_port + + def outgoing_request(self, request, destination): + """Send an HTTPS request to each Santiago client. + + Don't queue, just immediately send the reply to each location we know. + + It's both simple and as reliable as possible. + + """ + params = urllib.urlencode(request) + + # TODO: Does HTTPSConnection require the cert and key? + # Is the fact that the server has it sufficient? I think so. + connection = httplib.HTTPSConnection(destination.split("//")[1]) + + # proxying required and available only in Python 2.7 or later. + # TODO: fail if Python version < 2.7. + if sys.version_info >= (2, 7): + connection.set_tunnel(self.proxy_host, self.proxy_port) + + connection.request("GET", "/?%s" % params) + connection.close() diff --git a/ugly_hacks/santiago/simple_santiago.py b/ugly_hacks/santiago/simple_santiago.py index d846e24e6..b65eeadce 100644 --- a/ugly_hacks/santiago/simple_santiago.py +++ b/ugly_hacks/santiago/simple_santiago.py @@ -29,14 +29,16 @@ We also don't: - Use a reasonable data-store. - Have a decent control mechanism. -FIXME: Split into protocol-specific listeners and senders. -FIXME: add that whole pgp thing. -FIXME: remove @cherrypy.expose from everything but index. -TODO: add doctests +:FIXME: add that whole pgp thing. +:TODO: add doctests +:TODO: Create startup script that adds all necessary things to the PYTHONPATH. +:FIXME: allow multiple listeners and senders per protocol (with different + proxies) """ from collections import defaultdict as DefaultDict import gnupg +import logging import sys try: @@ -88,39 +90,64 @@ class SimpleSantiago(object): hosts are unreachable from some points. """ - self.senders = senders self.hosting = hosting self.consuming = consuming self.requests = DefaultDict(set) - self.listeners = listeners - - self._create_listeners() self.me = me - def _create_listeners(self): - """Iterates through each known protocol creating listeners for all.""" + self.listeners = self._create_connectors(listeners, "Listener") + self.senders = self._create_connectors(senders, "Sender") - # FIXME: Split into protocol-specific listeners and senders. + def _create_connectors(self, settings, connector): + """Iterates through each protocol given, creating connectors for all. - # def configure_gui(self): - # """Launch the gui specified in the launcher's config files. - # - # """ - # gui_name = self.config.get(self.META_DATA, "gui") - # import_name = "guis.%(gui_name)s.gemrb_gui_%(gui_name)s" % locals() - # __import__(import_name) # TODO import every gui in the gui dir until one succeeds - # - # gui_module = sys.modules[import_name] - # - # self.gui = gui_module.Gui(self) + This assumes that the caller correctly passes parameters for each + connector. If not, we log a TypeError and continue to serve any + connectors we can create successfully. If other types of errors occur, + we quit. - for protocol in self.listeners.iterkeys(): - method = "_create_%s_listener" % protocol + """ + connectors = dict() + + for protocol in settings.iterkeys(): + module = SimpleSantiago._get_protocol_module(protocol) try: - getattr(self, method)(**self.listeners[protocol]) - except KeyError: - pass + connectors[protocol] = \ + getattr(module, connector)(self, **settings[protocol]) + + # log a type error, assume all others are fatal. + except TypeError: + logging.error("Could not create %s %s with %s", + protocol, connector, str(settings[protocol])) + + return connectors + + @classmethod + def _get_protocol_module(cls, protocol): + """Return the requested protocol module. + + FIXME: Assumes the current directory is in sys.path + + """ + import_name = "protocols." + protocol + + if not import_name in sys.modules: + __import__(import_name) + + return sys.modules[import_name] + + def start(self): + """Start all listeners and senders attached to this Santiago. + + When this has finished, the Santiago will be ready to go. + + """ + for connector in list(self.listeners.itervalues()) + \ + list(self.senders.itervalues()): + connector.start() + + logging.debug("Santiago started!") def am_i(self, server): """Verify whether this server is the specified server.""" @@ -147,16 +174,16 @@ class SimpleSantiago(object): """ try: return self.hosting[client][service] - except KeyError: - pass + except KeyError as e: + logging.exception(e) def get_client_locations(self, host, service): """Return where the host serves the service for me, the client.""" try: return self.consuming[service][host] - except KeyError: - pass + except KeyError as e: + logging.exception(e) def query(self, host, service): """Request a service from another Santiago. @@ -164,10 +191,14 @@ class SimpleSantiago(object): This tag starts the entire Santiago request process. """ - self.requests[host].add(service) + try: + self.requests[host].add(service) - self.request(host, self.me, host, self.me, - service, None, self.get_client_locations(host, "santiago")) + self.outgoing_request( + host, self.me, host, self.me, + service, None, self.get_client_locations(host, "santiago")) + except Exception as e: + logging.exception("Couldn't handle %s.%s", host, service) def outgoing_request(self, from_, to, host, client, service, locations, reply_to): @@ -179,13 +210,14 @@ class SimpleSantiago(object): # best guess reply_to if we don't know. reply_to = reply_to or self.get_host_locations(to, "santiago") - request = self.gpg.sign_encrypt({ - "from": from_, "to": to, "host": host, "client": client, - "service": service, "locations": locations or "", - "reply_to": reply_to}) + # FIXME: pgp sign + encrypt here. + request = {"from": from_, "to": to, "host": host, "client": client, + "service": service, "locations": locations or "", + "reply_to": reply_to} for destination in self.get_client_locations(to, "santiago"): - getattr(self, destination.split(":")[0] + "_request")(request) + protocol = destination.split(":")[0] + self.senders[protocol].outgoing_request(request, destination) def incoming_request(self, **kwargs): """Provide a service to a client. @@ -205,9 +237,13 @@ class SimpleSantiago(object): attacker knows that the last request brought down a system. """ + logging.debug("Incoming request: ", str(kwargs)) + # no matter what happens, the sender will never hear about it. try: - request = self.unpack_request(kwargs) + request = self._unpack_request(kwargs) + + logging.debug("Unpacked request: ", str(request)) # is this appropriate for both sending and receiving? # nope. @@ -220,11 +256,10 @@ class SimpleSantiago(object): self.handle_request(request["from"], request["to"], request["host"], request["client"], request["service"], request["reply_to"]) - except Exception, e: - #raise e - print "Exception!", e + except Exception as e: + logging.exception("Error: ", str(e)) - def unpack_request(self, kwargs): + def _unpack_request(self, kwargs): """Decrypt and verify the request. Give up if it doesn't pass muster. @@ -254,7 +289,7 @@ class SimpleSantiago(object): try: self.hosting[from_] self.hosting[client] - except KeyError: + except KeyError as e: return if not self.am_i(to): @@ -265,9 +300,10 @@ class SimpleSantiago(object): else: self.learn_service(client, "santiago", reply_to) - self.request(self.me, client, self.me, client, - service, self.get_host_locations(client, service), - self.get_host_locations(client, "santiago")) + self.outgoing_request( + self.me, client, self.me, client, + service, self.get_host_locations(client, service), + self.get_host_locations(client, "santiago")) def proxy(self, to, host, client, service, reply_to): """Pass off a request to another Santiago. @@ -276,12 +312,14 @@ class SimpleSantiago(object): original host as well as me. TODO: add tests. + TODO: improve proxying. """ - self.request(self.me, to, host, client, - service, reply_to) - self.request(self.me, to, host, client, - service, self.get_client_locations(host, "santiago")) + self.outgoing_request(self.me, to, host, client, + service, reply_to) + self.outgoing_request( + self.me, to, host, client, + service, self.get_client_locations(host, "santiago")) def handle_reply(self, from_, to, host, client, service, locations, reply_to): @@ -295,7 +333,7 @@ class SimpleSantiago(object): try: self.consuming[service][from_] self.consuming[service][host] - except KeyError: + except KeyError as e: return if not self.am_i(to): @@ -323,64 +361,73 @@ class SimpleSantiago(object): try: with open(name, "w") as output: output.write(str(getattr(self, datum))) - except: - pass + except Exception as e: + logging.exception("Could not save %s as %s", datum, name) -class SantiagoListener(object): - """Generic Santiago Listener superclass.""" +class SantiagoConnector(object): + """Generic Santiago connector superclass. + All types of connectors should inherit from this class. These are the + "controllers" in the MVC paradigm. + + """ def __init__(self, santiago): self.santiago = santiago -class SantiagoSender(object): - """Generic Santiago Sender superclass.""" + def start(self): + """Called when initialization is complete. - def __init__(self, santiago): - self.santiago = santiago + Cannot block. + """ + pass + +class SantiagoListener(SantiagoConnector): + """Generic Santiago Listener superclass. + + This class contains one optional method, the request receiving method. This + method passes the request along to the Santiago host. + + """ + def incoming_request(self, **kwargs): + self.santiago.incoming_request(**kwargs) + +class SantiagoSender(SantiagoConnector): + """Generic Santiago Sender superclass. + + This class contains one required method, the request sending method. This + method sends a Santiago request via that protocol. + + """ + def outgoing_request(self): + raise Exception( + "santiago.SantiagoSender.outgoing_request not implemented.") if __name__ == "__main__": # FIXME: convert this to the withsqlite setup. - # load listeners - try: - listeners = load_data("b", "listeners") - except IOError: - https_port = 8090 - cert = "/tmp/santiagoTest/santiagoTest1.crt" - listeners = { "https": { "socket_port": https_port, - "ssl_certificate": cert, - "ssl_private_key": cert }, } - # load senders - try: - senders = load_data("b", "senders") - except IOError: - tor_proxy = "localhost" - tor_proxy_port = 8118 - senders = { "https": { "host": tor_proxy, - "port": tor_proxy_port} } + cert = "santiago.crt" + listeners = { "https": { "socket_port": 8080, + "ssl_certificate": cert, + "ssl_private_key": cert }, } + senders = { "https": { "proxy_host": "localhost", + "proxy_port": 8118} } + # load hosting try: hosting = load_data("b", "hosting") except IOError: - hosting = { "a": { "santiago": set( ["https://localhost:8090"] )}, - "b": { "santiago": set( ["https://localhost:8090"] )}} + hosting = { "a": { "santiago": set( ["https://localhost:8080"] )}, + "b": { "santiago": set( ["https://localhost:8080"] )}} # load consuming try: consuming = load_data("b", "consuming") except IOError: - consuming = { "santiago": { "b": set( ["https://localhost:8090"] ), + consuming = { "santiago": { "b": set( ["https://localhost:8080"] ), "a": set( ["someAddress.onion"] )}} # load the Santiago santiago_b = SimpleSantiago(listeners, senders, hosting, consuming, "b") - # TODO: integrate multiple servers: - # http://docs.cherrypy.org/dev/refman/process/servers.html - - # Start the application. - # cherrypy.Application( - cherrypy.quickstart( - santiago_b, '/') - # cherrypy.engine.start() + santiago_b.start()