blob: 3f1a909dff3a33b036b98e1cd7b6827a625d7e11 [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
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090020import logging
Bryan Duxbury50409112011-03-21 17:59:49 +000021import os
22import socket
23import ssl
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090024import sys
25import warnings
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000026
27from thrift.transport import TSocket
Bryan Duxbury50409112011-03-21 17:59:49 +000028from thrift.transport.TTransport import TTransportException
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000029
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090030logger = logging.getLogger(__name__)
31warnings.filterwarnings('default', category=DeprecationWarning, module=__name__)
Bryan Duxbury69720412012-01-03 17:32:30 +000032
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090033
34class TSSLBase(object):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090035 # SSLContext is not available for Python < 2.7.9
36 _has_ssl_context = sys.hexversion >= 0x020709F0
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090037
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090038 # ciphers argument is not available for Python < 2.7.0
39 _has_ciphers = sys.hexversion >= 0x020700F0
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090040
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090041 # For pythoon >= 2.7.9, use latest TLS that both client and server supports.
42 # SSL 2.0 and 3.0 are disabled via ssl.OP_NO_SSLv2 and ssl.OP_NO_SSLv3.
43 # For pythoon < 2.7.9, use TLS 1.0 since TLSv1_X nare OP_NO_SSLvX are unavailable.
44 _default_protocol = ssl.PROTOCOL_SSLv23 if _has_ssl_context else ssl.PROTOCOL_TLSv1
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090045
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090046 def _init_context(self, ssl_version):
47 if self._has_ssl_context:
48 self._context = ssl.SSLContext(ssl_version)
49 if self._context.protocol == ssl.PROTOCOL_SSLv23:
50 self._context.options |= ssl.OP_NO_SSLv2
51 self._context.options |= ssl.OP_NO_SSLv3
52 else:
53 self._context = None
54 self._ssl_version = ssl_version
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090055
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090056 @property
57 def ssl_version(self):
58 if self._has_ssl_context:
59 return self.ssl_context.protocol
60 else:
61 return self._ssl_version
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090062
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090063 @property
64 def ssl_context(self):
65 return self._context
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090066
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090067 SSL_VERSION = _default_protocol
68 """
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090069 Default SSL version.
70 For backword compatibility, it can be modified.
71 Use __init__ keywoard argument "ssl_version" instead.
72 """
73
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090074 def _deprecated_arg(self, args, kwargs, pos, key):
75 if len(args) <= pos:
76 return
77 real_pos = pos + 3
78 warnings.warn(
79 '%dth positional argument is deprecated. Use keyward argument insteand.' % real_pos,
80 DeprecationWarning)
81 if key in kwargs:
82 raise TypeError('Duplicate argument: %dth argument and %s keyward argument.', (real_pos, key))
83 kwargs[key] = args[pos]
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090084
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090085 def _unix_socket_arg(self, host, port, args, kwargs):
86 key = 'unix_socket'
87 if host is None and port is None and len(args) == 1 and key not in kwargs:
88 kwargs[key] = args[0]
89 return True
90 return False
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090091
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090092 def __getattr__(self, key):
93 if key == 'SSL_VERSION':
94 warnings.warn('Use ssl_version attribute instead.', DeprecationWarning)
95 return self.ssl_version
Nobuaki Sukegawaad835862015-12-23 23:32:09 +090096
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090097 def __init__(self, server_side, host, ssl_opts):
98 self._server_side = server_side
99 if TSSLBase.SSL_VERSION != self._default_protocol:
100 warnings.warn('SSL_VERSION is deprecated. Use ssl_version keyward argument instead.', DeprecationWarning)
101 self._context = ssl_opts.pop('ssl_context', None)
102 self._server_hostname = None
103 if not self._server_side:
104 self._server_hostname = ssl_opts.pop('server_hostname', host)
105 if self._context:
106 self._custom_context = True
107 if ssl_opts:
108 raise ValueError('Incompatible arguments: ssl_context and %s' % ' '.join(ssl_opts.keys()))
109 if not self._has_ssl_context:
110 raise ValueError('ssl_context is not available for this version of Python')
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900111 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900112 self._custom_context = False
113 ssl_version = ssl_opts.pop('ssl_version', TSSLBase.SSL_VERSION)
114 self._init_context(ssl_version)
115 self.cert_reqs = ssl_opts.pop('cert_reqs', ssl.CERT_REQUIRED)
116 self.ca_certs = ssl_opts.pop('ca_certs', None)
117 self.keyfile = ssl_opts.pop('keyfile', None)
118 self.certfile = ssl_opts.pop('certfile', None)
119 self.ciphers = ssl_opts.pop('ciphers', None)
120
121 if ssl_opts:
122 raise ValueError('Unknown keyword arguments: ', ' '.join(ssl_opts.keys()))
123
124 if self.cert_reqs != ssl.CERT_NONE:
125 if not self.ca_certs:
126 raise ValueError('ca_certs is needed when cert_reqs is not ssl.CERT_NONE')
127 if not os.access(self.ca_certs, os.R_OK):
128 raise IOError('Certificate Authority ca_certs file "%s" '
129 'is not readable, cannot validate SSL '
130 'certificates.' % (self.ca_certs))
131
132 @property
133 def certfile(self):
134 return self._certfile
135
136 @certfile.setter
137 def certfile(self, certfile):
138 if self._server_side and not certfile:
139 raise ValueError('certfile is needed for server-side')
140 if certfile and not os.access(certfile, os.R_OK):
141 raise IOError('No such certfile found: %s' % (certfile))
142 self._certfile = certfile
143
144 def _wrap_socket(self, sock):
145 if self._has_ssl_context:
146 if not self._custom_context:
147 self.ssl_context.verify_mode = self.cert_reqs
148 if self.certfile:
149 self.ssl_context.load_cert_chain(self.certfile, self.keyfile)
150 if self.ciphers:
151 self.ssl_context.set_ciphers(self.ciphers)
152 if self.ca_certs:
153 self.ssl_context.load_verify_locations(self.ca_certs)
154 return self.ssl_context.wrap_socket(sock, server_side=self._server_side,
155 server_hostname=self._server_hostname)
156 else:
157 ssl_opts = {
158 'ssl_version': self._ssl_version,
159 'server_side': self._server_side,
160 'ca_certs': self.ca_certs,
161 'keyfile': self.keyfile,
162 'certfile': self.certfile,
163 'cert_reqs': self.cert_reqs,
164 }
165 if self.ciphers:
166 if self._has_ciphers:
167 ssl_opts['ciphers'] = self.ciphers
168 else:
169 logger.warning('ciphers is specified but ignored due to old Python version')
170 return ssl.wrap_socket(sock, **ssl_opts)
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900171
172
173class TSSLSocket(TSocket.TSocket, TSSLBase):
Bryan Duxbury50409112011-03-21 17:59:49 +0000174 """
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900175 SSL implementation of TSocket
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900176
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900177 This class creates outbound sockets wrapped using the
178 python standard ssl module for encrypted connections.
179 """
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900180
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900181 # New signature
182 # def __init__(self, host='localhost', port=9090, unix_socket=None, **ssl_args):
183 # Deprecated signature
184 # def __init__(self, host='localhost', port=9090, validate=True, ca_certs=None, keyfile=None, certfile=None, unix_socket=None, ciphers=None):
185 def __init__(self, host='localhost', port=9090, *args, **kwargs):
186 """Positional arguments: ``host``, ``port``, ``unix_socket``
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900187
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900188 Keyword arguments: ``keyfile``, ``certfile``, ``cert_reqs``, ``ssl_version``,
189 ``ca_certs``, ``ciphers`` (Python 2.7.0 or later),
190 ``server_hostname`` (Python 2.7.9 or later)
191 Passed to ssl.wrap_socket. See ssl.wrap_socket documentation.
Bryan Duxbury50409112011-03-21 17:59:49 +0000192
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900193 Alternative keywoard arguments: (Python 2.7.9 or later)
194 ``ssl_context``: ssl.SSLContext to be used for SSLContext.wrap_socket
195 ``server_hostname``: Passed to SSLContext.wrap_socket
196 """
197 self.is_valid = False
198 self.peercert = None
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900199
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900200 if args:
201 if len(args) > 6:
202 raise TypeError('Too many positional argument')
203 if not self._unix_socket_arg(host, port, args, kwargs):
204 self._deprecated_arg(args, kwargs, 0, 'validate')
205 self._deprecated_arg(args, kwargs, 1, 'ca_certs')
206 self._deprecated_arg(args, kwargs, 2, 'keyfile')
207 self._deprecated_arg(args, kwargs, 3, 'certfile')
208 self._deprecated_arg(args, kwargs, 4, 'unix_socket')
209 self._deprecated_arg(args, kwargs, 5, 'ciphers')
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900210
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900211 validate = kwargs.pop('validate', None)
212 if validate is not None:
213 cert_reqs_name = 'CERT_REQUIRED' if validate else 'CERT_NONE'
214 warnings.warn(
215 'validate is deprecated. Use cert_reqs=ssl.%s instead' % cert_reqs_name,
216 DeprecationWarning)
217 if 'cert_reqs' in kwargs:
218 raise TypeError('Cannot specify both validate and cert_reqs')
219 kwargs['cert_reqs'] = ssl.CERT_REQUIRED if validate else ssl.CERT_NONE
220
221 unix_socket = kwargs.pop('unix_socket', None)
222 TSSLBase.__init__(self, False, host, kwargs)
223 TSocket.TSocket.__init__(self, host, port, unix_socket)
224
225 @property
226 def validate(self):
227 warnings.warn('Use cert_reqs instead', DeprecationWarning)
228 return self.cert_reqs != ssl.CERT_NONE
229
230 @validate.setter
231 def validate(self, value):
232 warnings.warn('Use cert_reqs instead', DeprecationWarning)
233 self.cert_reqs = ssl.CERT_REQUIRED if value else ssl.CERT_NONE
234
235 def open(self):
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000236 try:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900237 res0 = self._resolveAddr()
238 for res in res0:
239 sock_family, sock_type = res[0:2]
240 ip_port = res[4]
241 plain_sock = socket.socket(sock_family, sock_type)
242 self.handle = self._wrap_socket(plain_sock)
243 self.handle.settimeout(self._timeout)
244 try:
245 self.handle.connect(ip_port)
246 except socket.error as e:
247 if res is not res0[-1]:
248 logger.warning('Error while connecting with %s. Trying next one.', ip_port, exc_info=True)
249 continue
250 else:
251 raise
252 break
jfarrelld565e2f2015-03-18 21:02:47 -0400253 except socket.error as e:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900254 if self._unix_socket:
255 message = 'Could not connect to secure socket %s: %s' \
256 % (self._unix_socket, e)
257 else:
258 message = 'Could not connect to %s:%d: %s' % (self.host, self.port, e)
259 logger.error('Error while connecting with %s.', ip_port, exc_info=True)
260 raise TTransportException(type=TTransportException.NOT_OPEN,
261 message=message)
262 if self.validate:
263 self._validate_cert()
Bryan Duxbury50409112011-03-21 17:59:49 +0000264
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900265 def _validate_cert(self):
266 """internal method to validate the peer's SSL certificate, and to check the
267 commonName of the certificate to ensure it matches the hostname we
268 used to make this connection. Does not support subjectAltName records
269 in certificates.
Bryan Duxbury69720412012-01-03 17:32:30 +0000270
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900271 raises TTransportException if the certificate fails validation.
272 """
273 cert = self.handle.getpeercert()
274 self.peercert = cert
275 if 'subject' not in cert:
276 raise TTransportException(
277 type=TTransportException.NOT_OPEN,
278 message='No SSL certificate found from %s:%s' % (self.host, self.port))
279 fields = cert['subject']
280 for field in fields:
281 # ensure structure we get back is what we expect
282 if not isinstance(field, tuple):
283 continue
284 cert_pair = field[0]
285 if len(cert_pair) < 2:
286 continue
287 cert_key, cert_value = cert_pair[0:2]
288 if cert_key != 'commonName':
289 continue
290 certhost = cert_value
291 # this check should be performed by some sort of Access Manager
292 if certhost == self.host:
293 # success, cert commonName matches desired hostname
294 self.is_valid = True
295 return
296 else:
297 raise TTransportException(
298 type=TTransportException.UNKNOWN,
299 message='Hostname we connected to "%s" doesn\'t match certificate '
300 'provided commonName "%s"' % (self.host, certhost))
Bryan Duxbury69720412012-01-03 17:32:30 +0000301 raise TTransportException(
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900302 type=TTransportException.UNKNOWN,
303 message='Could not validate SSL certificate from '
304 'host "%s". Cert=%s' % (self.host, cert))
Bryan Duxbury69720412012-01-03 17:32:30 +0000305
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000306
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900307class TSSLServerSocket(TSocket.TServerSocket, TSSLBase):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900308 """SSL implementation of TServerSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000309
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900310 This uses the ssl module's wrap_socket() method to provide SSL
311 negotiated encryption.
Bryan Duxbury50409112011-03-21 17:59:49 +0000312 """
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900313
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900314 # New signature
315 # def __init__(self, host='localhost', port=9090, unix_socket=None, **ssl_args):
316 # Deprecated signature
317 # def __init__(self, host=None, port=9090, certfile='cert.pem', unix_socket=None, ciphers=None):
318 def __init__(self, host=None, port=9090, *args, **kwargs):
319 """Positional arguments: ``host``, ``port``, ``unix_socket``
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900320
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900321 Keyword arguments: ``keyfile``, ``certfile``, ``cert_reqs``, ``ssl_version``,
322 ``ca_certs``, ``ciphers`` (Python 2.7.0 or later)
323 See ssl.wrap_socket documentation.
Bryan Duxbury50409112011-03-21 17:59:49 +0000324
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900325 Alternative keywoard arguments: (Python 2.7.9 or later)
326 ``ssl_context``: ssl.SSLContext to be used for SSLContext.wrap_socket
327 ``server_hostname``: Passed to SSLContext.wrap_socket
328 """
329 if args:
330 if len(args) > 3:
331 raise TypeError('Too many positional argument')
332 if not self._unix_socket_arg(host, port, args, kwargs):
333 self._deprecated_arg(args, kwargs, 0, 'certfile')
334 self._deprecated_arg(args, kwargs, 1, 'unix_socket')
335 self._deprecated_arg(args, kwargs, 2, 'ciphers')
Bryan Duxbury69720412012-01-03 17:32:30 +0000336
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900337 if 'ssl_context' not in kwargs:
338 # Preserve existing behaviors for default values
339 if 'cert_reqs' not in kwargs:
340 kwargs['cert_reqs'] = ssl.CERT_NONE
341 if'certfile' not in kwargs:
342 kwargs['certfile'] = 'cert.pem'
Bryan Duxbury69720412012-01-03 17:32:30 +0000343
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900344 unix_socket = kwargs.pop('unix_socket', None)
345 TSSLBase.__init__(self, True, None, kwargs)
346 TSocket.TServerSocket.__init__(self, host, port, unix_socket)
Bryan Duxbury50409112011-03-21 17:59:49 +0000347
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900348 def setCertfile(self, certfile):
349 """Set or change the server certificate file used to wrap new connections.
350
351 @param certfile: The filename of the server certificate,
352 i.e. '/etc/certs/server.pem'
353 @type certfile: str
354
355 Raises an IOError exception if the certfile is not present or unreadable.
356 """
357 warnings.warn('Use certfile property instead.', DeprecationWarning)
358 self.certfile = certfile
359
360 def accept(self):
361 plain_client, addr = self.handle.accept()
362 try:
363 client = self._wrap_socket(plain_client)
364 except ssl.SSLError:
365 logger.error('Error while accepting from %s', addr, exc_info=True)
366 # failed handshake/ssl wrap, close socket to client
367 plain_client.close()
368 # raise
369 # We can't raise the exception, because it kills most TServer derived
370 # serve() methods.
371 # Instead, return None, and let the TServer instance deal with it in
372 # other exception handling. (but TSimpleServer dies anyway)
373 return None
374 result = TSocket.TSocket()
375 result.setHandle(client)
376 return result