diff --git a/cli/koji b/cli/koji
index c247d842..8da52001 100755
--- a/cli/koji
+++ b/cli/koji
@@ -79,7 +79,7 @@ def get_options():
parser.disable_interspersed_args()
parser.add_option("-c", "--config", dest="configFile",
help=_("use alternate configuration file"), metavar="FILE",
- default="/etc/koji.conf")
+ default="~/.koji/config")
parser.add_option("--keytab", help=_("specify a Kerberos keytab to use"))
parser.add_option("--principal", help=_("specify a Kerberos principal to use"))
parser.add_option("--runas", help=_("run as the specified user (requires special privileges)"))
@@ -131,23 +131,27 @@ def get_options():
'server' : 'http://localhost/kojihub',
'web_url' : 'http://localhost/koji',
'topdir' : '/mnt/koji',
+ 'cert': '~/.koji/client.crt',
+ 'ca': '~/.koji/clientca.crt',
+ 'serverca': '~/.koji/serverca.crt'
}
- if os.access(options.configFile, os.F_OK):
- f = open(options.configFile)
- config = ConfigParser.ConfigParser()
- config.readfp(f)
- f.close()
- #XXX - really need a more robust config file setup, but this will have
- # to do for now
- if config.has_section('koji'):
- for name, value in config.items('koji'):
- #note the defaults dictionary also serves to indicate which
- #options *can* be set via the config file. Such options should
- #not have a default value set in the option parser.
- if defaults.has_key(name):
- defaults[name] = value
+ # grab settings from /etc/koji.conf first, and allow them to be
+ # overridden by user config
+ for configFile in ('/etc/koji.conf', options.configFile):
+ if os.access(configFile, os.F_OK):
+ f = open(configFile)
+ config = ConfigParser.ConfigParser()
+ config.readfp(f)
+ f.close()
+ if config.has_section('koji'):
+ for name, value in config.items('koji'):
+ #note the defaults dictionary also serves to indicate which
+ #options *can* be set via the config file. Such options should
+ #not have a default value set in the option parser.
+ if defaults.has_key(name):
+ defaults[name] = value
for name, value in defaults.iteritems():
- if getattr(options, name) is None:
+ if getattr(options, name, None) is None:
setattr(options, name, value)
return options, cmd, args[1:]
@@ -3060,8 +3064,8 @@ def activate_session(session):
if options.noauth:
#skip authentication
pass
- elif options.user:
- #authenticate using user/password
+ elif os.path.isfile(os.path.expanduser(options.cert)) or options.user:
+ # authenticate using SSL client cert or user/password
session.login()
elif sys.modules.has_key('krbV'):
try:
@@ -3084,7 +3088,7 @@ if __name__ == "__main__":
options, command, args = get_options()
session_opts = {}
- for k in ('user', 'password', 'debug_xmlrpc', 'debug'):
+ for k in ('cert', 'ca', 'serverca', 'user', 'password', 'debug_xmlrpc', 'debug'):
session_opts[k] = getattr(options,k)
session = koji.ClientSession(options.server,session_opts)
rv = 0
diff --git a/cli/koji.conf b/cli/koji.conf
index de4b4f4d..123648cb 100644
--- a/cli/koji.conf
+++ b/cli/koji.conf
@@ -11,3 +11,13 @@
;path to the koji top directory
;topdir = /mnt/koji
+;configuration for SSL athentication
+
+;client certificate
+;cert = ~/.koji/client.crt
+
+;certificate of the CA that issued the client certificate
+;ca = ~/.koji/clientca.crt
+
+;certificate of the CA that issued the HTTP server certificate
+;serverca = ~/.koji/serverca.crt
diff --git a/hub/httpd.conf b/hub/httpd.conf
index 08d9242b..d60a07dc 100644
--- a/hub/httpd.conf
+++ b/hub/httpd.conf
@@ -15,6 +15,7 @@ Alias /koji-hub "/usr/share/koji-hub/XMLRPC"
PythonOption AuthPrincipal kojihub@EXAMPLE.COM
PythonOption AuthKeytab /etc/koji.keytab
PythonOption ProxyPrincipals kojihub@EXAMPLE.COM
+ PythonOption LoginCreatesUser On
PythonOption KojiWebURL http://kojiweb.example.com/koji
#format string for host principals (%s = hostname)
PythonOption HostPrincipalFormat %s@EXAMPLE.COM
@@ -26,3 +27,10 @@ Alias /koji-hub "/usr/share/koji-hub/XMLRPC"
#autoreload is mostly useless to us (it would only reload kojixmlrpc.py)
+# uncomment this to enable authentication via SSL client certificates
+#
+# SSLVerifyClient require
+# SSLVerifyDepth 10
+# SSLUserName SSL_CLIENT_S_DN_CN
+# SSLOptions +StdEnvVars +ExportCertData
+#
diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py
index 89348707..1c10e863 100644
--- a/hub/kojixmlrpc.py
+++ b/hub/kojixmlrpc.py
@@ -274,6 +274,7 @@ def handler(req, profiling=False):
h.register_module(hostFunctions,"host")
h.register_function(koji.auth.login)
h.register_function(koji.auth.krbLogin)
+ h.register_function(koji.auth.sslLogin)
h.register_function(koji.auth.logout)
h.register_function(koji.auth.subsession)
h.register_function(koji.auth.logoutChild)
diff --git a/koji/__init__.py b/koji/__init__.py
index 9066d493..99e5f02f 100644
--- a/koji/__init__.py
+++ b/koji/__init__.py
@@ -47,6 +47,7 @@ import urllib
import urlparse
import xmlrpclib
from xmlrpclib import loads, Fault
+import ssl.XMLRPCServerProxy
def _(args):
"""Stub function for translation"""
@@ -147,6 +148,7 @@ USER_STATUS = Enum((
# normal == username/password
AUTHTYPE_NORMAL = 0
AUTHTYPE_KERB = 1
+AUTHTYPE_SSL = 2
#dependency types
DEP_REQUIRE = 0
@@ -1049,6 +1051,7 @@ class ClientSession(object):
if opts == None:
opts = {}
self.opts = opts
+ self.proxyClass = xmlrpclib.ServerProxy
self.proxyOpts = {'allow_none':1}
if opts.get('debug_xmlrpc',False):
self.proxyOpts['verbose']=1
@@ -1071,14 +1074,22 @@ class ClientSession(object):
self.callnum = 0
url = "%s?%s" %(self.baseurl,urllib.urlencode(sinfo))
self.sinfo = sinfo
- self.proxy = xmlrpclib.ServerProxy(url,**self.proxyOpts)
+ self.proxy = self.proxyClass(url,**self.proxyOpts)
def login(self,opts=None):
- sinfo = self.callMethod('login',self.opts['user'], self.opts['password'],opts)
- if not sinfo:
- return False
- self.setSession(sinfo)
- return True
+ if self.opts.get('cert') and \
+ os.path.isfile(os.path.expanduser(self.opts['cert'])):
+ return self.ssl_login(os.path.expanduser(self.opts['cert']),
+ os.path.expanduser(self.opts['ca']),
+ os.path.expanduser(self.opts['serverca']))
+ elif self.opts.get('user') and self.opts.get('password'):
+ sinfo = self.callMethod('login',self.opts['user'], self.opts['password'],opts)
+ if not sinfo:
+ return False
+ self.setSession(sinfo)
+ return True
+ else:
+ raise AuthError, 'no credentials provided'
def subsession(self):
"Create a subsession"
@@ -1108,7 +1119,7 @@ class ClientSession(object):
ccache.init(cprinc)
ccache.init_creds_keytab(principal=cprinc, keytab=keytab)
else:
- raise GenericError, 'cannot specify a principal without a keytab'
+ raise AuthError, 'cannot specify a principal without a keytab'
else:
# We're trying to log ourself in. Connect using existing credentials.
cprinc = ccache.principal()
@@ -1167,6 +1178,24 @@ class ClientSession(object):
return 'host/%s@%s' % (servername, domain)
+ def ssl_login(self, cert, ca, serverca):
+ if not self.baseurl.startswith('https:'):
+ raise AuthError, '%s is not a SSL server URL, and you have configured SSL authentication' % self.baseurl
+
+ certs = {}
+ certs['key_and_cert'] = os.path.expanduser(cert)
+ certs['ca_cert'] = os.path.expanduser(ca)
+ certs['peer_ca_cert'] = os.path.expanduser(serverca)
+ # only use a timeout during login
+ self.proxy = ssl.XMLRPCServerProxy.PlgXMLRPCServerProxy(self.baseurl, certs, timeout=60)
+ sinfo = self.callMethod('sslLogin')
+ if not sinfo:
+ return False
+ self.proxyClass = ssl.XMLRPCServerProxy.PlgXMLRPCServerProxy
+ self.proxyOpts['certs'] = certs
+ self.setSession(sinfo)
+ return True
+
def logout(self):
if not self.logged_in:
return
@@ -1213,7 +1242,7 @@ class ClientSession(object):
sinfo['callnum'] = self.callnum
self.callnum += 1
url = "%s?%s" %(self.baseurl,urllib.urlencode(sinfo))
- proxy = xmlrpclib.ServerProxy(url,**self.proxyOpts)
+ proxy = self.proxyClass(url,**self.proxyOpts)
else:
proxy = self.proxy
tries = 0
diff --git a/koji/auth.py b/koji/auth.py
index 69b5de52..15cf36bb 100644
--- a/koji/auth.py
+++ b/koji/auth.py
@@ -276,7 +276,10 @@ class Session(object):
login_principal = cprinc.name
user_id = self.getUserIdFromKerberos(login_principal)
if not user_id:
- user_id = self.createUserFromKerberos(login_principal)
+ if context.opts.get('LoginCreatesUser', '').lower() in ('yes', 'on', 'true', '1'):
+ user_id = self.createUserFromKerberos(login_principal)
+ else:
+ raise koji.AuthError, 'Unknown Kerberos principal: %s' % login_principal
hostip = context.req.connection.remote_ip
if hostip == '127.0.0.1':
@@ -317,6 +320,44 @@ class Session(object):
return (local_ip, local_port, remote_ip, remote_port)
+ def sslLogin(self):
+ if self.logged_in:
+ raise koji.AuthError, "Already logged in"
+
+ # populate standard CGI variables
+ context.req.add_common_vars()
+ env = context.req.subprocess_env
+
+ if not (env.has_key('HTTPS') and
+ env['HTTPS'] == 'on'):
+ raise koji.AuthError, 'cannot call sslLogin() via a non-https connection'
+
+ subject = context.req.user
+ if not subject:
+ raise koji.AuthError, 'could not determine the subject of the client certificate'
+
+ # assume that the certificate subject is their Koji username
+ cursor = context.cnx.cursor()
+ query = """SELECT id, status FROM users
+ WHERE name = %(subject)s"""
+ cursor.execute(query, locals())
+ result = cursor.fetchone()
+ if result:
+ user_id, status = result
+ else:
+ if context.opts.get('LoginCreatesUser', '').lower() in ('yes', 'on', 'true', '1'):
+ user_id = self.createUser(subject, koji.USERTYPES['NORMAL'], '')
+ status = None
+ else:
+ raise koji.AuthError, 'Unknown user: %s' % subject
+
+ hostip = context.req.connection.remote_ip
+ if hostip == '127.0.0.1':
+ hostip = socket.gethostbyname(socket.gethostname())
+
+ sinfo = self.createSession(user_id, hostip, koji.AUTHTYPE_SSL)
+ return sinfo
+
def makeExclusive(self,force=False):
"""Make this session exclusive"""
c = context.cnx.cursor()
@@ -488,6 +529,23 @@ class Session(object):
else:
return None
+ def createUser(self, name, usertype, krb_principal):
+ """
+ Create a new user, using the provided values.
+ Return the user_id of the newly-created user.
+ """
+ cursor = context.cnx.cursor()
+ select = """SELECT nextval('users_id_seq')"""
+ cursor.execute(select, locals())
+ user_id = cursor.fetchone()[0]
+
+ insert = """INSERT INTO users (id, name, usertype, krb_principal)
+ VALUES (%(user_id)i, %(name)s, %(usertype)i, %(krb_principal)s)"""
+ cursor.execute(insert, locals())
+ context.cnx.commit()
+
+ return user_id
+
def createUserFromKerberos(self, krb_principal):
"""Create a new user, based on the Kerberos principal. Their
username will be everything before the "@" in the principal.
@@ -498,17 +556,7 @@ class Session(object):
user_name = krb_principal[:atidx]
user_type = koji.USERTYPES['NORMAL']
- c = context.cnx.cursor()
- select = """SELECT nextval('users_id_seq')"""
- c.execute(select, locals())
- user_id = c.fetchone()[0]
-
- insert = """INSERT INTO users (id, name, password, usertype, krb_principal)
- VALUES (%(user_id)i, %(user_name)s, null, %(user_type)i, %(krb_principal)s)"""
- c.execute(insert, locals())
- context.cnx.commit()
-
- return user_id
+ return self.createUser(user_name, user_type, krb_principal)
def get_user_groups(user_id):
"""Get user groups
@@ -549,6 +597,9 @@ def login(*args,**opts):
def krbLogin(*args, **opts):
return context.session.krbLogin(*args, **opts)
+def sslLogin(*args, **opts):
+ return context.session.sslLogin(*args, **opts)
+
def logout():
return context.session.logout()
diff --git a/koji/ssl/SSLCommon.py b/koji/ssl/SSLCommon.py
new file mode 100644
index 00000000..68c20c69
--- /dev/null
+++ b/koji/ssl/SSLCommon.py
@@ -0,0 +1,127 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Copyright 2005 Dan Williams and Red Hat, Inc.
+
+import os, sys
+from OpenSSL import SSL
+import SSLConnection
+import httplib
+import socket
+import SocketServer
+
+def our_verify(connection, x509, errNum, errDepth, preverifyOK):
+ # print "Verify: errNum = %s, errDepth = %s, preverifyOK = %s" % (errNum, errDepth, preverifyOK)
+
+ # preverifyOK should tell us whether or not the client's certificate
+ # correctly authenticates against the CA chain
+ return preverifyOK
+
+
+def CreateSSLContext(certs):
+ key_and_cert = certs['key_and_cert']
+ ca_cert = certs['ca_cert']
+ peer_ca_cert = certs['peer_ca_cert']
+ for f in key_and_cert, ca_cert, peer_ca_cert:
+ if f and not os.access(f, os.R_OK):
+ print "%s does not exist or is not readable." % f
+ os._exit(1)
+
+ ctx = SSL.Context(SSL.SSLv3_METHOD) # SSLv3 only
+ ctx.use_certificate_file(key_and_cert)
+ ctx.use_privatekey_file(key_and_cert)
+ ctx.load_client_ca(ca_cert)
+ ctx.load_verify_locations(peer_ca_cert)
+ verify = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT
+ ctx.set_verify(verify, our_verify)
+ ctx.set_verify_depth(10)
+ ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_TLSv1)
+ return ctx
+
+
+
+class PlgBaseServer(SocketServer.ThreadingTCPServer):
+ allow_reuse_address = 1
+
+ def __init__(self, server_addr, req_handler):
+ self._quit = False
+ self.allow_reuse_address = 1
+ SocketServer.ThreadingTCPServer.__init__(self, server_addr, req_handler)
+
+ def stop(self):
+ self._quit = True
+
+ def serve_forever(self):
+ while not self._quit:
+ self.handle_request()
+ self.server_close()
+
+
+class PlgBaseSSLServer(PlgBaseServer):
+ """ SSL-enabled variant """
+
+ def __init__(self, server_address, req_handler, certs, timeout=None):
+ self._timeout = timeout
+ self.ssl_ctx = CreateSSLContext(certs)
+
+ PlgBaseServer.__init__(self, server_address, req_handler)
+
+ sock = socket.socket(self.address_family, self.socket_type)
+ con = SSL.Connection(self.ssl_ctx, sock)
+ self.socket = SSLConnection.SSLConnection(con)
+ if sys.version_info[:3] >= (2, 3, 0):
+ self.socket.settimeout(self._timeout)
+ self.server_bind()
+ self.server_activate()
+
+ host, port = self.socket.getsockname()[:2]
+ self.server_name = socket.getfqdn(host)
+ self.server_port = port
+
+
+class PlgHTTPSConnection(httplib.HTTPConnection):
+ "This class allows communication via SSL."
+
+ response_class = httplib.HTTPResponse
+
+ def __init__(self, host, port=None, ssl_context=None, strict=None, timeout=None):
+ httplib.HTTPConnection.__init__(self, host, port, strict)
+ self.ssl_ctx = ssl_context
+ self._timeout = timeout
+
+ def connect(self):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ con = SSL.Connection(self.ssl_ctx, sock)
+ self.sock = SSLConnection.SSLConnection(con)
+ if sys.version_info[:3] >= (2, 3, 0):
+ self.sock.settimeout(self._timeout)
+ self.sock.connect((self.host, self.port))
+
+
+class PlgHTTPS(httplib.HTTP):
+ """Compatibility with 1.5 httplib interface
+
+ Python 1.5.2 did not have an HTTPS class, but it defined an
+ interface for sending http requests that is also useful for
+ https.
+ """
+
+ _http_vsn = 11
+ _http_vsn_str = 'HTTP/1.1'
+
+ _connection_class = PlgHTTPSConnection
+
+ def __init__(self, host='', port=None, ssl_context=None, strict=None, timeout=None):
+ self._setup(self._connection_class(host, port, ssl_context, strict, timeout))
+
diff --git a/koji/ssl/SSLConnection.py b/koji/ssl/SSLConnection.py
new file mode 100644
index 00000000..072320e4
--- /dev/null
+++ b/koji/ssl/SSLConnection.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+#
+# Higher-level SSL objects used by rpclib
+#
+# Copyright (c) 2002 Red Hat, Inc.
+#
+# Author: Mihai Ibanescu
+# Modifications by Dan Williams
+
+
+from OpenSSL import SSL, crypto
+import os, string, time, socket, select
+
+
+class SSLConnection:
+ """
+ This whole class exists just to filter out a parameter
+ passed in to the shutdown() method in SimpleXMLRPC.doPOST()
+ """
+
+ DEFAULT_TIMEOUT = 20
+
+ def __init__(self, conn):
+ """
+ Connection is not yet a new-style class,
+ so I'm making a proxy instead of subclassing.
+ """
+ self.__dict__["conn"] = conn
+ self.__dict__["close_refcount"] = 1
+ self.__dict__["closed"] = False
+ self.__dict__["timeout"] = self.DEFAULT_TIMEOUT
+
+ def __del__(self):
+ self.__dict__["conn"].close()
+
+ def __getattr__(self,name):
+ return getattr(self.__dict__["conn"], name)
+
+ def __setattr__(self,name, value):
+ setattr(self.__dict__["conn"], name, value)
+
+ def settimeout(self, timeout):
+ if timeout == None:
+ self.__dict__["timeout"] = self.DEFAULT_TIMEOUT
+ else:
+ self.__dict__["timeout"] = timeout
+ self.__dict__["conn"].settimeout(timeout)
+
+ def shutdown(self, how=1):
+ """
+ SimpleXMLRpcServer.doPOST calls shutdown(1),
+ and Connection.shutdown() doesn't take
+ an argument. So we just discard the argument.
+ """
+ self.__dict__["conn"].shutdown()
+
+ def accept(self):
+ """
+ This is the other part of the shutdown() workaround.
+ Since servers create new sockets, we have to infect
+ them with our magic. :)
+ """
+ c, a = self.__dict__["conn"].accept()
+ return (SSLConnection(c), a)
+
+ def makefile(self, mode, bufsize):
+ """
+ We need to use socket._fileobject Because SSL.Connection
+ doesn't have a 'dup'. Not exactly sure WHY this is, but
+ this is backed up by comments in socket.py and SSL/connection.c
+
+ Since httplib.HTTPSResponse/HTTPConnection depend on the
+ socket being duplicated when they close it, we refcount the
+ socket object and don't actually close until its count is 0.
+ """
+ self.__dict__["close_refcount"] = self.__dict__["close_refcount"] + 1
+ return PlgFileObject(self, mode, bufsize)
+
+ def close(self):
+ if self.__dict__["closed"]:
+ return
+ self.__dict__["close_refcount"] = self.__dict__["close_refcount"] - 1
+ if self.__dict__["close_refcount"] == 0:
+ self.shutdown()
+ self.__dict__["conn"].close()
+ self.__dict__["closed"] = True
+
+ def sendall(self, data, flags=0):
+ """
+ - Use select() to simulate a socket timeout without setting the socket
+ to non-blocking mode.
+ - Don't use pyOpenSSL's sendall() either, since it just loops on WantRead
+ or WantWrite, consuming 100% CPU, and never times out.
+ """
+ timeout = self.__dict__["timeout"]
+ con = self.__dict__["conn"]
+ (read, write, excpt) = select.select([], [con], [], timeout)
+ if not con in write:
+ raise socket.timeout((110, "Operation timed out."))
+
+ starttime = time.time()
+ origlen = len(data)
+ sent = -1
+ while len(data):
+ curtime = time.time()
+ if curtime - starttime > timeout:
+ raise socket.timeout((110, "Operation timed out."))
+
+ try:
+ sent = con.send(data, flags)
+ except SSL.SysCallError, e:
+ if e[0] == 32: # Broken Pipe
+ self.close()
+ sent = 0
+ else:
+ raise socket.error(e)
+ except (SSL.WantWriteError, SSL.WantReadError):
+ time.sleep(0.2)
+ continue
+
+ data = data[sent:]
+ return origlen - len(data)
+
+ def recv(self, bufsize, flags=0):
+ """
+ Use select() to simulate a socket timeout without setting the socket
+ to non-blocking mode
+ """
+ timeout = self.__dict__["timeout"]
+ con = self.__dict__["conn"]
+ (read, write, excpt) = select.select([con], [], [], timeout)
+ if not con in read:
+ raise socket.timeout((110, "Operation timed out."))
+
+ starttime = time.time()
+ while True:
+ curtime = time.time()
+ if curtime - starttime > timeout:
+ raise socket.timeout((110, "Operation timed out."))
+
+ try:
+ return con.recv(bufsize, flags)
+ except SSL.ZeroReturnError:
+ return None
+ except SSL.WantReadError:
+ time.sleep(0.2)
+ return None
+
+class PlgFileObject(socket._fileobject):
+ def close(self):
+ """
+ socket._fileobject doesn't actually _close_ the socket,
+ which we want it to do, so we have to override.
+ """
+ try:
+ if self._sock:
+ self.flush()
+ self._sock.close()
+ finally:
+ self._sock = None
+
diff --git a/koji/ssl/XMLRPCServerProxy.py b/koji/ssl/XMLRPCServerProxy.py
new file mode 100644
index 00000000..71bcee74
--- /dev/null
+++ b/koji/ssl/XMLRPCServerProxy.py
@@ -0,0 +1,175 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Modified by Dan Williams
+# Further modified by Mike Bonnet
+
+import os, sys
+import SSLCommon
+import urllib
+import xmlrpclib
+
+__version__='0.12'
+
+class PlgSSL_Transport(xmlrpclib.Transport):
+
+ user_agent = "pyOpenSSL_XMLRPC/%s - %s" % (__version__, xmlrpclib.Transport.user_agent)
+
+ def __init__(self, ssl_context, timeout=None, use_datetime=0):
+ if sys.version_info[:3] >= (2, 5, 0):
+ xmlrpclib.Transport.__init__(self, use_datetime)
+ self.ssl_ctx=ssl_context
+ self._timeout = timeout
+ self._https = None
+
+ def make_connection(self, host):
+ # Handle username and password.
+ try:
+ host, extra_headers, x509 = self.get_host_info(host)
+ except AttributeError:
+ # Yay for Python 2.2
+ pass
+ _host, _port = urllib.splitport(host)
+ self._https = SSLCommon.PlgHTTPS(_host, (_port and int(_port) or 443), ssl_context=self.ssl_ctx, timeout=self._timeout)
+ return self._https
+
+ def close(self):
+ if self._https:
+ self._https.close()
+ self._https = None
+
+
+class Plg_ClosableTransport(xmlrpclib.Transport):
+ """Override make_connection so we can close it."""
+ def __init__(self):
+ self._http = None
+
+ def make_connection(self, host):
+ # create a HTTP connection object from a host descriptor
+ import httplib
+ host, extra_headers, x509 = self.get_host_info(host)
+ self._http = httplib.HTTP(host)
+ return self._http
+
+ def close(self):
+ if self._http:
+ self._http.close()
+ self._http = None
+
+
+class PlgXMLRPCServerProxy(xmlrpclib.ServerProxy):
+ def __init__(self, uri, certs, timeout=None, verbose=0, allow_none=0):
+ if certs and len(certs) > 0:
+ self.ctx = SSLCommon.CreateSSLContext(certs)
+ self._transport = PlgSSL_Transport(ssl_context=self.ctx, timeout=timeout)
+ else:
+ self._transport = Plg_ClosableTransport()
+ xmlrpclib.ServerProxy.__init__(self, uri, transport=self._transport,
+ verbose=verbose, allow_none=allow_none)
+
+ def cancel(self):
+ self._transport.close()
+
+
+###########################################################
+# Testing stuff
+###########################################################
+
+
+import threading
+import time
+import random
+import OpenSSL
+import socket
+
+client_start = False
+
+threadlist_lock = threading.Lock()
+threadlist = {}
+
+class TestClient(threading.Thread):
+ def __init__(self, certs, num, tm):
+ self.server = PlgXMLRPCServerProxy("https://127.0.0.1:8886", certs, timeout=20)
+ self.num = i
+ self.tm = tm
+ threading.Thread.__init__(self)
+
+ def run(self):
+ while not client_start:
+ time.sleep(0.05)
+ i = 0
+ while i < 5:
+ reply = None
+ try:
+ reply = self.server.ping(self.num, i)
+ except OpenSSL.SSL.Error, e:
+ reply = "OpenSSL Error (%s)" % e
+ except socket.timeout, e:
+ reply = "Socket timeout (%s)" % e
+ threadlist_lock.acquire()
+ self.tm.inc()
+ threadlist_lock.release()
+ print "TRY(%d / %d): %s" % (self.num, i, reply)
+ time.sleep(0.05)
+ i = i + 1
+ threadlist_lock.acquire()
+ del threadlist[self]
+ threadlist_lock.release()
+
+class TimeoutCounter:
+ def __init__(self):
+ self._timedout = 0
+ self._lock = threading.Lock();
+
+ def inc(self):
+ self._lock.acquire()
+ self._timedout = self._timedout + 1
+ self._lock.release()
+
+ def get(self):
+ return self._timedout
+
+if __name__ == '__main__':
+ if len(sys.argv) < 4:
+ print "Usage: python XMLRPCServerProxy.py key_and_cert ca_cert peer_ca_cert"
+ sys.exit(1)
+
+ certs = {}
+ certs['key_and_cert'] = sys.argv[1]
+ certs['ca_cert'] = sys.argv[2]
+ certs['peer_ca_cert'] = sys.argv[3]
+
+ tm = TimeoutCounter()
+ i = 100
+ while i > 0:
+ t = TestClient(certs, i, tm)
+ threadlist[t] = None
+ print "Created thread %d." % i
+ t.start()
+ i = i - 1
+
+ time.sleep(3)
+ print "Unleashing threads."
+ client_start = True
+ while True:
+ try:
+ time.sleep(0.25)
+ threadlist_lock.acquire()
+ if len(threadlist) == 0:
+ break
+ threadlist_lock.release()
+ except KeyboardInterrupt:
+ os._exit(0)
+ print "All done. (%d timed out)" % tm.get()
+
diff --git a/koji/ssl/__init__.py b/koji/ssl/__init__.py
new file mode 100644
index 00000000..180fed66
--- /dev/null
+++ b/koji/ssl/__init__.py
@@ -0,0 +1 @@
+# identify this as the ssl module