diff mbox

[ovs-dev,v2] python: Add SSL support to the python ovs client library

Message ID e520370b-45b8-db2c-e3e9-9655665189ca@redhat.com
State Accepted
Headers show

Commit Message

Numan Siddique Oct. 5, 2016, 12:20 p.m. UTC
SSL support is added to the ovs/stream.py. pyOpenSSL library is used
to support SSL. If this library is not present, then the SSL stream
is not registered with the Stream class.

Signed-off-by: Numan Siddique <nusiddiq@redhat.com>
---
 python/ovs/poller.py |  8 +++++
 python/ovs/stream.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 tests/ovsdb-idl.at   | 30 +++++++++++++++--
 tests/test-ovsdb.py  |  7 ++++
 4 files changed, 131 insertions(+), 5 deletions(-)

v1 -> v2
--------
 * Fixed the TypeError exception seen with pyopenssl version 0.14

Comments

Ben Pfaff Oct. 5, 2016, 5:01 p.m. UTC | #1
On Wed, Oct 05, 2016 at 05:50:24PM +0530, Numan Siddique wrote:
> SSL support is added to the ovs/stream.py. pyOpenSSL library is used
> to support SSL. If this library is not present, then the SSL stream
> is not registered with the Stream class.
> 
> Signed-off-by: Numan Siddique <nusiddiq@redhat.com>

Applied, thanks!
diff mbox

Patch

diff --git a/python/ovs/poller.py b/python/ovs/poller.py
index de6bf22..d7cb7d3 100644
--- a/python/ovs/poller.py
+++ b/python/ovs/poller.py
@@ -20,6 +20,11 @@  import socket
 import os
 
 try:
+    from OpenSSL import SSL
+except ImportError:
+    SSL = None
+
+try:
     import eventlet.patcher
 
     def _using_eventlet_green_select():
@@ -54,6 +59,9 @@  class _SelectSelect(object):
     def register(self, fd, events):
         if isinstance(fd, socket.socket):
             fd = fd.fileno()
+        if SSL and isinstance(fd, SSL.Connection):
+            fd = fd.fileno()
+
         assert isinstance(fd, int)
         if events & POLLIN:
             self.rlist.append(fd)
diff --git a/python/ovs/stream.py b/python/ovs/stream.py
index 97b22ac..59578b1 100644
--- a/python/ovs/stream.py
+++ b/python/ovs/stream.py
@@ -22,6 +22,11 @@  import ovs.poller
 import ovs.socket_util
 import ovs.vlog
 
+try:
+    from OpenSSL import SSL
+except ImportError:
+    SSL = None
+
 vlog = ovs.vlog.Vlog("stream")
 
 
@@ -39,7 +44,7 @@  def stream_or_pstream_needs_probes(name):
 
 
 class Stream(object):
-    """Bidirectional byte stream.  Currently only Unix domain sockets
+    """Bidirectional byte stream.  Unix domain sockets, tcp and ssl
     are implemented."""
 
     # States.
@@ -54,6 +59,10 @@  class Stream(object):
 
     _SOCKET_METHODS = {}
 
+    _SSL_private_key_file = None
+    _SSL_certificate_file = None
+    _SSL_ca_cert_file = None
+
     @staticmethod
     def register_method(method, cls):
         Stream._SOCKET_METHODS[method + ":"] = cls
@@ -68,7 +77,7 @@  class Stream(object):
     @staticmethod
     def is_valid_name(name):
         """Returns True if 'name' is a stream name in the form "TYPE:ARGS" and
-        TYPE is a supported stream type (currently only "unix:" and "tcp:"),
+        TYPE is a supported stream type ("unix:", "tcp:" and "ssl:"),
         otherwise False."""
         return bool(Stream._find_method(name))
 
@@ -116,7 +125,7 @@  class Stream(object):
             return error, None
         else:
             status = ovs.socket_util.check_connection_completion(sock)
-            return 0, Stream(sock, name, status)
+            return 0, cls(sock, name, status)
 
     @staticmethod
     def _open(suffix, dscp):
@@ -264,6 +273,18 @@  class Stream(object):
         # Don't delete the file: we might have forked.
         self.socket.close()
 
+    @staticmethod
+    def ssl_set_private_key_file(file_name):
+        Stream._SSL_private_key_file = file_name
+
+    @staticmethod
+    def ssl_set_certificate_file(file_name):
+        Stream._SSL_certificate_file = file_name
+
+    @staticmethod
+    def ssl_set_ca_cert_file(file_name):
+        Stream._SSL_ca_cert_file = file_name
+
 
 class PassiveStream(object):
     @staticmethod
@@ -362,6 +383,7 @@  def usage(name):
 Active %s connection methods:
   unix:FILE               Unix domain socket named FILE
   tcp:IP:PORT             TCP socket to IP with port no of PORT
+  ssl:IP:PORT             SSL socket to IP with port no of PORT
 
 Passive %s connection methods:
   punix:FILE              Listen on Unix domain socket FILE""" % (name, name)
@@ -385,3 +407,66 @@  class TCPStream(Stream):
             sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
         return error, sock
 Stream.register_method("tcp", TCPStream)
+
+
+class SSLStream(Stream):
+
+    @staticmethod
+    def verify_cb(conn, cert, errnum, depth, ok):
+        return ok
+
+    @staticmethod
+    def _open(suffix, dscp):
+        error, sock = TCPStream._open(suffix, dscp)
+        if error:
+            return error, None
+
+        # Create an SSL context
+        ctx = SSL.Context(SSL.SSLv23_METHOD)
+        ctx.set_verify(SSL.VERIFY_PEER, SSLStream.verify_cb)
+        ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
+        ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF)
+        # If the client has not set the SSL configuration files
+        # exception would be raised.
+        ctx.use_privatekey_file(Stream._SSL_private_key_file)
+        ctx.use_certificate_file(Stream._SSL_certificate_file)
+        ctx.load_verify_locations(Stream._SSL_ca_cert_file)
+
+        ssl_sock = SSL.Connection(ctx, sock)
+        ssl_sock.set_connect_state()
+        return error, ssl_sock
+
+    def connect(self):
+        retval = super(SSLStream, self).connect()
+
+        if retval:
+            return retval
+
+        # TCP Connection is successful. Now do the SSL handshake
+        try:
+            self.socket.do_handshake()
+        except SSL.WantReadError:
+            return errno.EAGAIN
+
+        return 0
+
+    def recv(self, n):
+        try:
+            return super(SSLStream, self).recv(n)
+        except SSL.WantReadError:
+            return (errno.EAGAIN, "")
+
+    def send(self, buf):
+        try:
+            if isinstance(buf, six.text_type):
+                # Convert to byte stream if the buffer is string type/unicode.
+                # pyopenssl version 0.14 expects the buffer to be byte string.
+                buf = buf.encode('utf-8')
+            return super(SSLStream, self).send(buf)
+        except SSL.WantWriteError:
+            return errno.EAGAIN
+
+
+if SSL:
+    # Register SSL only if the OpenSSL module is available
+    Stream.register_method("ssl", SSLStream)
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index d633dbb..e57a3a4 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -1198,10 +1198,36 @@  m4_define([OVSDB_CHECK_IDL_NOTIFY_PY],
    OVSDB_SERVER_SHUTDOWN
    AT_CLEANUP])
 
+# This test uses the Python IDL implementation with ssl
+m4_define([OVSDB_CHECK_IDL_NOTIFY_SSL_PY],
+  [AT_SETUP([$1 - SSL])
+   AT_SKIP_IF([test $HAVE_PYTHON = no])
+   $PYTHON -m OpenSSL.SSL
+   SSL_PRESENT=$?
+   AT_SKIP_IF([test $SSL_PRESENT != 0])
+   AT_KEYWORDS([ovsdb server idl Python notify - ssl socket])
+   AT_CHECK([ovsdb-tool create db $abs_srcdir/idltest.ovsschema],
+             [0], [stdout], [ignore])
+   PKIDIR=$abs_top_builddir/tests
+   AT_CHECK([ovsdb-server --log-file '-vPATTERN:console:ovsdb-server|%c|%m' \
+             --detach --no-chdir --pidfile="`pwd`"/pid \
+             --private-key=$PKIDIR/testpki-privkey2.pem \
+             --certificate=$PKIDIR/testpki-cert2.pem \
+             --ca-cert=$PKIDIR/testpki-cacert.pem \
+             --remote=pssl:0:127.0.0.1 --unixctl="`pwd`"/unixctl db], [0], [ignore], [ignore])
+   PARSE_LISTENING_PORT([ovsdb-server.log], [TCP_PORT])
+   AT_CHECK([$PYTHON $srcdir/test-ovsdb.py  -t10 idl $srcdir/idltest.ovsschema \
+             ssl:127.0.0.1:$TCP_PORT $PKIDIR/testpki-privkey.pem \
+             $PKIDIR/testpki-cert.pem $PKIDIR/testpki-cacert.pem $2],
+            [0], [stdout], [ignore], [kill `cat pid`])
+   AT_CHECK([sort stdout | ${PERL} $srcdir/uuidfilt.pl]m4_if([$5],,, [[| $5]]),
+            [0], [$3], [], [kill `cat pid`])
+   OVSDB_SERVER_SHUTDOWN
+   AT_CLEANUP])
 
 m4_define([OVSDB_CHECK_IDL_NOTIFY],
-   [OVSDB_CHECK_IDL_NOTIFY_PY($@)])
-
+   [OVSDB_CHECK_IDL_NOTIFY_PY($@)
+    OVSDB_CHECK_IDL_NOTIFY_SSL_PY($@)])
 
 OVSDB_CHECK_IDL_NOTIFY([simple idl verify notify],
   [['track-notify' \
diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
index e1cfdad..b27ad28 100644
--- a/tests/test-ovsdb.py
+++ b/tests/test-ovsdb.py
@@ -27,6 +27,7 @@  from ovs.db import data
 import ovs.db.types
 import ovs.ovsuuid
 import ovs.poller
+import ovs.stream
 import ovs.util
 from ovs.fatal_signal import signal_alarm
 import six
@@ -519,6 +520,12 @@  def do_idl(schema_file, remote, *commands):
     schema_helper = ovs.db.idl.SchemaHelper(schema_file)
     track_notify = False
 
+    if remote.startswith("ssl:"):
+        ovs.stream.Stream.ssl_set_private_key_file(commands[0])
+        ovs.stream.Stream.ssl_set_certificate_file(commands[1])
+        ovs.stream.Stream.ssl_set_ca_cert_file(commands[2])
+        commands = commands[3:]
+
     if commands and commands[0] == "track-notify":
         commands = commands[1:]
         track_notify = True