blob: 9be0912f9d419f6452f8f36490ceda13687e44ec [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):
35 # SSLContext is not available for Python < 2.7.9
36 _has_ssl_context = sys.hexversion >= 0x020709F0
37
38 # ciphers argument is not available for Python < 2.7.0
39 _has_ciphers = sys.hexversion >= 0x020700F0
40
41 # 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
45
46 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
55
56 @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
62
63 @property
64 def ssl_context(self):
65 return self._context
66
67 SSL_VERSION = _default_protocol
Bryan Duxbury50409112011-03-21 17:59:49 +000068 """
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
74 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]
84
85 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
91
92 def __getattr__(self, key):
93 if key == 'SSL_VERSION':
94 warnings.warn('Use ssl_version attribute instead.', DeprecationWarning)
95 return self.ssl_version
96
97 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')
111 else:
112 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)
171
172
173class TSSLSocket(TSocket.TSocket, TSSLBase):
174 """
175 SSL implementation of TSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000176
177 This class creates outbound sockets wrapped using the
178 python standard ssl module for encrypted connections.
Bryan Duxbury50409112011-03-21 17:59:49 +0000179 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000180
Nobuaki Sukegawaad835862015-12-23 23:32:09 +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``
Bryan Duxbury69720412012-01-03 17:32:30 +0000187
Nobuaki Sukegawaad835862015-12-23 23:32:09 +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.
192
193 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
Bryan Duxbury50409112011-03-21 17:59:49 +0000196 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000197 self.is_valid = False
198 self.peercert = None
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900199
200 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')
210
211 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)
Bryan Duxbury16066592011-03-22 18:06:04 +0000223 TSocket.TSocket.__init__(self, host, port, unix_socket)
Bryan Duxbury50409112011-03-21 17:59:49 +0000224
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900225 @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
Bryan Duxbury50409112011-03-21 17:59:49 +0000235 def open(self):
236 try:
237 res0 = self._resolveAddr()
238 for res in res0:
Bryan Duxbury69720412012-01-03 17:32:30 +0000239 sock_family, sock_type = res[0:2]
Bryan Duxbury50409112011-03-21 17:59:49 +0000240 ip_port = res[4]
241 plain_sock = socket.socket(sock_family, sock_type)
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900242 self.handle = self._wrap_socket(plain_sock)
Bryan Duxbury50409112011-03-21 17:59:49 +0000243 self.handle.settimeout(self._timeout)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000244 try:
Bryan Duxbury50409112011-03-21 17:59:49 +0000245 self.handle.connect(ip_port)
jfarrelld565e2f2015-03-18 21:02:47 -0400246 except socket.error as e:
Bryan Duxbury50409112011-03-21 17:59:49 +0000247 if res is not res0[-1]:
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900248 logger.warning('Error while connecting with %s. Trying next one.', ip_port, exc_info=True)
Bryan Duxbury50409112011-03-21 17:59:49 +0000249 continue
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000250 else:
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900251 raise
Bryan Duxbury50409112011-03-21 17:59:49 +0000252 break
jfarrelld565e2f2015-03-18 21:02:47 -0400253 except socket.error as e:
Bryan Duxbury50409112011-03-21 17:59:49 +0000254 if self._unix_socket:
Roger Meier52820d02012-11-08 23:11:14 +0000255 message = 'Could not connect to secure socket %s: %s' \
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900256 % (self._unix_socket, e)
Bryan Duxbury50409112011-03-21 17:59:49 +0000257 else:
Roger Meier52820d02012-11-08 23:11:14 +0000258 message = 'Could not connect to %s:%d: %s' % (self.host, self.port, e)
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900259 logger.error('Error while connecting with %s.', ip_port, exc_info=True)
Bryan Duxbury69720412012-01-03 17:32:30 +0000260 raise TTransportException(type=TTransportException.NOT_OPEN,
261 message=message)
Bryan Duxbury50409112011-03-21 17:59:49 +0000262 if self.validate:
263 self._validate_cert()
264
265 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
271 raises TTransportException if the certificate fails validation.
272 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000273 cert = self.handle.getpeercert()
274 self.peercert = cert
275 if 'subject' not in cert:
Bryan Duxbury69720412012-01-03 17:32:30 +0000276 raise TTransportException(
277 type=TTransportException.NOT_OPEN,
278 message='No SSL certificate found from %s:%s' % (self.host, self.port))
Bryan Duxbury50409112011-03-21 17:59:49 +0000279 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
Jake Farrell877125c2013-06-07 23:47:22 -0400291 # this check should be performed by some sort of Access Manager
Bryan Duxbury50409112011-03-21 17:59:49 +0000292 if certhost == self.host:
293 # success, cert commonName matches desired hostname
294 self.is_valid = True
Bryan Duxbury69720412012-01-03 17:32:30 +0000295 return
Bryan Duxbury50409112011-03-21 17:59:49 +0000296 else:
Bryan Duxbury69720412012-01-03 17:32:30 +0000297 raise TTransportException(
298 type=TTransportException.UNKNOWN,
299 message='Hostname we connected to "%s" doesn\'t match certificate '
300 'provided commonName "%s"' % (self.host, certhost))
301 raise TTransportException(
302 type=TTransportException.UNKNOWN,
303 message='Could not validate SSL certificate from '
304 'host "%s". Cert=%s' % (self.host, cert))
305
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000306
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900307class TSSLServerSocket(TSocket.TServerSocket, TSSLBase):
Bryan Duxbury69720412012-01-03 17:32:30 +0000308 """SSL implementation of TServerSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000309
310 This uses the ssl module's wrap_socket() method to provide SSL
311 negotiated encryption.
312 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000313
Nobuaki Sukegawaad835862015-12-23 23:32:09 +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``
Bryan Duxbury69720412012-01-03 17:32:30 +0000320
Nobuaki Sukegawaad835862015-12-23 23:32:09 +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.
Roger Meierfc1303e2014-12-02 00:11:54 +0100324
Nobuaki Sukegawaad835862015-12-23 23:32:09 +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
Bryan Duxbury50409112011-03-21 17:59:49 +0000328 """
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900329 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')
336
337 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'
343
344 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
348 def setCertfile(self, certfile):
349 """Set or change the server certificate file used to wrap new connections.
Bryan Duxbury69720412012-01-03 17:32:30 +0000350
351 @param certfile: The filename of the server certificate,
352 i.e. '/etc/certs/server.pem'
Bryan Duxbury50409112011-03-21 17:59:49 +0000353 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000354
Bryan Duxbury50409112011-03-21 17:59:49 +0000355 Raises an IOError exception if the certfile is not present or unreadable.
356 """
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900357 warnings.warn('Use certfile property instead.', DeprecationWarning)
Bryan Duxbury50409112011-03-21 17:59:49 +0000358 self.certfile = certfile
359
360 def accept(self):
361 plain_client, addr = self.handle.accept()
Bryan Duxbury16066592011-03-22 18:06:04 +0000362 try:
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900363 client = self._wrap_socket(plain_client)
364 except ssl.SSLError:
365 logger.error('Error while accepting from %s', addr, exc_info=True)
Bryan Duxbury16066592011-03-22 18:06:04 +0000366 # failed handshake/ssl wrap, close socket to client
367 plain_client.close()
Nobuaki Sukegawaad835862015-12-23 23:32:09 +0900368 # raise
Bryan Duxbury69720412012-01-03 17:32:30 +0000369 # We can't raise the exception, because it kills most TServer derived
370 # serve() methods.
Bryan Duxbury16066592011-03-22 18:06:04 +0000371 # Instead, return None, and let the TServer instance deal with it in
372 # other exception handling. (but TSimpleServer dies anyway)
Bryan Duxbury69720412012-01-03 17:32:30 +0000373 return None
Bryan Duxbury50409112011-03-21 17:59:49 +0000374 result = TSocket.TSocket()
Bryan Duxbury50409112011-03-21 17:59:49 +0000375 result.setHandle(client)
376 return result