blob: 6d79ac6a6c463bfc64b4a7d854d6f6e47e5b3bce [file] [log] [blame]
Bryan Duxbury50409112011-03-21 17:59:49 +00001#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18#
Bryan Duxbury69720412012-01-03 17:32:30 +000019
Bryan Duxbury50409112011-03-21 17:59:49 +000020import os
21import socket
22import ssl
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000023
24from thrift.transport import TSocket
Bryan Duxbury50409112011-03-21 17:59:49 +000025from thrift.transport.TTransport import TTransportException
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000026
Bryan Duxbury69720412012-01-03 17:32:30 +000027
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000028class TSSLSocket(TSocket.TSocket):
Bryan Duxbury50409112011-03-21 17:59:49 +000029 """
30 SSL implementation of client-side TSocket
31
32 This class creates outbound sockets wrapped using the
33 python standard ssl module for encrypted connections.
Bryan Duxbury69720412012-01-03 17:32:30 +000034
Bryan Duxbury50409112011-03-21 17:59:49 +000035 The protocol used is set using the class variable
36 SSL_VERSION, which must be one of ssl.PROTOCOL_* and
37 defaults to ssl.PROTOCOL_TLSv1 for greatest security.
38 """
39 SSL_VERSION = ssl.PROTOCOL_TLSv1
40
Bryan Duxbury69720412012-01-03 17:32:30 +000041 def __init__(self,
42 host='localhost',
43 port=9090,
44 validate=True,
45 ca_certs=None,
46 unix_socket=None):
47 """Create SSL TSocket
48
49 @param validate: Set to False to disable SSL certificate validation
Bryan Duxbury50409112011-03-21 17:59:49 +000050 @type validate: bool
51 @param ca_certs: Filename to the Certificate Authority pem file, possibly a
52 file downloaded from: http://curl.haxx.se/ca/cacert.pem This is passed to
53 the ssl_wrap function as the 'ca_certs' parameter.
54 @type ca_certs: str
Bryan Duxbury69720412012-01-03 17:32:30 +000055
Bryan Duxbury50409112011-03-21 17:59:49 +000056 Raises an IOError exception if validate is True and the ca_certs file is
57 None, not present or unreadable.
58 """
59 self.validate = validate
60 self.is_valid = False
61 self.peercert = None
62 if not validate:
63 self.cert_reqs = ssl.CERT_NONE
64 else:
65 self.cert_reqs = ssl.CERT_REQUIRED
66 self.ca_certs = ca_certs
Bryan Duxbury16066592011-03-22 18:06:04 +000067 if validate:
68 if ca_certs is None or not os.access(ca_certs, os.R_OK):
Bryan Duxbury69720412012-01-03 17:32:30 +000069 raise IOError('Certificate Authority ca_certs file "%s" '
70 'is not readable, cannot validate SSL '
71 'certificates.' % (ca_certs))
Bryan Duxbury16066592011-03-22 18:06:04 +000072 TSocket.TSocket.__init__(self, host, port, unix_socket)
Bryan Duxbury50409112011-03-21 17:59:49 +000073
74 def open(self):
75 try:
76 res0 = self._resolveAddr()
77 for res in res0:
Bryan Duxbury69720412012-01-03 17:32:30 +000078 sock_family, sock_type = res[0:2]
Bryan Duxbury50409112011-03-21 17:59:49 +000079 ip_port = res[4]
80 plain_sock = socket.socket(sock_family, sock_type)
Bryan Duxbury69720412012-01-03 17:32:30 +000081 self.handle = ssl.wrap_socket(plain_sock,
82 ssl_version=self.SSL_VERSION,
83 do_handshake_on_connect=True,
84 ca_certs=self.ca_certs,
85 cert_reqs=self.cert_reqs)
Bryan Duxbury50409112011-03-21 17:59:49 +000086 self.handle.settimeout(self._timeout)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000087 try:
Bryan Duxbury50409112011-03-21 17:59:49 +000088 self.handle.connect(ip_port)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000089 except socket.error, e:
Bryan Duxbury50409112011-03-21 17:59:49 +000090 if res is not res0[-1]:
91 continue
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000092 else:
Bryan Duxbury50409112011-03-21 17:59:49 +000093 raise e
94 break
95 except socket.error, e:
96 if self._unix_socket:
97 message = 'Could not connect to secure socket %s' % self._unix_socket
98 else:
99 message = 'Could not connect to %s:%d' % (self.host, self.port)
Bryan Duxbury69720412012-01-03 17:32:30 +0000100 raise TTransportException(type=TTransportException.NOT_OPEN,
101 message=message)
Bryan Duxbury50409112011-03-21 17:59:49 +0000102 if self.validate:
103 self._validate_cert()
104
105 def _validate_cert(self):
106 """internal method to validate the peer's SSL certificate, and to check the
107 commonName of the certificate to ensure it matches the hostname we
108 used to make this connection. Does not support subjectAltName records
109 in certificates.
Bryan Duxbury69720412012-01-03 17:32:30 +0000110
111 raises TTransportException if the certificate fails validation.
112 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000113 cert = self.handle.getpeercert()
114 self.peercert = cert
115 if 'subject' not in cert:
Bryan Duxbury69720412012-01-03 17:32:30 +0000116 raise TTransportException(
117 type=TTransportException.NOT_OPEN,
118 message='No SSL certificate found from %s:%s' % (self.host, self.port))
Bryan Duxbury50409112011-03-21 17:59:49 +0000119 fields = cert['subject']
120 for field in fields:
121 # ensure structure we get back is what we expect
122 if not isinstance(field, tuple):
123 continue
124 cert_pair = field[0]
125 if len(cert_pair) < 2:
126 continue
127 cert_key, cert_value = cert_pair[0:2]
128 if cert_key != 'commonName':
129 continue
130 certhost = cert_value
131 if certhost == self.host:
132 # success, cert commonName matches desired hostname
133 self.is_valid = True
Bryan Duxbury69720412012-01-03 17:32:30 +0000134 return
Bryan Duxbury50409112011-03-21 17:59:49 +0000135 else:
Bryan Duxbury69720412012-01-03 17:32:30 +0000136 raise TTransportException(
137 type=TTransportException.UNKNOWN,
138 message='Hostname we connected to "%s" doesn\'t match certificate '
139 'provided commonName "%s"' % (self.host, certhost))
140 raise TTransportException(
141 type=TTransportException.UNKNOWN,
142 message='Could not validate SSL certificate from '
143 'host "%s". Cert=%s' % (self.host, cert))
144
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000145
146class TSSLServerSocket(TSocket.TServerSocket):
Bryan Duxbury69720412012-01-03 17:32:30 +0000147 """SSL implementation of TServerSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000148
149 This uses the ssl module's wrap_socket() method to provide SSL
150 negotiated encryption.
151 """
152 SSL_VERSION = ssl.PROTOCOL_TLSv1
153
Bryan Duxbury69720412012-01-03 17:32:30 +0000154 def __init__(self,
155 host=None,
156 port=9090,
157 certfile='cert.pem',
158 unix_socket=None):
Bryan Duxbury50409112011-03-21 17:59:49 +0000159 """Initialize a TSSLServerSocket
Bryan Duxbury69720412012-01-03 17:32:30 +0000160
161 @param certfile: filename of the server certificate, defaults to cert.pem
Bryan Duxbury50409112011-03-21 17:59:49 +0000162 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000163 @param host: The hostname or IP to bind the listen socket to,
164 i.e. 'localhost' for only allowing local network connections.
165 Pass None to bind to all interfaces.
Bryan Duxbury50409112011-03-21 17:59:49 +0000166 @type host: str
167 @param port: The port to listen on for inbound connections.
168 @type port: int
169 """
170 self.setCertfile(certfile)
Bryan Duxbury16066592011-03-22 18:06:04 +0000171 TSocket.TServerSocket.__init__(self, host, port)
Bryan Duxbury50409112011-03-21 17:59:49 +0000172
173 def setCertfile(self, certfile):
174 """Set or change the server certificate file used to wrap new connections.
Bryan Duxbury69720412012-01-03 17:32:30 +0000175
176 @param certfile: The filename of the server certificate,
177 i.e. '/etc/certs/server.pem'
Bryan Duxbury50409112011-03-21 17:59:49 +0000178 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000179
Bryan Duxbury50409112011-03-21 17:59:49 +0000180 Raises an IOError exception if the certfile is not present or unreadable.
181 """
182 if not os.access(certfile, os.R_OK):
183 raise IOError('No such certfile found: %s' % (certfile))
184 self.certfile = certfile
185
186 def accept(self):
187 plain_client, addr = self.handle.accept()
Bryan Duxbury16066592011-03-22 18:06:04 +0000188 try:
189 client = ssl.wrap_socket(plain_client, certfile=self.certfile,
190 server_side=True, ssl_version=self.SSL_VERSION)
191 except ssl.SSLError, ssl_exc:
192 # failed handshake/ssl wrap, close socket to client
193 plain_client.close()
194 # raise ssl_exc
Bryan Duxbury69720412012-01-03 17:32:30 +0000195 # We can't raise the exception, because it kills most TServer derived
196 # serve() methods.
Bryan Duxbury16066592011-03-22 18:06:04 +0000197 # Instead, return None, and let the TServer instance deal with it in
198 # other exception handling. (but TSimpleServer dies anyway)
Bryan Duxbury69720412012-01-03 17:32:30 +0000199 return None
Bryan Duxbury50409112011-03-21 17:59:49 +0000200 result = TSocket.TSocket()
Bryan Duxbury50409112011-03-21 17:59:49 +0000201 result.setHandle(client)
202 return result