From 950153a447b02cdb987c939cac32e63caa8cf3aa Mon Sep 17 00:00:00 2001 From: Scott Baker Date: Tue, 29 Sep 2009 22:36:34 +0000 Subject: [PATCH] check in modpythonapi from raven project --- sfa/server/modpythonapi/ApiExceptionCodes.py | 2 + sfa/server/modpythonapi/ApiExceptions.py | 15 ++ sfa/server/modpythonapi/AuthenticatedApi.py | 71 ++++++ .../modpythonapi/AuthenticatedClient.py | 24 +++ sfa/server/modpythonapi/BaseApi.py | 203 ++++++++++++++++++ sfa/server/modpythonapi/BaseClient.py | 46 ++++ sfa/server/modpythonapi/ModPython.py | 68 ++++++ sfa/server/modpythonapi/TestApi.py | 19 ++ sfa/server/modpythonapi/installTest.sh | 37 ++++ sfa/server/modpythonapi/test.py | 44 ++++ sfa/server/modpythonapi/test.sh | 3 + sfa/server/modpythonapi/testapi.conf | 5 + 12 files changed, 537 insertions(+) create mode 100644 sfa/server/modpythonapi/ApiExceptionCodes.py create mode 100644 sfa/server/modpythonapi/ApiExceptions.py create mode 100755 sfa/server/modpythonapi/AuthenticatedApi.py create mode 100755 sfa/server/modpythonapi/AuthenticatedClient.py create mode 100755 sfa/server/modpythonapi/BaseApi.py create mode 100755 sfa/server/modpythonapi/BaseClient.py create mode 100755 sfa/server/modpythonapi/ModPython.py create mode 100755 sfa/server/modpythonapi/TestApi.py create mode 100755 sfa/server/modpythonapi/installTest.sh create mode 100755 sfa/server/modpythonapi/test.py create mode 100755 sfa/server/modpythonapi/test.sh create mode 100644 sfa/server/modpythonapi/testapi.conf diff --git a/sfa/server/modpythonapi/ApiExceptionCodes.py b/sfa/server/modpythonapi/ApiExceptionCodes.py new file mode 100644 index 00000000..cd811d23 --- /dev/null +++ b/sfa/server/modpythonapi/ApiExceptionCodes.py @@ -0,0 +1,2 @@ +FAULT_UNHANDLEDSERVEREXCEPTION = 901 +FAULT_BADREQUESTHASH = 902 diff --git a/sfa/server/modpythonapi/ApiExceptions.py b/sfa/server/modpythonapi/ApiExceptions.py new file mode 100644 index 00000000..834afd2c --- /dev/null +++ b/sfa/server/modpythonapi/ApiExceptions.py @@ -0,0 +1,15 @@ +import traceback +import xmlrpclib + +FAULTCODE = 900 + +class UnhandledServerException(xmlrpclib.Fault): + def __init__(self, type, value, tb): + exc_str = ''.join(traceback.format_exception(type, value, tb)) + faultString = exc_str # "Unhandled exception: " + str(type) + "\n" + exc_str + xmlrpclib.Fault.__init__(self, FAULTCODE + 1, faultString) + +class BadRequestHash(xmlrpclib.Fault): + def __init__(self, hash = None): + faultString = "bad request hash: " + str(hash) + xmlrpclib.Fault.__init__(self, FAULTCODE + 2, faultString) diff --git a/sfa/server/modpythonapi/AuthenticatedApi.py b/sfa/server/modpythonapi/AuthenticatedApi.py new file mode 100755 index 00000000..c909346b --- /dev/null +++ b/sfa/server/modpythonapi/AuthenticatedApi.py @@ -0,0 +1,71 @@ +import xmlrpclib + +from BaseApi import BaseApi + +from sfa.trust.credential import Credential +from sfa.trust.gid import GID +from sfa.trust.trustedroot import TrustedRootList + +from ApiExceptionCodes import * + +class BadRequestHash(xmlrpclib.Fault): + def __init__(self, hash = None): + faultString = "bad request hash: " + str(hash) + xmlrpclib.Fault.__init__(self, FAULT_BADREQUESTHASH, faultString) + +class AuthenticatedApi(BaseApi): + def __init__(self, encoding = "utf-8", trustedRootsDir=None): + BaseApi.__init__(self, encoding) + if trustedRootsDir: + self.trusted_cert_list = TrustedRootList(trustedRootsDir).get_list() + else: + self.trusted_cert_list = None + + def register_functions(self): + BaseApi.register_functions(self) + self.register_function(self.gidNoop) + + def verifyGidRequestHash(self, gid, hash, arglist): + key = gid.get_pubkey() + if not key.verify_string(str(arglist), hash): + raise BadRequestHash(hash) + + def verifyCredRequestHash(self, cred, hash, arglist): + gid = cred.get_gid_caller() + self.verifyGidRequestHash(gid, hash, arglist) + + def validateGid(self, gid): + if self.trusted_cert_list: + gid.verify_chain(self.trusted_cert_list) + + def validateCred(self, cred): + if self.trusted_cert_list: + cred.verify_chain(self.trusted_cert_list) + caller_gid = cred.get_gid_caller() + object_gid = cred.get_gid_object() + if caller_gid: + caller_gid.verify_chain(self.trusted_cert_list) + if object_gid: + object_gid.verify_chain(self.trusted_cert_list) + + def authenticateGid(self, gidStr, argList, requestHash): + gid = GID(string = gidStr) + self.validateGid(gid) + self.verifyGidRequestHash(gid, requestHash, argList) + return gid + + def authenticateCred(self, credStr, argList, requestHash): + cred = Credential(string = credStr) + self.validateCred(cred) + self.verifyCredRequestHash(cred, requestHash, argList) + return cred + + def gidNoop(self, gidStr, value, requestHash): + self.authenticateGid(gidStr, [gidStr, value], requestHash) + return value + + def credNoop(self, credStr, value, requestHash): + self.authenticateCred(credStr, [credStr, value], requestHash) + return value + + diff --git a/sfa/server/modpythonapi/AuthenticatedClient.py b/sfa/server/modpythonapi/AuthenticatedClient.py new file mode 100755 index 00000000..6b705fc4 --- /dev/null +++ b/sfa/server/modpythonapi/AuthenticatedClient.py @@ -0,0 +1,24 @@ +from sfa.trust.certificate import Keypair +from sfa.trust.gid import GID + +from BaseClient import BaseClient + +class AuthenticatedClient(BaseClient): + def __init__(self, url, private_key_file, gid_file=None, cred_file=None): + BaseClient.__init__(self, url) + self.private_key_file = private_key_file + self.gid_file = gid_file + self.cred_file = cred_file + self.private_key = Keypair(filename = self.private_key_file) + if gid_file: + self.gid = GID(filename = self.gid_file) + if cred_file: + self.cred = Credential(filename = self.cred_file) + + def computeRequestHash(self, argList): + return self.private_key.sign_string(str(argList)) + + def gidNoop(self, value): + gidStr = self.gid.save_to_string(True) + reqHash = self.computeRequestHash([gidStr, value]) + return self.server.gidNoop(gidStr, value, reqHash) diff --git a/sfa/server/modpythonapi/BaseApi.py b/sfa/server/modpythonapi/BaseApi.py new file mode 100755 index 00000000..95538ad3 --- /dev/null +++ b/sfa/server/modpythonapi/BaseApi.py @@ -0,0 +1,203 @@ +# +# PLCAPI XML-RPC and SOAP interfaces +# +# Aaron Klingaman +# Mark Huang +# +# Copyright (C) 2004-2006 The Trustees of Princeton University +# $Id: API.py 14587 2009-07-19 13:18:50Z thierry $ +# $URL: https://svn.planet-lab.org/svn/PLCAPI/trunk/PLC/API.py $ +# + +import sys +import traceback +import string + +import xmlrpclib +import logging +import logging.handlers + +from ApiExceptionCodes import * + +# Wrapper around xmlrpc fault to include a traceback of the server to the +# client. This is done to aid in debugging from a client perspective. + +class FaultWithTraceback(xmlrpclib.Fault): + def __init__(self, code, faultString, exc_info): + type, value, tb = exc_info + exc_str = ''.join(traceback.format_exception(type, value, tb)) + faultString = faultString + "\nFAULT_TRACEBACK:" + exc_str + xmlrpclib.Fault.__init__(self, code, faultString) + +# Exception to report to the caller when some non-XMLRPC fault occurs on the +# server. For example a TypeError. + +class UnhandledServerException(FaultWithTraceback): + def __init__(self, exc_info): + type, value, tb = exc_info + faultString = "Unhandled exception: " + str(type) + FaultWithTraceback.__init__(self, FAULT_UNHANDLEDSERVEREXCEPTION, faultString, exc_info) + +# See "2.2 Characters" in the XML specification: +# +# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] +# avoiding +# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF] + +invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F)) +xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii)) + +def xmlrpclib_escape(s, replace = string.replace): + """ + xmlrpclib does not handle invalid 7-bit control characters. This + function augments xmlrpclib.escape, which by default only replaces + '&', '<', and '>' with entities. + """ + + # This is the standard xmlrpclib.escape function + s = replace(s, "&", "&") + s = replace(s, "<", "<") + s = replace(s, ">", ">",) + + # Replace invalid 7-bit control characters with '?' + return s.translate(xml_escape_table) + +def xmlrpclib_dump(self, value, write): + """ + xmlrpclib cannot marshal instances of subclasses of built-in + types. This function overrides xmlrpclib.Marshaller.__dump so that + any value that is an instance of one of its acceptable types is + marshalled as that type. + + xmlrpclib also cannot handle invalid 7-bit control characters. See + above. + """ + + # Use our escape function + args = [self, value, write] + if isinstance(value, (str, unicode)): + args.append(xmlrpclib_escape) + + try: + # Try for an exact match first + f = self.dispatch[type(value)] + except KeyError: + # Try for an isinstance() match + for Type, f in self.dispatch.iteritems(): + if isinstance(value, Type): + f(*args) + return + raise TypeError, "cannot marshal %s objects" % type(value) + else: + f(*args) + +# You can't hide from me! +xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump + +# SOAP support is optional +try: + import SOAPpy + from SOAPpy.Parser import parseSOAPRPC + from SOAPpy.Types import faultType + from SOAPpy.NS import NS + from SOAPpy.SOAPBuilder import buildSOAP +except ImportError: + SOAPpy = None + +def import_deep(name): + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +class BaseApi: + def __init__(self, encoding = "utf-8"): + self.encoding = encoding + self.init_logger() + self.funcs = {} + self.register_functions() + + def init_logger(self): + self.logger = logging.getLogger("ApiLogger") + self.logger.setLevel(logging.INFO) + self.logger.addHandler(logging.handlers.RotatingFileHandler(self.get_log_name(), maxBytes=100000, backupCount=5)) + + def get_log_name(self): + return "/tmp/apilogfile.txt" + + def register_functions(self): + self.register_function(self.noop) + + def register_function(self, function, name = None): + if name is None: + name = function.__name__ + self.funcs[name] = function + + def call(self, source, method, *args): + """ + Call the named method from the specified source with the + specified arguments. + """ + + if not method in self.funcs: + raise "Unknown method: " + method + + return self.funcs[method](*args) + + def handle(self, source, data): + """ + Handle an XML-RPC or SOAP request from the specified source. + """ + + # Parse request into method name and arguments + try: + interface = xmlrpclib + (args, method) = xmlrpclib.loads(data) + methodresponse = True + except Exception, e: + if SOAPpy is not None: + interface = SOAPpy + (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1) + method = r._name + args = r._aslist() + # XXX Support named arguments + else: + raise e + + self.logger.debug("OP:" + str(method) + " from " + str(source)) + + try: + result = self.call(source, method, *args) + except xmlrpclib.Fault, fault: + self.logger.warning("FAULT: " + str(fault.faultCode) + " " + str(fault.faultString)) + self.logger.info(traceback.format_exc()) + # Handle expected faults + if interface == xmlrpclib: + result = FaultWithTraceback(fault.faultCode, fault.faultString, sys.exc_info()) + methodresponse = None + elif interface == SOAPpy: + result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method) + result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString)) + self.logger.debug + except: + self.logger.warning("EXCEPTION: " + str(sys.exc_info()[0])) + self.logger.info(traceback.format_exc()) + result = UnhandledServerException(sys.exc_info()) + methodresponse = None + + # Return result + if interface == xmlrpclib: + if not isinstance(result, xmlrpclib.Fault): + result = (result,) + data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1) + elif interface == SOAPpy: + data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding) + + return data + + def noop(self, value): + return value + + + diff --git a/sfa/server/modpythonapi/BaseClient.py b/sfa/server/modpythonapi/BaseClient.py new file mode 100755 index 00000000..448f9346 --- /dev/null +++ b/sfa/server/modpythonapi/BaseClient.py @@ -0,0 +1,46 @@ +import xmlrpclib + +from ApiExceptionCodes import * + +VerboseExceptions = False + +def EnableVerboseExceptions(x=True): + global VerboseExceptions + VerboseExceptions = x + +class ExceptionUnmarshaller(xmlrpclib.Unmarshaller): + def close(self): + try: + return xmlrpclib.Unmarshaller.close(self) + except xmlrpclib.Fault, e: + # if the server tagged some traceback info onto the end of the + # exception text, then print it out on the client. + + if "\nFAULT_TRACEBACK:" in e.faultString: + parts = e.faultString.split("\nFAULT_TRACEBACK:") + e.faultString = parts[0] + if VerboseExceptions: + print "\n|Server Traceback:", "\n|".join(parts[1].split("\n")) + + raise e + +class ExceptionReportingTransport(xmlrpclib.Transport): + def make_connection(self, host): + import httplib + if host.startswith("https:"): + return httplib.HTTPS(host) + else: + return httplib.HTTP(host) + + def getparser(self): + unmarshaller = ExceptionUnmarshaller() + parser = xmlrpclib.ExpatParser(unmarshaller) + return parser, unmarshaller + +class BaseClient(): + def __init__(self, url): + self.url = url + self.server = xmlrpclib.ServerProxy(self.url, ExceptionReportingTransport()) + + def noop(self, value): + return self.server.noop(value) diff --git a/sfa/server/modpythonapi/ModPython.py b/sfa/server/modpythonapi/ModPython.py new file mode 100755 index 00000000..64ceb990 --- /dev/null +++ b/sfa/server/modpythonapi/ModPython.py @@ -0,0 +1,68 @@ +# +# Apache mod_python interface +# +# Aaron Klingaman +# Mark Huang +# +# Copyright (C) 2004-2006 The Trustees of Princeton University +# + +import sys +import traceback +import xmlrpclib +from mod_python import apache + +from API import RemoteApi +api = RemoteApi() + +class unbuffered: + """ + Write to /var/log/httpd/error_log. See + + http://www.modpython.org/FAQ/faqw.py?req=edit&file=faq02.003.htp + """ + + def write(self, data): + sys.stderr.write(data) + sys.stderr.flush() + +#log = unbuffered() + +def handler(req): + try: + if req.method != "POST": + req.content_type = "text/html" + req.send_http_header() + req.write(""" + +PLCAPI XML-RPC/SOAP Interface + +

PLCAPI XML-RPC/SOAP Interface

+

Please use XML-RPC or SOAP to access the PLCAPI.

+ +""") + return apache.OK + + # Read request + request = req.read(int(req.headers_in['content-length'])) + + # mod_python < 3.2: The IP address portion of remote_addr is + # incorrect (always 0.0.0.0) when IPv6 is enabled. + # http://issues.apache.org/jira/browse/MODPYTHON-64?page=all + (remote_ip, remote_port) = req.connection.remote_addr + remote_addr = (req.connection.remote_ip, remote_port) + + # Handle request + response = api.handle(remote_addr, request) + + # Write response + req.content_type = "text/xml; charset=" + api.encoding + req.send_http_header() + req.write(response) + + return apache.OK + + except Exception, err: + # Log error in /var/log/httpd/(ssl_)?error_log + print >> log, err, traceback.format_exc() + return apache.HTTP_INTERNAL_SERVER_ERROR diff --git a/sfa/server/modpythonapi/TestApi.py b/sfa/server/modpythonapi/TestApi.py new file mode 100755 index 00000000..11daed51 --- /dev/null +++ b/sfa/server/modpythonapi/TestApi.py @@ -0,0 +1,19 @@ +from AuthenticatedApi import AuthenticatedApi, BadRequestHash + +class RemoteApi(AuthenticatedApi): + def __init__(self, encoding="utf-8", trustedRootsDir="/usr/local/testapi/var/trusted_roots"): + return AuthenticatedApi.__init__(self, encoding) + + def get_log_name(self): + return "/usr/local/testapi/var/logfile.txt" + + def register_functions(self): + AuthenticatedApi.register_functions(self) + self.register_function(self.typeError) + self.register_function(self.badRequestHash) + + def typeError(self): + raise TypeError() + + def badRequestHash(self): + raise BadRequestHash("somehashvalue") diff --git a/sfa/server/modpythonapi/installTest.sh b/sfa/server/modpythonapi/installTest.sh new file mode 100755 index 00000000..7ae489e9 --- /dev/null +++ b/sfa/server/modpythonapi/installTest.sh @@ -0,0 +1,37 @@ +GENI_SRC_DIR=/home/smbaker/projects/geniwrapper/trunk + +mkdir -p /usr/local/testapi/bin +mkdir -p /usr/local/testapi/bin/sfa/trust +mkdir -p /usr/local/testapi/bin/sfa/util +mkdir -p /usr/local/testapi/var/trusted_roots +mkdir -p /repository/testapi + +# source code for the API +cp BaseApi.py /usr/local/testapi/bin/ +cp AuthenticatedApi.py /usr/local/testapi/bin/ +cp TestApi.py /usr/local/testapi/bin/API.py +cp ModPython.py /usr/local/testapi/bin/ +cp ApiExceptionCodes.py /usr/local/testapi/bin/ + +# trusted root certificates that match gackstestuser.* +cp trusted_roots/*.gid /usr/local/testapi/var/trusted_roots/ + +# apache config file to enable the api +cp testapi.conf /etc/httpd/conf.d/ + +# copy over geniwrapper stuff that we need +echo > /usr/local/testapi/bin/sfa/__init__.py +echo > /usr/local/testapi/bin/sfa/trust/__init__.py +echo > /usr/local/testapi/bin/sfa/util/__init__.py +cp $GENI_SRC_DIR/sfa/trust/gid.py /usr/local/testapi/bin/sfa/trust/ +cp $GENI_SRC_DIR/sfa/trust/certificate.py /usr/local/testapi/bin/sfa/trust/ +cp $GENI_SRC_DIR/sfa/trust/trustedroot.py /usr/local/testapi/bin/sfa/trust/ +cp $GENI_SRC_DIR/sfa/trust/credential.py /usr/local/testapi/bin/sfa/trust/ +cp $GENI_SRC_DIR/sfa/trust/rights.py /usr/local/testapi/bin/sfa/trust/ +cp $GENI_SRC_DIR/sfa/util/faults.py /usr/local/testapi/bin/sfa/util/ + +# make everything owned by apache +chown -R apache /usr/local/testapi +chown apache /etc/httpd/conf.d/testapi.conf + +/etc/init.d/httpd restart \ No newline at end of file diff --git a/sfa/server/modpythonapi/test.py b/sfa/server/modpythonapi/test.py new file mode 100755 index 00000000..d3fafed9 --- /dev/null +++ b/sfa/server/modpythonapi/test.py @@ -0,0 +1,44 @@ +import sys +import traceback + +from BaseClient import BaseClient, EnableVerboseExceptions +from AuthenticatedClient import AuthenticatedClient + +EnableVerboseExceptions(True) + +HOST = "localhost" +URL = "http://" + HOST + "/TESTAPI/" +SURL = "https://" + HOST + "/TESTAPI/" + +print "*** testing some valid ops; these should print \"Hello, World\" ***" + +bc = BaseClient(URL) +print "HTTP noop:", bc.noop("Hello, World") + +ac = AuthenticatedClient(URL, "gackstestuser.pkey", "gackstestuser.gid") +print "HTTP gidNoop:", ac.gidNoop("Hello, World") + +bc = BaseClient(SURL) +print "HTTPS noop:", bc.noop("Hello, World") + +ac = AuthenticatedClient(URL, "gackstestuser.pkey", "gackstestuser.gid") +print "HTTPS gidNoop:", ac.gidNoop("Hello, World") + +print +print "*** testing some exception handling: ***" + +bc = BaseClient(URL) +print "HTTP typeError:", +try: + result = bc.server.typeError() + print result +except Exception, e: + print ''.join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) + +print "HTTP badrequesthash:", +try: + result = bc.server.badRequestHash() + print result +except: + print ''.join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) + diff --git a/sfa/server/modpythonapi/test.sh b/sfa/server/modpythonapi/test.sh new file mode 100755 index 00000000..745c6a84 --- /dev/null +++ b/sfa/server/modpythonapi/test.sh @@ -0,0 +1,3 @@ +export PYTHONPATH=/home/smbaker/projects/geniwrapper/trunk + +python ./test.py diff --git a/sfa/server/modpythonapi/testapi.conf b/sfa/server/modpythonapi/testapi.conf new file mode 100644 index 00000000..5495fd24 --- /dev/null +++ b/sfa/server/modpythonapi/testapi.conf @@ -0,0 +1,5 @@ + + SetHandler mod_python + PythonPath "sys.path + ['/usr/local/testapi/bin/']" + PythonHandler ModPython + \ No newline at end of file -- 2.43.0