blob: 81e0984261a15dbd1abeff1f208ecdbe26563536 [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,
Jake Farrell877125c2013-06-07 23:47:22 -040046 keyfile=None,
47 certfile=None,
Bryan Duxbury69720412012-01-03 17:32:30 +000048 unix_socket=None):
49 """Create SSL TSocket
50
51 @param validate: Set to False to disable SSL certificate validation
Bryan Duxbury50409112011-03-21 17:59:49 +000052 @type validate: bool
53 @param ca_certs: Filename to the Certificate Authority pem file, possibly a
54 file downloaded from: http://curl.haxx.se/ca/cacert.pem This is passed to
55 the ssl_wrap function as the 'ca_certs' parameter.
56 @type ca_certs: str
Jake Farrell877125c2013-06-07 23:47:22 -040057 @param keyfile: The private key
58 @type keyfile: str
59 @param certfile: The cert file
60 @type certfile: str
61
Bryan Duxbury50409112011-03-21 17:59:49 +000062 Raises an IOError exception if validate is True and the ca_certs file is
63 None, not present or unreadable.
64 """
65 self.validate = validate
66 self.is_valid = False
67 self.peercert = None
68 if not validate:
69 self.cert_reqs = ssl.CERT_NONE
70 else:
71 self.cert_reqs = ssl.CERT_REQUIRED
72 self.ca_certs = ca_certs
Jake Farrell877125c2013-06-07 23:47:22 -040073 self.keyfile = keyfile
74 self.certfile = certfile
Bryan Duxbury16066592011-03-22 18:06:04 +000075 if validate:
76 if ca_certs is None or not os.access(ca_certs, os.R_OK):
Bryan Duxbury69720412012-01-03 17:32:30 +000077 raise IOError('Certificate Authority ca_certs file "%s" '
78 'is not readable, cannot validate SSL '
79 'certificates.' % (ca_certs))
Bryan Duxbury16066592011-03-22 18:06:04 +000080 TSocket.TSocket.__init__(self, host, port, unix_socket)
Bryan Duxbury50409112011-03-21 17:59:49 +000081
82 def open(self):
83 try:
84 res0 = self._resolveAddr()
85 for res in res0:
Bryan Duxbury69720412012-01-03 17:32:30 +000086 sock_family, sock_type = res[0:2]
Bryan Duxbury50409112011-03-21 17:59:49 +000087 ip_port = res[4]
88 plain_sock = socket.socket(sock_family, sock_type)
Bryan Duxbury69720412012-01-03 17:32:30 +000089 self.handle = ssl.wrap_socket(plain_sock,
90 ssl_version=self.SSL_VERSION,
91 do_handshake_on_connect=True,
92 ca_certs=self.ca_certs,
Jake Farrell877125c2013-06-07 23:47:22 -040093 keyfile=self.keyfile,
94 certfile=self.certfile,
Bryan Duxbury69720412012-01-03 17:32:30 +000095 cert_reqs=self.cert_reqs)
Bryan Duxbury50409112011-03-21 17:59:49 +000096 self.handle.settimeout(self._timeout)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000097 try:
Bryan Duxbury50409112011-03-21 17:59:49 +000098 self.handle.connect(ip_port)
Todd Lipcon2b2560e2012-12-10 14:29:59 -080099 except socket.error, e:
Bryan Duxbury50409112011-03-21 17:59:49 +0000100 if res is not res0[-1]:
101 continue
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000102 else:
Bryan Duxbury50409112011-03-21 17:59:49 +0000103 raise e
104 break
Todd Lipcon2b2560e2012-12-10 14:29:59 -0800105 except socket.error, e:
Bryan Duxbury50409112011-03-21 17:59:49 +0000106 if self._unix_socket:
Roger Meier52820d02012-11-08 23:11:14 +0000107 message = 'Could not connect to secure socket %s: %s' \
108 % (self._unix_socket, e)
Bryan Duxbury50409112011-03-21 17:59:49 +0000109 else:
Roger Meier52820d02012-11-08 23:11:14 +0000110 message = 'Could not connect to %s:%d: %s' % (self.host, self.port, e)
Bryan Duxbury69720412012-01-03 17:32:30 +0000111 raise TTransportException(type=TTransportException.NOT_OPEN,
112 message=message)
Bryan Duxbury50409112011-03-21 17:59:49 +0000113 if self.validate:
114 self._validate_cert()
115
116 def _validate_cert(self):
117 """internal method to validate the peer's SSL certificate, and to check the
118 commonName of the certificate to ensure it matches the hostname we
119 used to make this connection. Does not support subjectAltName records
120 in certificates.
Bryan Duxbury69720412012-01-03 17:32:30 +0000121
122 raises TTransportException if the certificate fails validation.
123 """
Bryan Duxbury50409112011-03-21 17:59:49 +0000124 cert = self.handle.getpeercert()
125 self.peercert = cert
126 if 'subject' not in cert:
Bryan Duxbury69720412012-01-03 17:32:30 +0000127 raise TTransportException(
128 type=TTransportException.NOT_OPEN,
129 message='No SSL certificate found from %s:%s' % (self.host, self.port))
Bryan Duxbury50409112011-03-21 17:59:49 +0000130 fields = cert['subject']
131 for field in fields:
132 # ensure structure we get back is what we expect
133 if not isinstance(field, tuple):
134 continue
135 cert_pair = field[0]
136 if len(cert_pair) < 2:
137 continue
138 cert_key, cert_value = cert_pair[0:2]
139 if cert_key != 'commonName':
140 continue
141 certhost = cert_value
Jake Farrell877125c2013-06-07 23:47:22 -0400142 # this check should be performed by some sort of Access Manager
Bryan Duxbury50409112011-03-21 17:59:49 +0000143 if certhost == self.host:
144 # success, cert commonName matches desired hostname
145 self.is_valid = True
Bryan Duxbury69720412012-01-03 17:32:30 +0000146 return
Bryan Duxbury50409112011-03-21 17:59:49 +0000147 else:
Bryan Duxbury69720412012-01-03 17:32:30 +0000148 raise TTransportException(
149 type=TTransportException.UNKNOWN,
150 message='Hostname we connected to "%s" doesn\'t match certificate '
151 'provided commonName "%s"' % (self.host, certhost))
152 raise TTransportException(
153 type=TTransportException.UNKNOWN,
154 message='Could not validate SSL certificate from '
155 'host "%s". Cert=%s' % (self.host, cert))
156
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000157
158class TSSLServerSocket(TSocket.TServerSocket):
Bryan Duxbury69720412012-01-03 17:32:30 +0000159 """SSL implementation of TServerSocket
Bryan Duxbury50409112011-03-21 17:59:49 +0000160
161 This uses the ssl module's wrap_socket() method to provide SSL
162 negotiated encryption.
163 """
164 SSL_VERSION = ssl.PROTOCOL_TLSv1
165
Bryan Duxbury69720412012-01-03 17:32:30 +0000166 def __init__(self,
167 host=None,
168 port=9090,
169 certfile='cert.pem',
170 unix_socket=None):
Bryan Duxbury50409112011-03-21 17:59:49 +0000171 """Initialize a TSSLServerSocket
Bryan Duxbury69720412012-01-03 17:32:30 +0000172
173 @param certfile: filename of the server certificate, defaults to cert.pem
Bryan Duxbury50409112011-03-21 17:59:49 +0000174 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000175 @param host: The hostname or IP to bind the listen socket to,
176 i.e. 'localhost' for only allowing local network connections.
177 Pass None to bind to all interfaces.
Bryan Duxbury50409112011-03-21 17:59:49 +0000178 @type host: str
179 @param port: The port to listen on for inbound connections.
180 @type port: int
181 """
182 self.setCertfile(certfile)
Bryan Duxbury16066592011-03-22 18:06:04 +0000183 TSocket.TServerSocket.__init__(self, host, port)
Bryan Duxbury50409112011-03-21 17:59:49 +0000184
185 def setCertfile(self, certfile):
186 """Set or change the server certificate file used to wrap new connections.
Bryan Duxbury69720412012-01-03 17:32:30 +0000187
188 @param certfile: The filename of the server certificate,
189 i.e. '/etc/certs/server.pem'
Bryan Duxbury50409112011-03-21 17:59:49 +0000190 @type certfile: str
Bryan Duxbury69720412012-01-03 17:32:30 +0000191
Bryan Duxbury50409112011-03-21 17:59:49 +0000192 Raises an IOError exception if the certfile is not present or unreadable.
193 """
194 if not os.access(certfile, os.R_OK):
195 raise IOError('No such certfile found: %s' % (certfile))
196 self.certfile = certfile
197
198 def accept(self):
199 plain_client, addr = self.handle.accept()
Bryan Duxbury16066592011-03-22 18:06:04 +0000200 try:
201 client = ssl.wrap_socket(plain_client, certfile=self.certfile,
202 server_side=True, ssl_version=self.SSL_VERSION)
Todd Lipcon2b2560e2012-12-10 14:29:59 -0800203 except ssl.SSLError, ssl_exc:
Bryan Duxbury16066592011-03-22 18:06:04 +0000204 # failed handshake/ssl wrap, close socket to client
205 plain_client.close()
206 # raise ssl_exc
Bryan Duxbury69720412012-01-03 17:32:30 +0000207 # We can't raise the exception, because it kills most TServer derived
208 # serve() methods.
Bryan Duxbury16066592011-03-22 18:06:04 +0000209 # Instead, return None, and let the TServer instance deal with it in
210 # other exception handling. (but TSimpleServer dies anyway)
Bryan Duxbury69720412012-01-03 17:32:30 +0000211 return None
Bryan Duxbury50409112011-03-21 17:59:49 +0000212 result = TSocket.TSocket()
Bryan Duxbury50409112011-03-21 17:59:49 +0000213 result.setHandle(client)
214 return result