checked in initial versions
authorScott Baker <bakers@cs.arizona.edu>
Tue, 5 Aug 2008 17:36:55 +0000 (17:36 +0000)
committerScott Baker <bakers@cs.arizona.edu>
Tue, 5 Aug 2008 17:36:55 +0000 (17:36 +0000)
util/geniclient.py [new file with mode: 0644]
util/geniserver.py [new file with mode: 0644]
util/record.py [new file with mode: 0644]

diff --git a/util/geniclient.py b/util/geniclient.py
new file mode 100644 (file)
index 0000000..c85b8c1
--- /dev/null
@@ -0,0 +1,80 @@
+# geniclient.py
+#
+# geni client
+#
+# implements the client-side of the GENI API.
+#
+# TODO: Investigate ways to combine this with existing PLC API?
+
+import xmlrpclib
+
+from gid import *
+from credential import *
+from record import *
+
+# GeniTransport
+#
+# A transport for XMLRPC that works on top of HTTPS
+
+class GeniTransport(xmlrpclib.Transport):
+    key_file = None
+    cert_file = None
+    def make_connection(self, host):\r
+        # create a HTTPS connection object from a host descriptor\r
+        # host may be a string, or a (host, x509-dict) tuple\r
+        import httplib\r
+        host, extra_headers, x509 = self.get_host_info(host)\r
+        try:\r
+            HTTPS = httplib.HTTPS()\r
+        except AttributeError:\r
+            raise NotImplementedError(\r
+                "your version of httplib doesn't support HTTPS"\r
+                )\r
+        else:\r
+            return httplib.HTTPS(host, None, key_file=self.key_file, cert_file=self.cert_file) #**(x509 or {}))\r
+\r
+# GeniClient:\r
+#\r
+# Class for performing GeniClient operations.\r
+\r
+class GeniClient():
+    # url = url of server
+    # key_file = private key file of client
+    # cert_file = x.509 cert of client
+    def __init__(self, url, key_file, cert_file):
+       self.url = url
+       self.key_file = key_file
+       self.cert_file = cert_file
+       self.transport = GeniTransport()
+       self.transport.key_file = self.key_file
+       self.transport.cert_file = self.cert_file
+       self.server = xmlrpclib.ServerProxy(self.url, self.transport)
+
+    def get_gid(self, name):
+       gid_str_list = self.server.get_gid(name)
+       gid_list = []
+       for str in gid_str_list:
+           gid_list.append(GID(string=str))
+       return gid_list
+
+    # get_self_credential
+    #
+    # a degenerate version of get_credential used by a client to get his
+    # initial credential when he doesn't have one. The same as calling
+    # get_credential(..., cred=None,...)
+
+    def get_self_credential(self, type, name):
+       cred_str = self.server.get_self_credential(type, name)
+       return Credential(string = cred_str)
+
+    def get_credential(self, cred, type, name):
+       cred_str = self.server.get_credential(cred.save_to_string(), type, name)
+       return Credential(string = cred_str)
+
+    def resolve(self, cred, name):
+       result_dict_list = self.server.resolve(cred.save_to_string(), name)
+       result_rec_list = []
+       for dict in result_dict_list:
+            result_rec_list.append(GeniRecord(dict=dict))
+       return result_rec_list
+
diff --git a/util/geniserver.py b/util/geniserver.py
new file mode 100644 (file)
index 0000000..6a8eb87
--- /dev/null
@@ -0,0 +1,188 @@
+# geniserver.py
+#
+# geniwrapper server
+#
+# implements a general-purpose server layer for geni. This should be usable on
+# the registry, component, or other interfaces.
+#
+# TODO: investigate ways to combine this with existing PLC server?
+
+import SimpleXMLRPCServer
+
+import SocketServer
+import BaseHTTPServer\r
+import SimpleHTTPServer\r
+import SimpleXMLRPCServer\r
+\r
+from excep import *\r
+from cert import *\r
+from credential import *\r
+\r
+import socket, os\r
+from OpenSSL import SSL\r
+\r
+# verify_callback\r
+#\r
+# verification callback for pyOpenSSL. We do our own checking of keys because\r
+# we have our own authentication spec. Thus we disable several of the normal\r
+# prohibitions that OpenSSL places on certificates\r
+\r
+def verify_callback(conn, x509, err, depth, preverify):\r
+    # if the cert has been preverified, then it is ok\r
+    if preverify:
+       #print "  preverified"
+       return 1
+
+    # we're only passing single certificates, not chains
+    if depth > 0:
+       #print "  depth > 0 in verify_callback"
+       return 0
+
+    # create a Certificate object and load it from the client's x509
+    ctx = conn.get_context()
+    server = ctx.get_app_data()
+    server.peer_cert = Certificate()
+    server.peer_cert.load_from_pyopenssl_x509(x509)
+
+    # the certificate verification done by openssl checks a number of things
+    # that we aren't interested in, so we look out for those error messages
+    # and ignore them
+
+    # allow self-signed certificates
+    if err == 18:
+       #print "  X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT"
+       return 1
+
+    # allow certs that don't have an issuer
+    if err == 20:
+       #print "  X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
+       return 1
+
+    # allow certs that are untrusted
+    if err == 21:
+       #print "  X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE"
+       return 1
+
+    # allow certs that are untrusted
+    if err == 27:
+       #print "  X509_V_ERR_CERT_UNTRUSTED"
+       return 1
+
+    print "  error", err, "in verify_callback"
+
+    return 0\r
+\r
+# SecureXMLServer\r
+#\r
+# taken from the web (XXX find reference). Implements an HTTPS xmlrpc server\r
+
+class SecureXMLRPCServer(BaseHTTPServer.HTTPServer,SimpleXMLRPCServer.SimpleXMLRPCDispatcher):\r
+    def __init__(self, server_address, HandlerClass, key_file, cert_file, logRequests=True):\r
+        """Secure XML-RPC server.\r
+\r
+        It it very similar to SimpleXMLRPCServer but it uses HTTPS for transporting XML data.\r
+        """\r
+        self.logRequests = logRequests\r
+\r
+        SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, None, None)\r
+        SocketServer.BaseServer.__init__(self, server_address, HandlerClass)\r
+        ctx = SSL.Context(SSL.SSLv23_METHOD)\r
+        ctx.use_privatekey_file(key_file)\r
+        ctx.use_certificate_file(cert_file)\r
+        ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback)\r
+        ctx.set_app_data(self)\r
+        self.socket = SSL.Connection(ctx, socket.socket(self.address_family,\r
+                                                        self.socket_type))\r
+        self.server_bind()\r
+        self.server_activate()\r
+\r
+# SecureXMLRpcRequestHandler\r
+#\r
+# taken from the web (XXX find reference). Implents HTTPS xmlrpc request handler\r
+\r
+class SecureXMLRpcRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):\r
+    """Secure XML-RPC request handler class.\r
+\r
+    It it very similar to SimpleXMLRPCRequestHandler but it uses HTTPS for transporting XML data.\r
+    """\r
+    def setup(self):\r
+        self.connection = self.request\r
+        self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)\r
+        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)\r
+\r
+    def do_POST(self):\r
+        """Handles the HTTPS POST request.\r
+\r
+        It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly.\r
+        """\r
+\r
+        try:\r
+            # get arguments\r
+            data = self.rfile.read(int(self.headers["content-length"]))\r
+            # In previous versions of SimpleXMLRPCServer, _dispatch\r
+            # could be overridden in this class, instead of in\r
+            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,\r
+            # check to see if a subclass implements _dispatch and dispatch\r
+            # using that method if present.\r
+            response = self.server._marshaled_dispatch(\r
+                    data, getattr(self, '_dispatch', None)\r
+                )\r
+        except: # This should only happen if the module is buggy\r
+            # internal error, report as HTTP server error\r
+            self.send_response(500)\r
+            self.end_headers()\r
+        else:\r
+            # got a valid XML RPC response\r
+            self.send_response(200)\r
+            self.send_header("Content-type", "text/xml")\r
+            self.send_header("Content-length", str(len(response)))\r
+            self.end_headers()\r
+            self.wfile.write(response)\r
+\r
+            # shut down the connection\r
+            self.wfile.flush()\r
+            self.connection.shutdown() # Modified here!\r
+
+# GeniServer
+#
+# Class for a general purpose geni server.
+#
+# Implements an HTTPS XML-RPC server. Generally it is expected that GENI
+# functions will take a credential string, which is passed to
+# decode_authentication. Decode_authentication() will verify the validity of
+# the credential, and verify that the user is using the key that matches the
+# GID supplied in the credential.
+
+class GeniServer():
+    def __init__(self, ip, port, key_file, cert_file):
+        self.key = Keypair(filename = key_file)
+        self.cert = Certificate(filename = cert_file)
+        self.server = SecureXMLRPCServer((ip, port), SecureXMLRpcRequestHandler, key_file, cert_file)
+        self.register_functions()
+
+    def decode_authentication(self, cred_string):
+        self.client_cred = Credential(string = cred_string)
+        self.client_gid = self.client_cred.get_gid_caller()
+
+        # make sure the client_gid is not blank
+        if not self.client_gid:
+            raise MissingCallerGID(self.client_cred.get_subject())
+
+        # make sure the client_gid matches the certificate that the client is using
+        peer_cert = self.server.peer_cert
+        if not peer_cert.is_pubkey(self.client_gid.get_pubkey()):
+            raise ConnectionKeyGIDMismatch(self.client_gid.get_subject())
+
+    # register_functions override this to add more functions
+    def register_functions(self):
+        self.server.register_function(self.noop)
+
+    def noop(self, cred, anything):
+        self.decode_authentication(cred)
+
+        return anything
+
+    def run(self):
+        self.server.serve_forever()
+
+
diff --git a/util/record.py b/util/record.py
new file mode 100644 (file)
index 0000000..7c01abd
--- /dev/null
@@ -0,0 +1,175 @@
+# record.py
+#
+# implements support for geni records
+#
+# TODO: Use existing PLC database methods? or keep this separate?
+
+from pg import DB
+
+# Record is a tuple (Name, GID, Type, Info)
+#    info is implemented as a pointer to a PLC record
+#    privkey is stored for convenience on the registry for entities which
+#        the registry holds their private keys
+
+class GeniRecord():
+    def __init__(self, name=None, gid=None, type=None, pointer=None, privkey=None, dict=None):
+        self.dirty=True
+        if name:
+            self.set_name(name)
+        if gid:
+            self.set_gid(gid)
+        if type:
+            self.set_type(type)
+        if pointer:
+            self.set_pointer(pointer)
+        if privkey:
+            self.set_privkey(privkey)
+        if dict:
+            self.set_name(dict['name'])
+            self.set_gid(dict['gid'])
+            self.set_type(dict['type'])
+            self.set_pointer(dict['pointer'])
+            self.set_privkey(dict['privkey'])
+
+    def set_name(self, name):
+        self.name = name
+        self.dirty = True
+
+    def set_gid(self, gid):
+        self.gid = gid
+        self.dirty = True
+
+    def set_type(self, type):
+        self.type = type
+        self.dirty = True
+
+    def set_pointer(self, pointer):
+        self.pointer = pointer
+        self.dirty = True
+
+    def set_privkey(self, privkey):
+        self.privkey = privkey
+        self.dirty = True
+
+    def get_key(self):
+        return self.name + "#" + self.type
+
+    def get_field_names(self):
+        return ["name", "gid", "type", "pointer", "privkey"]
+
+    def get_field_value_string(self, fieldname):
+        if fieldname == "key":
+            val = self.get_key()
+        else:
+            val = getattr(self, fieldname)
+        if isinstance(val, str):
+            return "'" + str(val) + "'"
+        else:
+            return str(val)
+
+    def get_field_value_strings(self, fieldnames):
+        strs = []
+        for fieldname in fieldnames:
+            strs.append(self.get_field_value_string(fieldname))
+        return strs
+
+    def as_dict(self):
+        dict = {}
+        names = self.get_field_names()
+        for name in names:
+            dict[name] = self.getattr(name)
+        return dict
+
+# GeniTable
+#
+# Represents a single table on a registry for a single authority.
+
+class GeniTable():
+    def __init__(self, create=False, hrn="unspecified.default.registry", cninfo=None, privkey=None, gid=None):
+        # XXX privkey/gid are necessary so the table can generate GIDs for its
+        #    records; might move that out of here as it doesn't seem the right place
+
+        self.hrn = hrn
+
+        # pgsql doesn't like table names with "." in them, to replace it with "$"
+        self.tablename = self.hrn.replace(".", "$")
+
+        # establish a connection to the pgsql server
+        self.cnx = DB(cninfo['dbname'], cninfo['address'], port=cninfo['port'], user=cninfo['user'], passwd=cninfo['password'])[
+
+        # the private key is necessary for creation of GIDs
+        self.privkey = privkey
+
+        self.gid = gid
+
+        # if asked to create the table, then create it
+        if create:
+            self.create()
+
+    def create(self):
+        querystr = "CREATE TABLE " + self.tablename + " ( \
+                key text, \
+                name text, \
+                gid text, \
+                type text, \
+                privkey text, \
+                pointer integer);"
+
+        self.cnx.query('DROP TABLE IF EXISTS ' + self.tablename)
+        self.cnx.query(querystr)
+
+    def insert(self, record):
+        fieldnames = ["key"] + record.get_field_names()
+        fieldvals = record.get_field_value_strings(fieldnames)
+        query_str = "INSERT INTO " + self.tablename + \
+                       "(" + ",".join(fieldnames) + ") " + \
+                       "VALUES(" + ",".join(fieldvals) + ")"
+        #print query_str
+        self.cnx.query(query_str)
+
+    def update(self, record):
+        names = record.get_field_names()
+        pairs = []
+        for name in names:
+           val = record.get_field_value_string(name)
+           pairs.append(name + " = " + val)
+        update = ", ".join(pairs)
+
+        query_str = "UPDATE " + self.tablename+ " SET " + update + " WHERE key = '" + record.get_key() + "'"
+        #print query_str
+        self.cnx.query(query_str)
+
+    def resolve_raw(self, type, hrn):
+        query_str = "SELECT * FROM " + self.tablename + " WHERE name = '" + hrn + "'"
+        dict_list = self.cnx.query(query_str).dictresult()
+        result_dict_list = []
+        for dict in dict_list:
+           if (type=="*") or (dict['type'] == type):
+               result_dict_list.append(dict)
+        return result_dict_list
+
+    def resolve(self, type, hrn):
+        result_dict_list = self.resolve_raw(type, hrn)
+        result_rec_list = []
+        for dict in result_dict_list:
+            result_rec_list.append(GeniRecord(dict=dict))
+        return result_rec_list
+
+    def create_gid(self, hrn, uuid, pubkey):
+        gid = GID(subject=hrn, uuid=uuid, hrn=hrn)
+        gid.set_pubkey(pubkey)
+        gid.set_issuer(key=self.privkey, subject=self.hrn)
+        gid.set_parent(self.gid)
+        gid.encode()
+        gid.sign()
+
+        return gid
+
+    def update_gid(self, record)
+        old_gid = GID(string = record.get_gid())
+        pubkey = old_gid.get_pubkey()
+
+        gid = self.create_gid(old_gid.get_hrn(), old_gid.get_uuid(), old_gid.get_pubkey())
+
+        record.set_gid(gid.save_to_string())
+