blob: 7bf28aa77bf2b796a93e1f09927295f3ee3b2607 [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)
Jake Farrell6c5ee6a2012-05-18 01:00:39 +000089 except socket.error as 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
Jake Farrell6c5ee6a2012-05-18 01:00:39 +000095 except socket.error as e:
Bryan Duxbury50409112011-03-21 17:59:49 +000096 if self._unix_socket:
Roger Meier52820d02012-11-08 23:11:14 +000097 message = 'Could not connect to secure socket %s: %s' \
98 % (self._unix_socket, e)
Bryan Duxbury50409112011-03-21 17:59:49 +000099 else:
Roger Meier52820d02012-11-08 23:11:14 +0000100 message = 'Could not connect to %s:%d: %s' % (self.host, self.port, e)
Bryan Duxbury69720412012-01-03 17:32:30 +0000101 raise TTransportException(type=TTransportException.NOT_OPEN,
102 message=message)
Bryan Duxbury50409112011-03-21 17:59:49 +0000103 if self.validate:
104 self._validate_cert()
105
106 def _validate_cert(self):
107 """internal method to validate the peer's SSL certificate, and to check the
108 commonName of the certificate to ensure it matches the hostname we
109 used to make this connection. Does not support subjectAltName records
110 in certificates.
Bryan Duxbury69720412012-01-03 17:32:30 +0000111
112 raises TTransportException if the certificate fails validation.
113 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000114 cert = self.handle.getpeercert()
115 self.peercert = cert
116 if 'subject' not in cert:
Bryan Duxbury69720412012-01-03 17:32:30 +0000117 raise TTransportException(
118 type=TTransportException.NOT_OPEN,
119 message='No SSL certificate found from %s:%s' % (self.host, self.port))
Bryan Duxbury50409112011-03-21 17:59:49 +0000120 fields = cert['subject']
121 for field in fields:
122 # ensure structure we get back is what we expect
123 if not isinstance(field, tuple):
124 continue
125 cert_pair = field[0]
126 if len(cert_pair) < 2:
127 continue
128 cert_key, cert_value = cert_pair[0:2]
129 if cert_key != 'commonName':
130 continue
131 certhost = cert_value
132 if certhost == self.host:
133 # success, cert commonName matches desired hostname
134 self.is_valid = True
Bryan Duxbury69720412012-01-03 17:32:30 +0000135 return
Bryan Duxbury50409112011-03-21 17:59:49 +0000136 else:
Bryan Duxbury69720412012-01-03 17:32:30 +0000137 raise TTransportException(
138 type=TTransportException.UNKNOWN,
139 message='Hostname we connected to "%s" doesn\'t match certificate '
140 'provided commonName "%s"' % (self.host, certhost))
141 raise TTransportException(
142 type=TTransportException.UNKNOWN,
143 message='Could not validate SSL certificate from '
144 'host "%s". Cert=%s' % (self.host, cert))
145
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000146
147class TSSLServerSocket(TSocket.TServerSocket):
Bryan Duxbury69720412012-01-03 17:32:30 +0000148 """SSL implementation of TServerSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000149
150 This uses the ssl module's wrap_socket() method to provide SSL
151 negotiated encryption.
152 """
153 SSL_VERSION = ssl.PROTOCOL_TLSv1
154
Bryan Duxbury69720412012-01-03 17:32:30 +0000155 def __init__(self,
156 host=None,
157 port=9090,
158 certfile='cert.pem',
159 unix_socket=None):
Bryan Duxbury50409112011-03-21 17:59:49 +0000160 """Initialize a TSSLServerSocket
Bryan Duxbury69720412012-01-03 17:32:30 +0000161
162 @param certfile: filename of the server certificate, defaults to cert.pem
Bryan Duxbury50409112011-03-21 17:59:49 +0000163 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000164 @param host: The hostname or IP to bind the listen socket to,
165 i.e. 'localhost' for only allowing local network connections.
166 Pass None to bind to all interfaces.
Bryan Duxbury50409112011-03-21 17:59:49 +0000167 @type host: str
168 @param port: The port to listen on for inbound connections.
169 @type port: int
170 """
171 self.setCertfile(certfile)
Bryan Duxbury16066592011-03-22 18:06:04 +0000172 TSocket.TServerSocket.__init__(self, host, port)
Bryan Duxbury50409112011-03-21 17:59:49 +0000173
174 def setCertfile(self, certfile):
175 """Set or change the server certificate file used to wrap new connections.
Bryan Duxbury69720412012-01-03 17:32:30 +0000176
177 @param certfile: The filename of the server certificate,
178 i.e. '/etc/certs/server.pem'
Bryan Duxbury50409112011-03-21 17:59:49 +0000179 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000180
Bryan Duxbury50409112011-03-21 17:59:49 +0000181 Raises an IOError exception if the certfile is not present or unreadable.
182 """
183 if not os.access(certfile, os.R_OK):
184 raise IOError('No such certfile found: %s' % (certfile))
185 self.certfile = certfile
186
187 def accept(self):
188 plain_client, addr = self.handle.accept()
Bryan Duxbury16066592011-03-22 18:06:04 +0000189 try:
190 client = ssl.wrap_socket(plain_client, certfile=self.certfile,
191 server_side=True, ssl_version=self.SSL_VERSION)
Jake Farrell6c5ee6a2012-05-18 01:00:39 +0000192 except ssl.SSLError as ssl_exc:
Bryan Duxbury16066592011-03-22 18:06:04 +0000193 # failed handshake/ssl wrap, close socket to client
194 plain_client.close()
195 # raise ssl_exc
Bryan Duxbury69720412012-01-03 17:32:30 +0000196 # We can't raise the exception, because it kills most TServer derived
197 # serve() methods.
Bryan Duxbury16066592011-03-22 18:06:04 +0000198 # Instead, return None, and let the TServer instance deal with it in
199 # other exception handling. (but TSimpleServer dies anyway)
Bryan Duxbury69720412012-01-03 17:32:30 +0000200 return None
Bryan Duxbury50409112011-03-21 17:59:49 +0000201 result = TSocket.TSocket()
Bryan Duxbury50409112011-03-21 17:59:49 +0000202 result.setHandle(client)
203 return result