blob: 5eff5e619f872007b487b07ba784aaa3f21469b5 [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#
19import os
20import socket
21import ssl
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000022
23from thrift.transport import TSocket
Bryan Duxbury50409112011-03-21 17:59:49 +000024from thrift.transport.TTransport import TTransportException
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000025
26class TSSLSocket(TSocket.TSocket):
Bryan Duxbury50409112011-03-21 17:59:49 +000027 """
28 SSL implementation of client-side TSocket
29
30 This class creates outbound sockets wrapped using the
31 python standard ssl module for encrypted connections.
32
33 The protocol used is set using the class variable
34 SSL_VERSION, which must be one of ssl.PROTOCOL_* and
35 defaults to ssl.PROTOCOL_TLSv1 for greatest security.
36 """
37 SSL_VERSION = ssl.PROTOCOL_TLSv1
38
39 def __init__(self, validate=True, ca_certs=None, *args, **kwargs):
40 """
41 @param validate: Set to False to disable SSL certificate validation entirely.
42 @type validate: bool
43 @param ca_certs: Filename to the Certificate Authority pem file, possibly a
44 file downloaded from: http://curl.haxx.se/ca/cacert.pem This is passed to
45 the ssl_wrap function as the 'ca_certs' parameter.
46 @type ca_certs: str
47
48 Raises an IOError exception if validate is True and the ca_certs file is
49 None, not present or unreadable.
50 """
51 self.validate = validate
52 self.is_valid = False
53 self.peercert = None
54 if not validate:
55 self.cert_reqs = ssl.CERT_NONE
56 else:
57 self.cert_reqs = ssl.CERT_REQUIRED
58 self.ca_certs = ca_certs
59 if validate and ca_certs is not None:
60 if not os.access(ca_certs, os.R_OK):
61 raise IOError('Certificate Authority ca_certs file "%s" is not readable, cannot validate SSL certificates.' % (ca_certs))
62 TSocket.TSocket.__init__(self, *args, **kwargs)
63
64 def open(self):
65 try:
66 res0 = self._resolveAddr()
67 for res in res0:
68 sock_family, sock_type= res[0:2]
69 ip_port = res[4]
70 plain_sock = socket.socket(sock_family, sock_type)
71 self.handle = ssl.wrap_socket(plain_sock, ssl_version=self.SSL_VERSION,
72 do_handshake_on_connect=True, ca_certs=self.ca_certs, cert_reqs=self.cert_reqs)
73 self.handle.settimeout(self._timeout)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000074 try:
Bryan Duxbury50409112011-03-21 17:59:49 +000075 self.handle.connect(ip_port)
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000076 except socket.error, e:
Bryan Duxbury50409112011-03-21 17:59:49 +000077 if res is not res0[-1]:
78 continue
Bryan Duxbury2b969ad2011-02-22 18:20:53 +000079 else:
Bryan Duxbury50409112011-03-21 17:59:49 +000080 raise e
81 break
82 except socket.error, e:
83 if self._unix_socket:
84 message = 'Could not connect to secure socket %s' % self._unix_socket
85 else:
86 message = 'Could not connect to %s:%d' % (self.host, self.port)
87 raise TTransportException(type=TTransportException.NOT_OPEN, message=message)
88 if self.validate:
89 self._validate_cert()
90
91 def _validate_cert(self):
92 """internal method to validate the peer's SSL certificate, and to check the
93 commonName of the certificate to ensure it matches the hostname we
94 used to make this connection. Does not support subjectAltName records
95 in certificates.
96
97 raises TTransportException if the certificate fails validation."""
98 cert = self.handle.getpeercert()
99 self.peercert = cert
100 if 'subject' not in cert:
101 raise TTransportException(type=TTransportException.NOT_OPEN,
102 message='No SSL certificate found from %s:%s' % (self.host, self.port))
103 fields = cert['subject']
104 for field in fields:
105 # ensure structure we get back is what we expect
106 if not isinstance(field, tuple):
107 continue
108 cert_pair = field[0]
109 if len(cert_pair) < 2:
110 continue
111 cert_key, cert_value = cert_pair[0:2]
112 if cert_key != 'commonName':
113 continue
114 certhost = cert_value
115 if certhost == self.host:
116 # success, cert commonName matches desired hostname
117 self.is_valid = True
118 return
119 else:
120 raise TTransportException(type=TTransportException.UNKNOWN,
121 message='Host name we connected to "%s" doesn\'t match certificate provided commonName "%s"' % (self.host, certhost))
122 raise TTransportException(type=TTransportException.UNKNOWN,
123 message='Could not validate SSL certificate from host "%s". Cert=%s' % (self.host, cert))
Bryan Duxbury2b969ad2011-02-22 18:20:53 +0000124
125class TSSLServerSocket(TSocket.TServerSocket):
Bryan Duxbury50409112011-03-21 17:59:49 +0000126 """
127 SSL implementation of TServerSocket
128
129 This uses the ssl module's wrap_socket() method to provide SSL
130 negotiated encryption.
131 """
132 SSL_VERSION = ssl.PROTOCOL_TLSv1
133
134 def __init__(self, certfile='cert.pem', *args, **kwargs):
135 """Initialize a TSSLServerSocket
136
137 @param certfile: The filename of the server certificate file, defaults to cert.pem
138 @type certfile: str
139 @param host: The hostname or IP to bind the listen socket to, i.e. 'localhost' for only allowing
140 local network connections. Pass None to bind to all interfaces.
141 @type host: str
142 @param port: The port to listen on for inbound connections.
143 @type port: int
144 """
145 self.setCertfile(certfile)
146 TSocket.TServerSocket.__init__(self, *args, **kwargs)
147
148 def setCertfile(self, certfile):
149 """Set or change the server certificate file used to wrap new connections.
150
151 @param certfile: The filename of the server certificate, i.e. '/etc/certs/server.pem'
152 @type certfile: str
153
154 Raises an IOError exception if the certfile is not present or unreadable.
155 """
156 if not os.access(certfile, os.R_OK):
157 raise IOError('No such certfile found: %s' % (certfile))
158 self.certfile = certfile
159
160 def accept(self):
161 plain_client, addr = self.handle.accept()
162 result = TSocket.TSocket()
163 client = ssl.wrap_socket(plain_client, certfile=self.certfile,
164 server_side=True, ssl_version=self.SSL_VERSION)
165 result.setHandle(client)
166 return result