Index: cred_printer/Makefile
===================================================================
--- cred_printer/Makefile	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/Makefile	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,12 @@
+
+
+VERSION=1.00
+DISTFILES=dist/cred_printer-${VERSION}.tar.gz
+
+all:	${DISTFILES}
+
+dist/cred_printer-${VERSION}.tar.gz:
+	python ./setup.py sdist
+
+clean:
+	rm -rf *.pyc *.core dist MANIFEST
Index: cred_printer/cred_printer/__init__.py
===================================================================
--- cred_printer/cred_printer/__init__.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/cred_printer/__init__.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,1 @@
+#!/usr/local/bin/python
Index: cred_printer/cred_printer/server.py
===================================================================
--- cred_printer/cred_printer/server.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/cred_printer/server.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,169 @@
+#!/usr/local/bin/python
+
+import sys
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+
+from M2Crypto import SSL
+from M2Crypto.SSL.SSLServer import SSLServer
+from SocketServer import TCPServer
+
+from service_error import service_error
+import xmlrpclib
+
+import os.path
+
+import logging
+import traceback
+
+# The SSL server here is based on the implementation described at
+# http://www.xml.com/pub/a/ws/2004/01/20/salz.html
+
+# Turn off the matching of hostname to certificate ID
+SSL.Connection.clientPostConnectionCheck = None
+
+class error_handlers:
+    """
+    Class to encapsulate the debugging and non-debugging error handlers.
+    """
+    def __init__(self, debug):
+	"""
+	Set the error handler
+	"""
+	if debug: self.handle_error = self.handle_error_debug
+	else: self.handle_error = self.handle_error_standard
+
+    def handle_error_debug(self, request=None, client_address=None):
+	"""
+	Debugging error handler.  Prints a stack trace.
+	"""
+	print '-'*40
+	traceback.print_exc()
+	print '-'*40
+
+
+    def handle_error_standard(self, request=None, address=None):
+	"""
+	Print standard error output, suitable for human consumption.
+	"""
+	if request or address:
+	    self.log.warn("[credd] Error on incoming connection: %s %s" % \
+		    (request or "", address or ""))
+	else:
+	    self.log.warn("[credd] Error on incoming connection " + \
+		    "(Likely SSL error)")
+
+
+
+class server(SSLServer, error_handlers):
+    """
+    Overloads a TCPServer to hold an XMLRPC implementation.  The implementation
+    needs to inclide an xmlrpc_services dist that maps from method name to
+    callable mamber that takes a single parameter (usually a list or dict) and
+    a certificate.
+    """
+    def __init__(self, ME, handler, ssl_ctx, impl, debug=False):
+	"""
+	Create an SSL server that handles the transport in handler using the
+	credentials in ssl_ctx, and interfacing to the implementation of fedd
+	services in fedd.  ME is the host port pair on which to bind.
+	"""
+	SSLServer.__init__(self, ME, handler, ssl_ctx)
+	error_handlers.__init__(self, debug)
+	self.impl = impl
+	self.xmlrpc_methods = impl.xmlrpc_services
+	self.log = logging.getLogger("creds")
+
+class simpleServer(TCPServer, error_handlers):
+    """
+    Presents the same interface as server to a threaded TCP server.  Allows a
+    binding of the same implementation and handler classes to
+    unencrypted/authenticated connections.
+    """
+    def __init__(self, ME, handler, impl, debug=False):
+	TCPServer.__init__(self, ME, handler)
+	error_handlers.__init__(self, debug)
+	self.impl = impl
+	self.xmlrpc_methods = impl.xmlrpc_services
+	self.log = logging.getLogger("creds")
+
+
+class xmlrpc_handler(BaseHTTPRequestHandler):
+    """
+    Standard connection between XMLRPC and the services in impl.
+
+    Much of this is boilerplate from 
+    http://www.xml.com/pub/a/ws/2004/01/20/salz.html
+    """
+    server_version = "credd/0.1 " + BaseHTTPRequestHandler.server_version
+
+    def send_xml(self, text, code=200):
+	"""Send an XML document as reply"""
+	self.send_response(code)
+	self.send_header('Content-type', 'text/xml; charset="utf-8"')
+	self.send_header('Content-Length', str(len(text)))
+	self.end_headers()
+	self.wfile.write(text)
+	self.wfile.flush()
+	# Make sure to close the socket when we're done
+	self.request.close()
+	#self.request.socket.close()
+
+    def do_POST(self):
+	"""Treat an HTTP POST request as an XMLRPC service call"""
+	# NB: XMLRPC faults are not HTTP errors, so the code is always 200,
+	# unless an HTTP error occurs, which we don't handle.
+
+	resp = None
+	data = None
+	method = None
+	cl = int(self.headers['content-length'])
+	data = self.rfile.read(cl)
+
+	try:
+	    params, method = xmlrpclib.loads(data)
+	except xmlrpclib.ResponseError:
+	    data = xmlrpclib.dumps(xmlrpclib.Fault("Client", 
+		"Malformed request"), methodresponse=True)
+
+	# Simple servers don't have peer certificates.
+	if getattr(self.request, 'get_peer_cert', None):
+	    fid = self.request.get_peer_cert()
+	else:
+	    fid = None
+
+
+	if method != None:
+	    try:
+		resp = self.xmlrpc_dispatch(method, params, fid)
+		data = xmlrpclib.dumps((resp,), encoding='UTF-8', 
+			methodresponse=True)
+	    except xmlrpclib.Fault, f:
+		data = xmlrpclib.dumps(f, methodresponse=True)
+		resp = None
+
+	self.send_xml(data)
+
+    def log_request(self, code=0, size=0):
+	"""
+	Log request to the fedd logger
+	"""
+	self.server.log.info("Successful XMLRPC request code %d" % code)
+
+
+    def xmlrpc_dispatch(self, method, req, fid):
+	"""
+	The connection to the implementation, using the  method maps
+
+	The implementation provides a mapping from XMLRPC method name to the
+	method in the implementation that provides the service.
+	"""
+	if self.server.xmlrpc_methods.has_key(method):
+	    try:
+		return self.server.xmlrpc_methods[method](req, fid)
+	    except service_error, e:
+		raise xmlrpclib.Fault(e.code_string(), e.desc)
+
+	else:
+	    raise xmlrpclib.Fault(100, "Unknown method: %s" % method)
+
Index: cred_printer/cred_printer/service_error.py
===================================================================
--- cred_printer/cred_printer/service_error.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/cred_printer/service_error.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,53 @@
+#!/usr/local/bin/python
+
+# This is used to make the service error reporting independent of the
+# transport.  The XMLRPC and SOAP dispatchers will convert it into
+# transport-specific errors.
+# Cribbed directly from fedd, because it's so convenient.  Some of the error
+# codes are inapplicable to the credential interpreter.
+class service_error(RuntimeError):
+    access = 1
+    protocol= 2
+    req = 3
+    server_config = 4
+    internal = 5
+    partial = 6
+    federant = 7
+    connect = 8
+    code_str = { 
+	access : "Access Denied",
+	protocol : "Protocol Error",
+	req : "Badly Formed Request",
+	server_config: "Server Configuration Error",
+	internal : "Internal Error",
+	partial: "Partial Embedding",
+	federant: "Federant Error",
+	connect: "Connection Error",
+    }
+    str_code = dict([ (v, k) for k, v in code_str.iteritems() ])
+    client_errors = ( req, partial)
+    server_errors = ( access, protocol, server_config, internal,
+	    federant, connect)
+
+    def __init__(self, code=None, desc=None, from_string=None, proof=None):
+	self.code = code
+	self.desc = desc
+	self.proof = proof or []
+	if not isinstance (self.proof, list): self.proof = [ proof ]
+	if code == None:
+	    self.set_code_from_string(from_string)
+	RuntimeError.__init__(self, desc)
+
+    def code_string(self, code=None):
+	code = code or self.code
+	return service_error.code_str.get(code)
+    
+    def set_code_from_string(self, errstr):
+	self.code = service_error.str_code.get(errstr, service_error.internal)
+	return self.code
+    
+    def is_client_error(self):
+	return self.code in service_error.client_errors
+
+    def is_server_error(self):
+	return self.code in service_error.server_errors
Index: cred_printer/cred_printer/util.py
===================================================================
--- cred_printer/cred_printer/util.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/cred_printer/util.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,131 @@
+#!/usr/local/bin/python
+
+import sys
+from M2Crypto import SSL
+
+# Turn off the matching of hostname to certificate ID
+SSL.Connection.clientPostConnectionCheck = None
+
+class ssl_context(SSL.Context):
+    """
+    Simple wrapper around an M2Crypto.SSL.Context to initialize it for use with
+    self-signed certs.
+    """
+    def __init__(self, my_cert, trusted_certs=None, password=None):
+	"""
+	construct a cred_ssl_context
+
+	@param my_cert: PEM file with my certificate in it
+	@param trusted_certs: PEM file with trusted certs in it (optional)
+	"""
+	SSL.Context.__init__(self)
+
+	# load_cert takes a callback to get a password, not a password, so if
+	# the caller provided a password, this creates a nonce callback using a
+	# lambda form.
+	if password != None and not callable(password):
+	    # This is cute.  password = lambda *args: password produces a
+	    # function object that returns itself rather than one that returns
+	    # the object itself.  This is because password is an object
+	    # reference and after the assignment it's a lambda.  So we assign
+	    # to a temp.
+	    pwd = str(password)
+	    password =lambda *args: pwd
+
+	# The calls to str below (and above) are because the underlying SSL
+	# stuff is intolerant of unicode.
+	if password != None:
+	    self.load_cert(str(my_cert), callback=password)
+	else:
+	    self.load_cert(str(my_cert))
+
+	# If no trusted certificates are specified, allow unknown CAs.
+	if trusted_certs: 
+	    self.load_verify_locations(trusted_certs)
+	    self.set_verify(SSL.verify_peer, 10)
+	else:
+	    callb = getattr(SSL.cb, "ssl_verify_callback")
+	    self.set_allow_unknown_ca(True)
+	    self.set_verify(SSL.verify_peer, 10, 
+		    callback=SSL.cb.ssl_verify_callback_allow_unknown_ca)
+
+# Python 2.7 broke the MCrypto 2.1 SSL_Transport.  If this is an older python,
+# use SSL_Transport unchanged, otherwise use a tweaked version.  Export them as
+# SSLTransport.
+if sys.version_info == 2 and sys.version_info < 7:
+    from M2Crypto.m2xmlrpclib import SSL_Transport
+    class SSLTransport(SSL_Transport): pass
+else:
+    # Most of this is directly from M2Crypto
+    import base64, string, sys
+
+    from xmlrpclib import *
+    import M2Crypto
+    import M2Crypto.SSL, M2Crypto.httpslib, M2Crypto.m2urllib
+
+    __version__ = M2Crypto.version
+
+    class SSLTransport(Transport):
+
+	user_agent = "M2Crypto_XMLRPC/%s - %s" % (__version__, 
+		Transport.user_agent)
+
+	def __init__(self, ssl_context=None, *args, **kw):
+	    if getattr(Transport, '__init__', None) is not None:
+		Transport.__init__(self, *args, **kw)
+	    if ssl_context is None:
+		self.ssl_ctx=M2Crypto.SSL.Context('sslv23')
+	    else:
+		self.ssl_ctx=ssl_context
+
+	def request(self, host, handler, request_body, verbose=0):
+	    # Handle username and password.
+	    user_passwd, host_port = M2Crypto.m2urllib.splituser(host)
+	    _host, _port = M2Crypto.m2urllib.splitport(host_port)
+
+	    # This is a difference, was an (obsolete) HTTPS object, but
+	    # HTTPSConnection is more supported and clear --tvf
+	    h = M2Crypto.httpslib.HTTPSConnection(_host, int(_port), 
+		    ssl_context=self.ssl_ctx)
+	    if verbose:
+		h.set_debuglevel(1)
+
+	    # What follows is as in xmlrpclib.Transport. (Except the authz bit.)
+	    h.putrequest("POST", handler)
+
+	    # required by HTTP/1.1
+	    h.putheader("Host", _host)
+
+	    # required by XML-RPC
+	    h.putheader("User-Agent", self.user_agent)
+	    h.putheader("Content-Type", "text/xml")
+	    h.putheader("Content-Length", str(len(request_body)))
+
+	    # Authorisation.
+	    if user_passwd is not None:
+		auth=string.strip(base64.encodestring(user_passwd))
+		h.putheader('Authorization', 'Basic %s' % auth)
+
+	    h.endheaders()
+
+	    if request_body:
+		h.send(request_body)
+
+	    # These are the modifications -- tvf
+
+	    resp = h.getresponse()
+	    errcode = resp.status
+	    errmsg = resp.reason
+
+	    # end mods -- tvf
+
+	    if errcode != 200:
+		raise ProtocolError(
+		    host + handler,
+		    errcode, errmsg,
+		    headers
+		    )
+
+	    self.verbose = verbose
+	    return self.parse_response(resp)
+
Index: cred_printer/cred_server.py
===================================================================
--- cred_printer/cred_server.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/cred_server.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,217 @@
+#!/usr/local/bin/python
+
+import sys
+
+import re
+import select
+from signal import signal, SIGINT, SIGTERM
+
+from cred_printer.server import xmlrpc_handler, server, simpleServer
+from cred_printer.service_error import service_error
+from cred_printer.util import ssl_context
+
+from xmlrpclib import Binary
+from tempfile import NamedTemporaryFile
+from string import join
+
+from optparse import OptionParser
+
+import ABAC
+import Creddy
+
+class OptParser(OptionParser):
+    """
+    Option parsing for clients.  Should be self-explanatory.
+    """
+    def __init__(self):
+	OptionParser.__init__(self)
+	self.add_option('--port', dest='port', type='int', default=13232,
+		help='port to listen on')
+	self.add_option('--cert', dest='cert', default=None,
+		help='My identity certificate (and key)')
+
+
+class credential_printer:
+    """
+    The implementation passed to the server to handle incoming requests.
+    Specifically, translate_creds will be called when the translate method is
+    invoked on the server.
+    """
+    def __init__(self):
+	"""
+	Initialize the method -> function dict.
+	"""
+	self.xmlrpc_services = { 'translate': self.translate_creds }
+
+    @staticmethod
+    def get_cred_data(d):
+	"""
+	Get the credential data from one of the aprameter dicts
+	"""
+	return d.get('credential', Binary()).data
+
+    @staticmethod
+    def attr_cred_to_string(r):
+	"""
+	Parse a an ABAC.Credential into a string representation.
+	"""
+	return "%s <- %s" % (r.head().string(), r.tail().string())
+
+    @staticmethod
+    def repl_IDs(s, id_dict):
+	"""
+	Replace all the keyids in the string with the name they map to in the
+	dict.  id_dict maps an ID certificate's keyid to a human-readable name.
+	"""
+	for k, n in id_dict.items():
+	    if re.search(k, s):
+		s = re.sub(k, n, s)
+	return s
+
+    def make_ID(self, cred):
+	"""
+	Create a Creddy.ID from the given binary, if possible.  Because
+	Creddy.ID doesn't take a binary blob, this writes a temp file and
+	creates the ID from that.  If successful, the ID is returned.
+	Otherwise None is returned.
+	"""
+	i = None
+	try:
+	    f = NamedTemporaryFile()
+	    f.write(cred)
+	    f.flush()
+	    i = Creddy.ID(f.name)
+	    f.close()
+	except:
+	    pass
+	finally:
+	    return i
+
+    def split_certs(self, cred_dict):
+	"""
+	A list of dicts of the form { id: identifier, credential: bits} is
+	divided into two such lists, one where the bits are attribute
+	certificates and one where they are IDs.  Dicts the ID cert list have
+	their 'type', 'str', 'auxstr' fields set to 'identity', the keyid, and
+	the common name in the certificate.  The temporary 'chunk' key is set
+	to the binary representation fo the credential.  The return value is a
+	tuple of ID dicts and attr dicts in that order.
+	"""
+	ids = [ ]
+	attrs = [ ]
+	for e in cred_dict:
+	    abac_ID = self.make_ID(self.get_cred_data(e))
+	    if abac_ID:
+		id_name = abac_ID.cert_filename()
+		if id_name.endswith('_ID.pem'):
+		    id_name = id_name[0:-7]
+		e['type'] = 'identity'
+		e['str'] = abac_ID.keyid()
+		e['auxstr'] = id_name
+		e['chunk'] = abac_ID.cert_chunk()
+		ids.append(e)
+	    else:
+		attrs.append(e)
+
+	return (ids, attrs)
+
+
+    def translate_attr(self, a, c, id_dict):
+	"""
+	Set the 'str' and 'auxstr' fields in the given attribute dict (a) by
+	importing it into a clone of the given context and parsing the result.
+	The 'errcode' key is set as well.
+	"""
+	cc = ABAC.Context(c)
+	errcode = cc.load_attribute_chunk(self.get_cred_data(a))
+	if errcode == ABAC.ABAC_CERT_SUCCESS:
+	    # Success, pull it out and parse it.
+	    creds = cc.credentials()
+	    # There should only be one credential in the list.  If not, throw
+	    # an error
+	    if len(creds) == 1:
+		a['str'] = self.attr_cred_to_string(creds[0])
+		a['auxstr'] = self.repl_IDs(a['str'], id_dict)
+		a['type'] = 'attribute'
+	    else:
+		raise service_error(service_error.server_config, 
+			'Multiple credentials in Context!?')
+	else:
+	    # Fail, clear the keys
+	    a['str'] = ''
+	    a['auxstr']= ''
+	    a['type'] = 'unknown'
+	a['errcode'] = errcode
+
+
+    def translate_creds(self, req, fid):
+	"""
+	Base translation routine: split the credential dicts into IDs and
+	attributes, initialize an ABAC context with all the known IDs, and
+	parse out each attribute.  Return a single list of all the modified
+	certificate dicts.  The server will encode that and return it or
+	convert any service_errors raised into an XMLRPC Fault.
+	"""
+	ids, attrs = self.split_certs(req[0])
+	ctxt = ABAC.Context()
+	id_dict = { }	# Used to create auxstrs.  It maps ID cert keyid->CN
+	for i in ids:
+	    if 'chunk' in i: 
+		errcode = ctxt.load_id_chunk(i['chunk'])
+		del i['chunk']
+	    else: 
+		errcode = ABAC.ABAC_CERT_INVALID
+
+	    if errcode == ABAC.ABAC_CERT_SUCCESS:
+		id_dict[i['str']] = i['auxstr']
+	    i['errcode'] = errcode
+	for a in attrs:
+	    self.translate_attr(a, ctxt, id_dict)
+	return ids + attrs
+
+
+def shutdown(sig, frame):
+    """
+    Signal handler.  Set the global active variable false if a terminating
+    signal is received.
+    """
+    global active
+    active = False
+
+# Main code
+
+# Parse args
+parser = OptParser()
+opts, args = parser.parse_args()
+
+if opts.cert:
+    # There's a certificate specified, set up as an SSL server.
+    try:
+	ctx = ssl_context(opts.cert)
+    except SSLError, e:
+	sys.exit("Cannot load %s: %s" % (opts.cert, e))
+
+    s = server(('localhost', opts.port), xmlrpc_handler, ctx, 
+	    credential_printer(), True)
+else:
+    # No certificate.  Be an open server
+    s = simpleServer(('localhost', opts.port), xmlrpc_handler, 
+	    credential_printer(), True)
+
+# Catch SIGINT and SIGTERM
+signal(SIGINT, shutdown)
+signal(SIGTERM, shutdown)
+
+# Do it.
+active = True
+while active:
+    try:
+	# When a request comes in handle it.  This extra selecting gives us
+	# space to catch terminating signals.
+	i, o, e = select.select((s,), (), (), 1.0)
+	if s in i: s.handle_request()
+    except select.error, e:
+	if e[0] == 4: pass
+	else: sys.exit("Unexpected error: %s" % e)
+
+sys.exit(0)
Index: cred_printer/setup.py
===================================================================
--- cred_printer/setup.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
+++ cred_printer/setup.py	(revision 02dcba536bb4ec0a836dcc142d06a551653ded6c)
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(name='cred_printer',
+	version='1.00',
+	description='ABAC X.509 to text service',
+	author='Ted Faber',
+	author_email='faber@isi.edu',
+	url='http://abac.isi.deterlab.net',
+	packages=['cred_printer'],
+	requires=['M2Crypto'],
+	provides=['cred_printer'],
+	scripts=['cred_client.py', 'cred_server.py' ],
+    )
