web.web.wsgiserver.ssl_pyopenssl

Package web :: Package web :: Package wsgiserver :: Module ssl_pyopenssl |
[hide private]
[frames] | no frames]

# Source Code for Module web.web.wsgiserver.ssl_pyopenssl

      1  """A library for integrating pyOpenSSL with CherryPy.
      2
      3  The OpenSSL module must be importable for SSL functionality.
      4  You can obtain it from http://pyopenssl.sourceforge.net/
      5
      6  To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
      7  SSLAdapter. There are two ways to use SSL:
      8
      9  Method One:
     10      ssl_adapter.context: an instance of SSL.Context.
     11
     12      If this is not None, it is assumed to be an SSL.Context instance,
     13      and will be passed to SSL.Connection on bind(). The developer is
     14      responsible for forming a valid Context object. This approach is
     15      to be preferred for more flexibility, e.g. if the cert and key are
     16      streams instead of files, or need decryption, or SSL.SSLv3_METHOD
     17      is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
     18      the pyOpenSSL documentation for complete options.
     19
     20  Method Two (shortcut):
     21      ssl_adapter.certificate: the filename of the server SSL certificate.
     22      ssl_adapter.private_key: the filename of the server's private key file.
     23
     24      Both are None by default. If ssl_adapter.context is None, but .private_key
     25      and .certificate are both given and valid, they will be read, and the
     26      context will be automatically created from them.
     27
     28      ssl_adapter.certificate_chain: (optional) the filename of CA's intermediate
     29          certificate bundle. This is needed for cheaper "chained root" SSL
     30          certificates, and should be left as None if not required.
     31  """
     32
     33  import socket
     34  import threading
     35  import time
     36
     37  from web import wsgiserver
     38
     39  try:
     40      from OpenSSL import SSL
     41      from OpenSSL import crypto
     42  except ImportError:
     43      SSL = None
     44
     45
     46 -class SSL_fileobject(wsgiserver.CP_fileobject):
     47      """SSL file object attached to a socket object."""
     48
     49      ssl_timeout = 3
     50      ssl_retry = .01
     51
     52 -    def _safe_call(self, is_reader, call, *args, **kwargs):
     53          """Wrap the given call with SSL error-trapping.
     54
     55          is_reader: if False EOF errors will be raised. If True, EOF errors
     56              will return "" (to emulate normal sockets).
     57          """
     58          start = time.time()
     59          while True:
     60              try:
     61                  return call(*args, **kwargs)
     62              except SSL.WantReadError:
     63                  # Sleep and try again. This is dangerous, because it means
     64                  # the rest of the stack has no way of differentiating
     65                  # between a "new handshake" error and "client dropped".
     66                  # Note this isn't an endless loop: there's a timeout below.
     67                  time.sleep(self.ssl_retry)
     68              except SSL.WantWriteError:
     69                  time.sleep(self.ssl_retry)
     70              except SSL.SysCallError, e:
     71                  if is_reader and e.args == (-1, 'Unexpected EOF'):
     72                      return ""
     73
     74                  errnum = e.args[0]
     75                  if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
     76                      return ""
     77                  raise socket.error(errnum)
     78              except SSL.Error, e:
     79                  if is_reader and e.args == (-1, 'Unexpected EOF'):
     80                      return ""
     81
     82                  thirdarg = None
     83                  try:
     84                      thirdarg = e.args[0][0][2]
     85                  except IndexError:
     86                      pass
     87
     88                  if thirdarg == 'http request':
     89                      # The client is talking HTTP to an HTTPS server.
     90                      raise wsgiserver.NoSSLError()
     91
     92                  raise wsgiserver.FatalSSLAlert(*e.args)
     93              except:
     94                  raise
     95
     96              if time.time() - start > self.ssl_timeout:
     97                  raise socket.timeout("timed out")
     98
     99 -    def recv(self, *args, **kwargs):
    100          buf = []
    101          r = super(SSL_fileobject, self).recv
    102          while True:
    103              data = self._safe_call(True, r, *args, **kwargs)
    104              buf.append(data)
    105              p = self._sock.pending()
    106              if not p:
    107                  return "".join(buf)
    108
    109 -    def sendall(self, *args, **kwargs):
    110          return self._safe_call(False, super(SSL_fileobject, self).sendall,
    111                                 *args, **kwargs)
    112
    113 -    def send(self, *args, **kwargs):
    114          return self._safe_call(False, super(SSL_fileobject, self).send,
    115                                 *args, **kwargs)
    116
    117
    118 -class SSLConnection:
    119      """A thread-safe wrapper for an SSL.Connection.
    120
    121      *args: the arguments to create the wrapped SSL.Connection(*args).
    122      """
    123
    124 -    def __init__(self, *args):
    125          self._ssl_conn = SSL.Connection(*args)
    126          self._lock = threading.RLock()
    127
    128      for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
    129                'renegotiate', 'bind', 'listen', 'connect', 'accept',
    130                'setblocking', 'fileno', 'close', 'get_cipher_list',
    131                'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
    132                'makefile', 'get_app_data', 'set_app_data', 'state_string',
    133                'sock_shutdown', 'get_peer_certificate', 'want_read',
    134                'want_write', 'set_connect_state', 'set_accept_state',
    135                'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
    136          exec("""def %s(self, *args):
    137          self._lock.acquire()
    138          try:
    139              return self._ssl_conn.%s(*args)
    140          finally:
    141              self._lock.release()
    142  """ % (f, f))
    143
    144 -    def shutdown(self, *args):
    145          self._lock.acquire()
    146          try:
    147              # pyOpenSSL.socket.shutdown takes no args
    148              return self._ssl_conn.shutdown()
    149          finally:
    150              self._lock.release()
    151
    152
    153 -class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
    154      """A wrapper for integrating pyOpenSSL with CherryPy."""
    155
    156 -    def __init__(self, certificate, private_key, certificate_chain=None, client_CA=None, ):
    157          if SSL is None:
    158              raise ImportError("You must install pyOpenSSL to use HTTPS.")
    159
    160          self.context = None
    161          self.certificate = certificate
    162          self.private_key = private_key
    163          self.certificate_chain = certificate_chain
    164          self.client_CA = client_CA
    165          self._environ = None
    166
    167 -    def bind(self, sock):
    168          """Wrap and return the given socket."""
    169          if self.context is None:
    170              self.context = self.get_context()
    171          conn = SSLConnection(self.context, sock)
    172          self._environ = self.get_environ()
    173          return conn
    174
    175 -    def wrap(self, sock):
    176          """Wrap and return the given socket, plus WSGI environ entries."""
    177          return sock, self._environ.copy()
    178
    179 -    def get_context(self):
    180          """Return an SSL.Context from self attributes."""
    181          # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
    182          c = SSL.Context(SSL.SSLv23_METHOD)
    183          c.use_privatekey_file(self.private_key)
    184          if self.certificate_chain:
    185              c.load_verify_locations(self.certificate_chain)
    186          c.use_certificate_file(self.certificate)
    187
    188          if self.client_CA:
    189              c.load_client_ca(self.client_CA)
    190
    191              c.set_verify_depth(2)
    192              c.load_verify_locations( self.client_CA )
    193
    194              def callback( conn, cert, errno, depth, retcode ):
    195                  return retcode
    196
    197              c.set_verify( SSL.VERIFY_FAIL_IF_NO_PEER_CERT | SSL.VERIFY_PEER, callback)
    198          return c
    199
    200
    201 -    def get_environ(self):
    202          """Return WSGI environ entries to be merged into each request."""
    203          ssl_environ = {
    204              "HTTPS": "on",
    205              # pyOpenSSL doesn't provide access to any of these AFAICT
    206  ##            'SSL_PROTOCOL': 'SSLv2',
    207  ##            SSL_CIPHER        string  The cipher specification name
    208  ##            SSL_VERSION_INTERFACE     string  The mod_ssl program version
    209  ##            SSL_VERSION_LIBRARY       string  The OpenSSL program version
    210              }
    211
    212          if self.certificate:
    213              # Server certificate attributes
    214              cert = open(self.certificate, 'rb').read()
    215              cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
    216              ssl_environ.update({
    217                  'SSL_SERVER_M_VERSION': cert.get_version(),
    218                  'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
    219  ##                'SSL_SERVER_V_START': Validity of server's certificate (start time),
    220  ##                'SSL_SERVER_V_END': Validity of server's certificate (end time),
    221                  })
    222
    223              for prefix, dn in [("I", cert.get_issuer()),
    224                                 ("S", cert.get_subject())]:
    225                  # X509Name objects don't seem to have a way to get the
    226                  # complete DN string. Use str() and slice it instead,
    227                  # because str(dn) == "<X509Name object '/C=US/ST=...'>"
    228                  dnstr = str(dn)[18:-2]
    229
    230                  wsgikey = 'SSL_SERVER_%s_DN' % prefix
    231                  ssl_environ[wsgikey] = dnstr
    232
    233                  # The DN should be of the form: /k1=v1/k2=v2, but we must allow
    234                  # for any value to contain slashes itself (in a URL).
    235                  while dnstr:
    236                      pos = dnstr.rfind("=")
    237                      dnstr, value = dnstr[:pos], dnstr[pos + 1:]
    238                      pos = dnstr.rfind("/")
    239                      dnstr, key = dnstr[:pos], dnstr[pos + 1:]
    240                      if key and value:
    241                          wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
    242                          ssl_environ[wsgikey] = value
    243
    244          return ssl_environ
    245
    246 -    def makefile(self, sock, mode='r', bufsize=-1):
    247          if SSL and isinstance(sock, SSL.ConnectionType):
    248              timeout = sock.gettimeout()
    249              f = SSL_fileobject(sock, mode, bufsize)
    250              f.ssl_timeout = timeout
    251              return f
    252          else:
    253              return wsgiserver.CP_fileobject(sock, mode, bufsize)
    254