Skip to content

Commit 75fc804

Browse files
author
Dariusz Suchojad
committed
SESPRINGPYTHONPY-155: Replaced PyOpenSSL with 'ssl' on the server-side. Added new CA-aware classes to sp.remoting.http
1 parent b9de3a9 commit 75fc804

File tree

2 files changed

+157
-138
lines changed

2 files changed

+157
-138
lines changed

src/springpython/remoting/http.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# stdlib
4+
import httplib
5+
import socket
6+
import ssl
7+
8+
class CAValidatingHTTPSConnection(httplib.HTTPConnection):
9+
""" This class allows communication via SSL/TLS and takes Certificate Authorities
10+
into account.
11+
"""
12+
13+
def __init__(self, host, port=None, keyfile=None, certfile=None,
14+
ca_certs=None, cert_reqs=None, strict=None, ssl_version=None,
15+
timeout=None):
16+
httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
17+
18+
self.keyfile = keyfile
19+
self.certfile = certfile
20+
self.ca_certs = ca_certs
21+
self.cert_reqs = cert_reqs
22+
self.ssl_version = ssl_version
23+
24+
def connect(self):
25+
""" Connect to a host on a given (SSL/TLS) port.
26+
"""
27+
sock = socket.create_connection((self.host, self.port), self.timeout)
28+
if self._tunnel_host:
29+
self.sock = sock
30+
self._tunnel()
31+
32+
self.sock = self.wrap_socket(sock)
33+
34+
def wrap_socket(self, sock):
35+
""" Gets a socket object and wraps it into an SSL/TLS-aware one. May be
36+
overridden in subclasses if the wrapping process needs to be customized.
37+
"""
38+
return ssl.wrap_socket(sock, self.keyfile, self.certfile,
39+
ca_certs=self.ca_certs, cert_reqs=self.cert_reqs,
40+
ssl_version=self.ssl_version)
41+
42+
class CAValidatingHTTPS(httplib.HTTP):
43+
""" A subclass of httplib.HTTP which is aware of Certificate Authorities
44+
used in SSL/TLS transactions.
45+
"""
46+
_connection_class = CAValidatingHTTPSConnection
47+
48+
def __init__(self, host=None, port=None, strict=None, keyfile=None, certfile=None, ca_certs=None,
49+
cert_reqs=None, ssl_version=None, timeout=None):
50+
self._setup(self._connection_class(host, port, keyfile, certfile, ca_certs,
51+
cert_reqs, strict, ssl_version, timeout))

src/springpython/remoting/xmlrpc.py

Lines changed: 106 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
import logging
66
import socket
77
import ssl
8+
import sys
9+
import traceback
810

911
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
1012
from xmlrpclib import ServerProxy, Error, Transport
1113

12-
# PyOpenSSL
13-
from OpenSSL import SSL
14-
1514
# Spring Python
16-
from springpython.util import TRACE1
15+
from springpython.remoting.http import CAValidatingHTTPS
1716

18-
__all__ = ["VerificationException", "SSLXMLRPCServer", "SSLXMLRPCClient"]
17+
__all__ = ["VerificationException", "SSLServer", "SSLClient"]
1918

2019
class VerificationException(Exception):
2120
""" Raised when the verification of a certificate's fields fails.
@@ -25,37 +24,6 @@ class VerificationException(Exception):
2524
# Server
2625
# ##############################################################################
2726

28-
# A slightly modified version of the public-domain code from
29-
# http://skvidal.fedorapeople.org/SecureXMLRPCServer.py
30-
class SSLSocketWrapper(object):
31-
""" This whole class exists just to filter out a parameter
32-
passed in to the shutdown() method in SimpleXMLRPC.doPOST()
33-
"""
34-
def __init__(self, conn):
35-
""" Connection is not yet a new-style class, so I'm making a proxy
36-
instead of subclassing."""
37-
self.__dict__["conn"] = conn
38-
39-
def __getattr__(self,name):
40-
return getattr(self.__dict__["conn"], name)
41-
42-
def __setattr__(self,name, value):
43-
setattr(self.__dict__["conn"], name, value)
44-
45-
def shutdown(self, how=1):
46-
""" SimpleXMLRpcServer.doPOST calls shutdown(1), and Connection.shutdown()
47-
doesn't take an argument. So we just discard the argument.
48-
"""
49-
self.__dict__["conn"].shutdown()
50-
51-
def accept(self):
52-
""" This is the other part of the shutdown() workaround. Since servers
53-
create new sockets, we have to infect them with our magic.
54-
"""
55-
c, a = self.__dict__["conn"].accept()
56-
return (SSLSocketWrapper(c), a)
57-
58-
5927
class RequestHandler(SimpleXMLRPCRequestHandler):
6028
rpc_paths = ("/", "/RPC2",)
6129

@@ -64,154 +32,154 @@ def setup(self):
6432
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
6533
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
6634

67-
class SSLXMLRPCServer(object, SimpleXMLRPCServer):
68-
def __init__(self, host=None, port=None, key_file=None, cert_file=None,
69-
ca_certs=None, cipher_list="DEFAULT", ssl_method=SSL.TLSv1_METHOD,
70-
ctx_options=SSL.OP_NO_SSLv2,
71-
verify_options=SSL.VERIFY_NONE,
72-
ssl_verify_depth=1, verify_fields=None):
35+
class SSLServer(object, SimpleXMLRPCServer):
36+
def __init__(self, host=None, port=None, ca_certs=None, keyfile=None, certfile=None,
37+
cert_reqs=ssl.CERT_OPTIONAL, ssl_version=ssl.PROTOCOL_TLSv1,
38+
do_handshake_on_connect=True, suppress_ragged_eofs=True, ciphers=None, **kwargs):
7339

7440
SimpleXMLRPCServer.__init__(self, (host, port), requestHandler=RequestHandler)
7541
self.logger = logging.getLogger(self.__class__.__name__)
76-
self.register_functions()
7742

78-
ctx = SSL.Context(ssl_method)
79-
ctx.set_options(ctx_options)
43+
self.ca_certs = ca_certs
44+
self.keyfile = keyfile
45+
self.certfile = certfile
46+
self.cert_reqs = cert_reqs
47+
self.ssl_version = ssl_version
48+
self.do_handshake_on_connect = do_handshake_on_connect
49+
self.suppress_ragged_eofs = suppress_ragged_eofs
50+
self.ciphers = ciphers
8051

81-
ctx.use_privatekey_file(key_file)
52+
# 'verify_fields' is taken from kwargs to allow for adding more keywords
53+
# in future versions.
54+
self.verify_fields = kwargs.get("verify_fields")
8255

83-
if cert_file:
84-
ctx.use_certificate_file(cert_file)
56+
self.register_functions()
8557

86-
if ca_certs:
87-
ctx.load_verify_locations(ca_certs)
58+
def get_request(self):
59+
""" Overridden from Socket.TCPServer.get_request, wraps the socket in
60+
an SSL context.
61+
"""
62+
sock, from_addr = self.socket.accept()
8863

89-
ctx.set_cipher_list(cipher_list)
64+
# 'ciphers' argument is new in 2.7 and we must support 2.6 so add it
65+
# to kwargs conditionally, depending on the Python version.
9066

91-
ctx.set_verify_depth(ssl_verify_depth)
92-
ctx.set_verify(verify_options, self.on_verify_peer)
93-
self.verify_fields = verify_fields
67+
kwargs = {"keyfile":self.keyfile, "certfile":self.certfile,
68+
"server_side":True, "cert_reqs":self.cert_reqs, "ssl_version":self.ssl_version,
69+
"ca_certs":self.ca_certs, "do_handshake_on_connect":self.do_handshake_on_connect,
70+
"suppress_ragged_eofs":self.suppress_ragged_eofs}
9471

95-
self.socket = SSLSocketWrapper(SSL.Connection(ctx,
96-
socket.socket(self.address_family, self.socket_type)))
72+
if sys.version_info >= (2, 7):
73+
kwargs["ciphers"] = self.ciphers
9774

98-
self.server_bind()
99-
self.server_activate()
75+
sock = ssl.wrap_socket(sock, **kwargs)
76+
return sock, from_addr
10077

101-
def on_verify_peer(self, conn, x509, error_number, error_depth, return_code):
102-
""" Verifies the other side's certificate. May be overridden in subclasses
103-
if the verification process needs to be customized.
78+
def verify_request(self, sock, from_addr):
79+
""" Overridden from Socket.TCPServer.verify_request, adds validation of the
80+
other side's certificate fields.
10481
"""
82+
try:
83+
if self.verify_fields:
10584

106-
if self.logger.isEnabledFor(TRACE1):
107-
self.logger.log(TRACE1, "on_verify_peer '%s', '%s', '%s', '%s'" % (
108-
error_number, error_depth, return_code))
109-
110-
# error_depth = 0 means we're dealing with the client's certificate
111-
# and not that of a CA.
112-
if self.verify_fields and error_depth == 0:
113-
114-
components = x509.get_subject().get_components()
115-
components = dict(components)
85+
cert = sock.getpeercert()
86+
if not cert:
87+
msg = "Couldn't verify fields, peer didn't send the certificate, from_addr='%s'" % (from_addr,)
88+
raise VerificationException(msg)
11689

117-
if self.logger.isEnabledFor(TRACE1):
118-
self.logger.log(TRACE1, "components received '%s'" % components)
90+
allow_peer, reason = self.verify_peer(cert)
91+
if not allow_peer:
92+
self.logger.error(reason)
93+
sock.close()
94+
return False
11995

120-
for verify_field in self.verify_fields:
96+
except Exception, e:
12197

122-
expected_value = self.verify_fields[verify_field]
123-
cert_value = components.get(verify_field, None)
98+
# It was either an error on our side or the client didn't send the
99+
# certificate even though self.cert_reqs was CERT_OPTIONAL (it couldn't
100+
# have been CERT_REQUIRED because we wouldn't have got so far, the
101+
# session would've been terminated much earlier in ssl.wrap_socket call).
102+
# Regardless of the reason we cannot accept the client in that case.
124103

125-
if not cert_value:
126-
msg = "Peer didn't send the '%s' field, fields received '%s'" % (
127-
verify_field, components)
128-
raise VerificationException(msg)
104+
msg = "Verification error='%s', cert='%s', from_addr='%s'" % (
105+
traceback.format_exc(e), sock.getpeercert(), from_addr)
106+
self.logger.error(msg)
129107

130-
if expected_value != cert_value:
131-
msg = "Expected the field '%s' to have value '%s' instead of '%s'" % (
132-
verify_field, expected_value, cert_value)
133-
raise VerificationException(msg)
108+
sock.close()
109+
return False
134110

135111
return True
136112

137-
def register_functions(self):
138-
raise NotImplementedError("Must be overridden by subclasses")
139-
140-
# ##############################################################################
141-
# Client
142-
# ##############################################################################
113+
def verify_peer(self, cert):
114+
""" Verifies the other side's certificate. May be overridden in subclasses
115+
if the verification process needs to be customized.
116+
"""
143117

118+
if self.logger.isEnabledFor(logging.DEBUG):
119+
self.logger.debug("verify_peer cert='%s'" % (cert))
144120

145-
class CAValidatingHTTPSConnection(httplib.HTTPConnection):
146-
""" This class allows communication via SSL and takes the CAs into account.
147-
"""
121+
subject = cert.get("subject")
122+
if not subject:
123+
msg = "Peer certificate doesn't have the 'subject' field, cert='%s'" % cert
124+
raise VerificationException(msg)
148125

149-
def __init__(self, host, port=None, key_file=None, cert_file=None,
150-
ca_certs=None, cert_reqs=None, strict=None, ssl_version=None,
151-
timeout=None):
152-
httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
126+
subject = dict(elem[0] for elem in subject)
153127

154-
self.key_file = key_file
155-
self.cert_file = cert_file
156-
self.ca_certs = ca_certs
157-
self.cert_reqs = cert_reqs
158-
self.ssl_version = ssl_version
128+
for verify_field in self.verify_fields:
159129

160-
def connect(self):
161-
""" Connect to a host on a given (SSL) port.
162-
"""
130+
expected_value = self.verify_fields[verify_field]
131+
cert_value = subject.get(verify_field, None)
163132

164-
sock = socket.create_connection((self.host, self.port), self.timeout)
165-
if self._tunnel_host:
166-
self.sock = sock
167-
self._tunnel()
133+
if not cert_value:
134+
reason = "Peer didn't send the '%s' field, subject fields received '%s'" % (
135+
verify_field, subject)
136+
return False, reason
168137

169-
self.sock = self.wrap_socket(sock)
138+
if expected_value != cert_value:
139+
reason = "Expected the subject field '%s' to have value '%s' instead of '%s'" % (
140+
verify_field, expected_value, subject)
141+
return False, reason
170142

171-
def wrap_socket(self, sock):
172-
""" Gets a socket object and wraps it into an SSL-aware one. May be
173-
overridden in subclasses if the wrapping process needs to be customized.
174-
"""
175-
return ssl.wrap_socket(sock, self.key_file, self.cert_file,
176-
ca_certs=self.ca_certs, cert_reqs=self.cert_reqs,
177-
ssl_version=self.ssl_version)
143+
return True, None
178144

179-
class CAHTTPS(httplib.HTTP):
180-
_connection_class = CAValidatingHTTPSConnection
145+
def register_functions(self):
146+
raise NotImplementedError("Must be overridden by subclasses")
181147

182-
def __init__(self, host=None, port=None, key_file=None, cert_file=None, ca_certs=None,
183-
cert_reqs=None, strict=None, ssl_version=None, timeout=None):
184-
self._setup(self._connection_class(host, port, key_file, cert_file, ca_certs,
185-
cert_reqs, strict, ssl_version, timeout))
148+
# ##############################################################################
149+
# Client
150+
# ##############################################################################
186151

187152
class SSLClientTransport(Transport):
188153
""" Handles an HTTPS transaction to an XML-RPC server.
189154
"""
190-
def __init__(self, key_file=None, cert_file=None, ca_certs=None, cert_reqs=None,
191-
ssl_version=None, timeout=None):
192-
self.key_file = key_file
193-
self.cert_file = cert_file
155+
def __init__(self, keyfile=None, certfile=None, ca_certs=None, cert_reqs=None,
156+
ssl_version=None, timeout=None, strict=None):
157+
self.keyfile = keyfile
158+
self.certfile = certfile
194159
self.ca_certs = ca_certs
195160
self.cert_reqs = cert_reqs
196161
self.ssl_version = ssl_version
197162
self.timeout = timeout
163+
self.strict = strict
198164

199165
Transport.__init__(self)
200166

201167
def make_connection(self, host):
202-
return CAHTTPS(host, key_file=self.key_file, cert_file=self.cert_file,
203-
ca_certs=self.ca_certs, cert_reqs=self.cert_reqs,
204-
ssl_version=self.ssl_version, timeout=self.timeout)
168+
return CAValidatingHTTPS(host, strict=self.strict, keyfile=self.keyfile,
169+
certfile=self.certfile, ca_certs=self.ca_certs, cert_reqs=self.cert_reqs,
170+
ssl_version=self.ssl_version, timeout=self.timeout)
205171

206-
class SSLXMLRPCClient(ServerProxy):
207-
def __init__(self, uri=None, transport=None, encoding=None, verbose=0,
208-
allow_none=0, use_datetime=0, key_file=None, cert_file=None,
209-
ca_certs=None, cert_reqs=ssl.CERT_OPTIONAL, ssl_version=ssl.PROTOCOL_TLSv1,
210-
timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
172+
class SSLClient(ServerProxy):
173+
def __init__(self, uri=None, ca_certs=None, keyfile=None, certfile=None,
174+
cert_reqs=ssl.CERT_OPTIONAL, ssl_version=ssl.PROTOCOL_TLSv1,
175+
transport=None, encoding=None, verbose=0, allow_none=0, use_datetime=0,
176+
timeout=socket._GLOBAL_DEFAULT_TIMEOUT, strict=None):
211177

212178
if not transport:
213-
transport=SSLClientTransport(key_file, cert_file, ca_certs, cert_reqs,
214-
ssl_version, timeout)
179+
transport=SSLClientTransport(keyfile, certfile, ca_certs, cert_reqs,
180+
ssl_version, timeout, strict)
215181

216182
ServerProxy.__init__(self, uri, transport, encoding, verbose,
217-
allow_none, use_datetime)
183+
allow_none, use_datetime)
184+
185+
self.logger = logging.getLogger(self.__class__.__name__)

0 commit comments

Comments
 (0)