help-gsasl
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH] python: Add Python bindings for libgsasl


From: David Michael
Subject: [PATCH] python: Add Python bindings for libgsasl
Date: Tue, 30 Jun 2015 13:52:26 -0400

This change adds a Python module that provides an interface to the
main libgsasl functions.  It is compatible with any Python versions
2.6 or later, including Python 3.

  * __init__.py:  The Pythonic interface to libgsasl is provided by
    this file.  It imports enum.py for named numerical constants.

  * enum.py:  Enumerations found in gsasl.h are converted to Python
    dictionaries defined in this file.  It has a Makefile rule that
    depends on gsasl.h, so it gets regenerated automatically if new
    constants are defined.  It will be generated by the maintainer
    during "make dist" so users of release archives won't need to
    build this file if gsasl.h has not been modified.

  * tests.py:  This file can be run through a Python interpreter to
    verify that all of the functions provided by __init__.py can be
    called and return successfully.  It is run by "make check".

The libgsasl build procedure was adjusted to include the above.

  * configure.ac:  The option --with-python/--without-python was
    added to decide whether to install the Python module.  If the
    option is not specified, the module is installed only if a
    usable Python interpreter is found.  If "--with" is given, the
    script aborts if it can't find a usable Python interpreter.  If
    "--without" is given, no check is performed.  Users can choose
    specific interpreters by setting the precious variable PYTHON.

  * Makefile.am:  When the configure script finds a usable Python
    interpreter, Make descends into the "python" subdirectory.

  * python/Makefile.am:  The module's Makefile ensures that enum.py
    has been built, orchestrates running tests.py for "make check",
    and installs and byte-compiles all the Python module files.

  * python/mkenum.sed:  This is a sed script that is called by the
    Makefile to generate enum.py from gsasl.h enumerations.

  * .gitignore:  Compiled Python files are now ignored.  Also, the
    file enum.py is ignored since it should be freshly generated
    similar to configure etc. during "make dist".
---


Hi,

I needed an easily portable SASL library for Python, so I wrote bindings
for libgsasl.  I've used it on various GNU/Linux distros, Hurd, Mac OS
X, and Windows.

Is there any interest in including the Python module with the upstream
project?

Thanks.

David


 .gitignore             |   3 +
 lib/Makefile.am        |   4 +
 lib/configure.ac       |  19 +++
 lib/python/Makefile.am |  25 +++
 lib/python/__init__.py | 411 +++++++++++++++++++++++++++++++++++++++++++++++++
 lib/python/mkenum.sed  |  24 +++
 lib/python/tests.py    | 200 ++++++++++++++++++++++++
 7 files changed, 686 insertions(+)
 create mode 100644 lib/python/Makefile.am
 create mode 100644 lib/python/__init__.py
 create mode 100644 lib/python/mkenum.sed
 create mode 100644 lib/python/tests.py

diff --git a/.gitignore b/.gitignore
index d41560b..8a77619 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
 *.gcda
 *.gcno
 *.gcov
+*.py[cdo]
 *~
+__pycache__/
 ABOUT-NLS
 ChangeLog
 INSTALL
@@ -846,6 +848,7 @@ lib/po/quot.sed
 lib/po/remove-potcdate.sed
 lib/po/remove-potcdate.sin
 lib/po/stamp-po
+lib/python/enum.py
 lib/saml20/.deps/
 lib/saml20/.libs/
 lib/saml20/Makefile
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 88298d7..f89c4d0 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -90,4 +90,8 @@ if KERBEROS_V5
 SUBDIRS += kerberos_v5
 endif
 
+if PYTHON
+SUBDIRS += python
+endif
+
 SUBDIRS += src tests gltests
diff --git a/lib/configure.ac b/lib/configure.ac
index 5f984de..2bb9ad1 100644
--- a/lib/configure.ac
+++ b/lib/configure.ac
@@ -327,6 +327,23 @@ fi
 AC_MSG_CHECKING([if non-ASCII support via Libidn should be built])
 AC_MSG_RESULT($stringprep)
 
+# Check for Python.
+AC_ARG_WITH([python],
+  AS_HELP_STRING([--without-python], [do not install Python bindings]),
+  [python=$withval], [python=auto])
+if test "x$python" != "xno"; then
+AM_PATH_PYTHON(,, [:])
+  if test "x$pythondir" != "x"; then
+    python=yes
+  else
+    if test "x$python" = "xyes"; then
+AC_MSG_ERROR([Python was explicitly requested, but could not be found])
+    fi
+    python=no
+  fi
+fi
+AM_CONDITIONAL([PYTHON], [test "$python" != "no"])
+
 # Allow disabling of client or server.
 AC_ARG_ENABLE(client,
               AS_HELP_STRING([--disable-client], [disable client code]),
@@ -427,6 +444,7 @@ AC_CONFIG_FILES([
   ntlm/Makefile
   plain/Makefile \
   po/Makefile.in
+  python/Makefile
   securid/Makefile
   src/Makefile
   tests/Makefile
@@ -441,6 +459,7 @@ AC_MSG_NOTICE([summary of library build options:
   Compiler:           cc: ${CC} cflags: ${CFLAGS} cppflags: ${CPPFLAGS}
   Warning flags:      $gl_gcc_warnings
   Library types:      Shared=${enable_shared}, Static=${enable_static}
+  Python bindings:    $python
   Valgrind:           ${VALGRIND}
   Obsolete functions: $obsolete
 
diff --git a/lib/python/Makefile.am b/lib/python/Makefile.am
new file mode 100644
index 0000000..137b69e
--- /dev/null
+++ b/lib/python/Makefile.am
@@ -0,0 +1,25 @@
+BUILT_SOURCES = enum.py
+EXTRA_DIST = mkenum.sed
+MAINTAINERCLEANFILES = enum.py
+
+moduledir = ${pythondir}/gsasl
+module_PYTHON = __init__.py enum.py tests.py
+
+enum.py: $(top_srcdir)/src/gsasl.h
+       $(SED) -n -f $(srcdir)/mkenum.sed $< > $@
+
+AM_LOG_FLAGS = -B
+AM_TESTS_ENVIRONMENT = PYTHONPATH=test; export PYTHONPATH; \
+LD_LIBRARY_PATH='$(abs_top_builddir)/src/.libs'; export LD_LIBRARY_PATH;
+LOG_COMPILER = $(PYTHON)
+TESTS = test/gsasl/tests.py
+
+# Assemble a complete module to test a rebuilt enum.py in a separate builddir.
+test/gsasl/tests.py: tests.py test/gsasl/__init__.py
+       $(MKDIR_P) $(@D); $(RM) $@; $(LN_S) ../../$< $@
+test/gsasl/__init__.py: __init__.py test/gsasl/enum.py
+       $(MKDIR_P) $(@D); $(RM) $@; $(LN_S) ../../$< $@
+test/gsasl/enum.py: enum.py
+       $(MKDIR_P) $(@D); $(RM) $@; $(LN_S) ../../$< $@
+clean-local:
+       -rm -rf test
diff --git a/lib/python/__init__.py b/lib/python/__init__.py
new file mode 100644
index 0000000..39d0c0f
--- /dev/null
+++ b/lib/python/__init__.py
@@ -0,0 +1,411 @@
+"""Pythonic interface to the gsasl library
+
+The module provides the following public classes:
+
+Gsasl(object) -- Initializes a libgsasl context and operates on it
+GsaslSession(object) -- Stores a session context and operates on it
+GsaslClient(GsaslSession) -- Initializes a client session context
+GsaslServer(GsaslSession) -- Initializes a server session context
+GsaslError(Exception) -- Raised when libgsasl functions return errors
+
+The module provides the following public functions:
+
+check_version -- Tests if the library is at least as new as requested
+saslprep -- Prepare a string (stringprep) with the SASLprep profile
+
+The module provides the following submodule:
+
+enum -- Exports dictionaries of named numerical constants
+
+Most of libgsasl's functionality is available through the above classes
+and functions, but the following library functions were omitted:
+
+  * gsasl_base64_from, gsasl_base64_to
+        Use the base64 module for this functionality.
+
+  * gsasl_hmac_md5, gsasl_hmac_sha1, gsasl_md5, gsasl_sha1
+        Use the hashlib and hmac modules for this functionality.
+
+  * gsasl_nonce, gsasl_random
+        Use the random module for this functionality.
+
+  * gsasl_simple_getpass
+        Use "if line.startswith(username + '\t')" in a file loop.
+
+  * gsasl_callback, gsasl_callback_set,
+    gsasl_callback_hook_get, gsasl_callback_hook_set,
+    gsasl_session_hook_get, gsasl_session_hook_set
+        Application callbacks are not implemented yet.
+
+The module provides the following functions that are not used in normal
+operation, but they can access the unexported libgsasl functions:
+
+get_library_handle -- Returns a CDLL object for libgsasl
+pythonize_memory -- Converts a C memory pointer to a Python string
+"""
+
+from ctypes import CDLL, byref, c_char_p, c_int, create_string_buffer, memmove
+from ctypes.util import find_library
+
+from gsasl import enum
+
+
+# Utility Functions
+
+def _b2s(data):
+    """Convert bytes to str for Python 3 ctypes compatibility."""
+    if data is not None and not isinstance(data, str):
+        return data.decode('utf-8')
+    return data
+
+
+def _s2b(string):
+    """Convert str to bytes for Python 3 ctypes compatibility."""
+    if string is not None and not isinstance(string, bytes):
+        return string.encode('utf-8')
+    return string
+
+
+def _s2p(string):
+    """Convert str to c_char_p for compatibility and convenience."""
+    if string is not None and not isinstance(string, bytes):
+        return c_char_p(string.encode('utf-8'))
+    return c_char_p(string)
+
+
+def get_library_handle():
+    """Return a CDLL object providing the raw interface to libgsasl.
+
+    Many of the library functions' return types are also specified here
+    before returning the CDLL object.  They are configured to either
+    raise an exception on non-zero return codes or to return a string
+    instead of constant character pointers.
+    """
+    def cb_errcheck_ok(result, func, arguments):
+        """Raise a GsaslError on non-zero return codes from libgsasl."""
+        if result != enum.rc.OK:
+            raise GsaslError(result)
+        return result
+
+    # Get a libgsasl handle, either a UNIX SO or a Windows DLL.
+    name = find_library('gsasl') or find_library('libgsasl-7') or 'libgsasl.so'
+    libgsasl = CDLL(name)
+
+    # Prepare functions that should always return GSASL_OK.
+    libgsasl.gsasl_base64_from.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_base64_to.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_client_mechlist.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_client_start.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_decode.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_encode.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_hmac_md5.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_hmac_sha1.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_init.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_md5.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_nonce.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_random.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_saslprep.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_server_mechlist.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_server_start.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_sha1.errcheck = cb_errcheck_ok
+    libgsasl.gsasl_simple_getpass.errcheck = cb_errcheck_ok
+
+    # Prepare functions that return an internally managed string.
+    libgsasl.gsasl_check_version.restype = c_char_p
+    libgsasl.gsasl_client_suggest_mechanism.restype = c_char_p
+    libgsasl.gsasl_mechanism_name.restype = c_char_p
+    libgsasl.gsasl_property_fast.restype = c_char_p
+    libgsasl.gsasl_property_get.restype = c_char_p
+    libgsasl.gsasl_strerror.restype = c_char_p
+    libgsasl.gsasl_strerror_name.restype = c_char_p
+
+    return libgsasl
+
+
+def pythonize_memory(pointer, length=None):
+    """Convert data at a pointer to a Python string and free its memory.
+
+    For binary data, this method should be given a c_char_p pointing at
+    the data and its length in bytes as arguments.
+
+    For plain (NUL-terminated) text, the length argument can be omitted.
+    """
+    if pointer is None or pointer.value is None:
+        return None
+    if length is None:
+        length = len(pointer.value)  # Don't copy the trailing NUL for text.
+    string_buffer = create_string_buffer(length)
+    memmove(string_buffer, pointer, length)
+    _lib.gsasl_free(pointer)
+    return string_buffer.raw
+
+
+# Pythonic Library Interface
+
+class GsaslError(Exception):
+    """Create an exception using error codes from libgsasl functions."""
+
+    def __init__(self, err):
+        """Initialize an error message generated from a return code.
+
+        The specific error code is saved as the "err" attribute, which
+        can be used in "except" blocks to handle known problems.
+        """
+        self.err = err
+        message = _b2s(_lib.gsasl_strerror(err))
+        name = _b2s(_lib.gsasl_strerror_name(err))
+        if name is not None:
+            message = '%s: %s' % (name, message)
+        super(GsaslError, self).__init__(message)
+
+
+class GsaslSession(object):
+    """Create an object that stores and operates on a session context.
+
+    This class should not be initialized directly.  Instead, see the
+    GsaslClient and GsaslServer subclasses which provide the session
+    context.  Any users that don't care about client vs server contexts
+    can do type checking with this class, while others can test with
+    GsaslClient and GsaslServer.
+    """
+
+    def __init__(self, gsasl, sctx):
+        """Initialize a session object by storing a context pointer.
+
+        The session context itself should be initialized by the calling
+        function.  That allows this class to remain unaware of the type
+        of session it's using.
+
+        A reference to the Gsasl object that initialized the session
+        context is also stored here so that it does not fall victim to
+        garbage collection while the session is still active.
+        """
+        self.gsasl = gsasl
+        self.sctx = sctx
+
+    def __del__(self):
+        """Destroy the session context."""
+        _lib.gsasl_finish(self.sctx)
+
+    @property
+    def mechanism_name(self):
+        """Return the name of the mechanism used by this session."""
+        return _b2s(_lib.gsasl_mechanism_name(self.sctx))
+
+    def decode(self, data):
+        """Decode the given data using the session's parameters.
+
+        The returned value is a byte string to support binary data.
+        """
+        input_bytes = _s2b(data)
+        input_len = 0 if input_bytes is None else len(input_bytes)
+        output = c_char_p()
+        output_len = c_int(0)
+        _lib.gsasl_decode(self.sctx,
+                          c_char_p(input_bytes), input_len,
+                          byref(output), byref(output_len))
+        return pythonize_memory(output, output_len.value)
+
+    def encode(self, data):
+        """Encode the given data using the session's parameters.
+
+        The returned value is a byte string to support binary data.
+        """
+        input_bytes = _s2b(data)
+        input_len = 0 if input_bytes is None else len(input_bytes)
+        output = c_char_p()
+        output_len = c_int(0)
+        _lib.gsasl_encode(self.sctx,
+                          c_char_p(input_bytes), input_len,
+                          byref(output), byref(output_len))
+        return pythonize_memory(output, output_len.value)
+
+    def property_fast(self, prop):
+        """Return the value of the given property, skipping callbacks.
+
+        If the given property is not a numerical value, assume it is a
+        key to look up the numerical value in enum.property.
+
+        The returned data is a byte string to support binary data.
+        """
+        if not isinstance(prop, int):
+            prop = enum.property[prop.upper()]
+        return _lib.gsasl_property_fast(self.sctx, prop)
+
+    def property_get(self, prop):
+        """Return the value of the given property.
+
+        If the given property is not a numerical value, assume it is a
+        key to look up the numerical value in enum.property.
+
+        The returned data is a byte string to support binary data.
+        """
+        if not isinstance(prop, int):
+            prop = enum.property[prop.upper()]
+        return _lib.gsasl_property_get(self.sctx, prop)
+
+    def property_set(self, prop, data):
+        """Store the given data as the value of the given property.
+
+        If the given property is not a numerical value, assume it is a
+        key to look up the numerical value in enum.property.
+
+        The actual library function used here is gsasl_property_set_raw
+        so that binary data in a Python string can be stored correctly.
+        """
+        if not isinstance(prop, int):
+            prop = enum.property[prop.upper()]
+        bdata = _s2b(data)
+        length = 0 if bdata is None else len(bdata)
+        _lib.gsasl_property_set_raw(self.sctx, prop, c_char_p(bdata), length)
+
+    def property_set_bulk(self, **kwargs):
+        """Given "key=value" arguments, set property "key" to "value".
+
+        For readability, property keys will be converted to uppercase.
+        """
+        for key, value in kwargs.items():
+            self.property_set(key, value)
+
+    def step(self, data=None):
+        """Return a step of SASL authentication output.
+
+        A tuple of the output data and a boolean are returned.  The
+        output data will be a byte string since it can be binary.  The
+        boolean will be True if authentication ended successfully, or it
+        will be False if more data is required.
+        """
+        input_bytes = _s2b(data)
+        input_len = 0 if input_bytes is None else len(input_bytes)
+        output = c_char_p()
+        output_len = c_int(0)
+        err = _lib.gsasl_step(self.sctx,
+                              c_char_p(input_bytes), input_len,
+                              byref(output), byref(output_len))
+        if err not in (enum.rc.OK, enum.rc.NEEDS_MORE):
+            raise GsaslError(err)
+        return pythonize_memory(output, output_len.value), err == enum.rc.OK
+
+    def step64(self, data=None):
+        """Given base64-encoded input, return encoded step output.
+
+        A tuple of the output data and a boolean are returned.  The
+        output data will be a string of the base64-encoded results.  The
+        boolean will be True if authentication ended successfully, or it
+        will be False if more data is required.
+        """
+        output = c_char_p()
+        err = _lib.gsasl_step64(self.sctx, _s2p(data), byref(output))
+        if err not in (enum.rc.OK, enum.rc.NEEDS_MORE):
+            raise GsaslError(err)
+        return _b2s(pythonize_memory(output)), err == enum.rc.OK
+
+
+class GsaslClient(GsaslSession):
+    """Create a GsaslSession with a client session context."""
+
+    def __init__(self, gsasl, mechanism):
+        """Initialize a new client context using the given mechanism."""
+        super(GsaslClient, self).__init__(gsasl, c_char_p())
+        _lib.gsasl_client_start(gsasl.ctx, _s2p(mechanism), byref(self.sctx))
+
+
+class GsaslServer(GsaslSession):
+    """Create a GsaslSession with a server session context."""
+
+    def __init__(self, gsasl, mechanism):
+        """Initialize a new server context using the given mechanism."""
+        super(GsaslServer, self).__init__(gsasl, c_char_p())
+        _lib.gsasl_server_start(gsasl.ctx, _s2p(mechanism), byref(self.sctx))
+
+
+class Gsasl(object):
+    """Create and operate on a libgsasl library context.
+
+    Instances of this class can be used to determine supported mechanism
+    names and spawn client and server sessions.
+    """
+
+    def __init__(self):
+        """Initialize and save a new library context pointer."""
+        self.ctx = c_char_p()
+        _lib.gsasl_init(byref(self.ctx))
+
+    def __del__(self):
+        """Destroy the library context."""
+        _lib.gsasl_done(self.ctx)
+
+    @property
+    def client_mechlist(self):
+        """Return a list of mechanisms available to client sessions."""
+        output = c_char_p()
+        _lib.gsasl_client_mechlist(self.ctx, byref(output))
+        return _b2s(pythonize_memory(output)).split()
+
+    @property
+    def server_mechlist(self):
+        """Return a list of mechanisms available to server sessions."""
+        output = c_char_p()
+        _lib.gsasl_server_mechlist(self.ctx, byref(output))
+        return _b2s(pythonize_memory(output)).split()
+
+    def client_suggest_mechanism(self, mechs=None):
+        """Return a suggested mechanism name that the client supports.
+
+        If a list (or space-separated string) of mechanism names is
+        given as an argument, the name returned will be a name that is
+        both in the list and supported by the client, or None if no such
+        mechanism exists.
+        """
+        if mechs is None:
+            mechs = self.client_mechlist
+        if hasattr(mechs, '__iter__') and not isinstance(mechs, (str, bytes)):
+            mechs = ' '.join(mechs)
+        return _b2s(_lib.gsasl_client_suggest_mechanism(self.ctx, _s2p(mechs)))
+
+    def client_start(self, mechanism):
+        """Return a new client session using the given mechanism."""
+        return GsaslClient(self, mechanism)
+
+    def client_support(self, mechanism):
+        """Return whether the client supports the given mechanism."""
+        return _lib.gsasl_client_support_p(self.ctx, _s2p(mechanism)) != 0
+
+    def server_start(self, mechanism):
+        """Return a new server session using the given mechanism."""
+        return GsaslServer(self, mechanism)
+
+    def server_support(self, mechanism):
+        """Return whether the server supports the given mechanism."""
+        return _lib.gsasl_server_support_p(self.ctx, _s2p(mechanism)) != 0
+
+
+def check_version(version=None):
+    """Test the libgsasl version against the given version number.
+
+    Return the library version if it is at least as new as the given
+    value, or return None otherwise.  If the version argument is None,
+    the library version is always returned.
+    """
+    return _b2s(_lib.gsasl_check_version(_s2p(version)))
+
+
+def saslprep(string, flags=0, **kwargs):
+    """Run the given string through the stringprep's SASLprep profile.
+
+    Values from enum.saslprep_flags can be passed as the flags argument.
+    Alternatively, names from enum.saslprep_flags can be supplied like
+    "name=True" or "name=False" to set or unset the flags, respectively.
+    """
+    if string is None:
+        return None  # Avoid passing NULL, otherwise saslprep segfaults.
+    for key, value in kwargs.items():
+        flag = enum.saslprep_flags[key.upper()]
+        flags = flags | flag if value else flags & ~flag
+    output = c_char_p()
+    prep_rc = c_int(0)  # This is currently unused after being set.
+    _lib.gsasl_saslprep(_s2p(string), flags, byref(output), byref(prep_rc))
+    return _b2s(pythonize_memory(output))
+
+
+_lib = get_library_handle()
diff --git a/lib/python/mkenum.sed b/lib/python/mkenum.sed
new file mode 100644
index 0000000..b7ce00b
--- /dev/null
+++ b/lib/python/mkenum.sed
@@ -0,0 +1,24 @@
+#!/bin/sed -nf
+
+# Define a way to access dict keys as attributes for readability.
+1s/.*/class EnumHack(dict):\n    __getattr__ = dict.get/p;
+
+# In a definition of an enumeration...
+/^ *typedef enum *$/,/}/{
+
+    # On a line defining a constant...
+    /= *[0-9]/{
+        # Create a Python dict entry with namespaces removed.
+        s/^ *GSASL_\([0-9A-Z_]*\) *= *\([0-9]\+\).*/    '\1': \2,/;
+        # Append to the hold space if something's there, otherwise replace it.
+        x;/./{x;H;};/^$/{x;h;};
+    };
+
+    # At the end of the enumeration...
+    /}/{
+        # Create the assignment with namespaces removed.
+        s/.*} *Gsasl_\([^ ;]*\).*/\n\1 = EnumHack({/;
+       # Prepend to the hold space, append the closing braces, and print it.
+        x;H;s/.*//;x;s/$/\n})/p;
+    };
+};
diff --git a/lib/python/tests.py b/lib/python/tests.py
new file mode 100644
index 0000000..5807801
--- /dev/null
+++ b/lib/python/tests.py
@@ -0,0 +1,200 @@
+"""Tests for the gsasl Python interface
+
+This file provides the following public classes:
+
+BaseGsaslTestCase(TestCase) -- Tests check_version and saslprep
+GsaslTestCase(TestCase) -- Tests Gsasl
+GsaslClientTestCase(TestCase) -- Tests GsaslClient
+GsaslServerTestCase(GsaslClientTestCase) -- Tests GsaslServer
+
+The tests defined by these classes are intended to verify only that the
+Python interface successfully returns correct data types and doesn't
+cause the interpreter to crash.  It is expected that the actual libgsasl
+functionality and standards conformance have been tested separately.
+"""
+
+from unittest import TestCase, main
+
+from gsasl import Gsasl, GsaslError, GsaslClient, GsaslServer
+from gsasl import check_version, saslprep
+
+
+# Choose a supported mechanism name to test against.
+MECH = 'PLAIN'
+
+
+class BaseGsaslTestCase(TestCase):
+    """Test standalone functions."""
+
+    def test_check_version(self):
+        """Test passing older, newer, and no version strings."""
+        version = check_version()
+        self.assertIsInstance(version, str)
+        self.assertEqual(check_version('0'), version)
+        self.assertIsNone(check_version('999'))
+
+    def test_saslprep(self):
+        """Test that ASCII characters are unchanged.
+
+        Assume libidn was already tested for the real functionality used
+        here, since if gsasl was built without libidn, saslprep will
+        raise an error on a non-ASCII character.
+        """
+        self.assertEqual(saslprep('abc'), 'abc')
+        self.assertIsNone(saslprep(None))
+
+
+class GsaslTestCase(TestCase):
+    """Test functions that rely on an initialized library context."""
+
+    def setUp(self):
+        """Set up a library context."""
+        self.gsasl = Gsasl()
+
+    def tearDown(self):
+        """Destroy the library context."""
+        del self.gsasl
+
+    def test_client_mechlist(self):
+        """Test that the client has a list of mechanisms."""
+        mechlist = self.gsasl.client_mechlist
+        self.assertIsInstance(mechlist, list)
+        self.assertIn(MECH, mechlist)
+
+    def test_client_start(self):
+        """Test that a client can be started."""
+        client = self.gsasl.client_start(MECH)
+        self.assertIsInstance(client, GsaslClient)
+        self.assertEqual(client.gsasl, self.gsasl)
+        self.assertRaises(GsaslError, lambda: self.gsasl.client_start(None))
+        self.assertRaises(GsaslError, lambda: self.gsasl.client_start(''))
+
+    def test_client_suggest_mechanism(self):
+        """Test that mechanisms are suggested from lists and strings."""
+        self.assertIsInstance(self.gsasl.client_suggest_mechanism(), str)
+        self.assertEqual(self.gsasl.client_suggest_mechanism(['X', MECH]),
+                         MECH)
+        self.assertEqual(self.gsasl.client_suggest_mechanism('X %s Y' % MECH),
+                         MECH)
+        self.assertEqual(self.gsasl.client_suggest_mechanism(MECH), MECH)
+        self.assertIsNone(self.gsasl.client_suggest_mechanism(['X', 'Y', 'Z']))
+        self.assertIsNone(self.gsasl.client_suggest_mechanism('X Y Z'))
+        self.assertIsNone(self.gsasl.client_suggest_mechanism(' '))
+        self.assertIsNone(self.gsasl.client_suggest_mechanism(''))
+
+    def test_client_support(self):
+        """Test that the client supports a mechanism."""
+        self.assertTrue(self.gsasl.client_support(MECH))
+        self.assertFalse(self.gsasl.client_support(None))
+        self.assertFalse(self.gsasl.client_support('NONSENSE'))
+        self.assertFalse(self.gsasl.client_support(' '))
+        self.assertFalse(self.gsasl.client_support(''))
+
+    def test_server_mechlist(self):
+        """Test that the server has a list of mechanisms."""
+        mechlist = self.gsasl.client_mechlist
+        self.assertIsInstance(mechlist, list)
+        self.assertIn(MECH, mechlist)
+
+    def test_server_start(self):
+        """Test that a server can be started."""
+        server = self.gsasl.server_start(MECH)
+        self.assertIsInstance(server, GsaslServer)
+        self.assertEqual(server.gsasl, self.gsasl)
+        self.assertRaises(GsaslError, lambda: self.gsasl.server_start(None))
+        self.assertRaises(GsaslError, lambda: self.gsasl.server_start(''))
+
+    def test_server_support(self):
+        """Test that the server supports a mechanism."""
+        self.assertTrue(self.gsasl.server_support(MECH))
+        self.assertFalse(self.gsasl.server_support(None))
+        self.assertFalse(self.gsasl.server_support('NONSENSE'))
+        self.assertFalse(self.gsasl.server_support(' '))
+        self.assertFalse(self.gsasl.server_support(''))
+
+
+class GsaslClientTestCase(TestCase):
+    """Test functions that rely on an initialized client context."""
+
+    def setUp(self):
+        """Set up a client session context."""
+        self.session = Gsasl().client_start(MECH)
+
+    def tearDown(self):
+        """Destroy the session context."""
+        del self.session
+
+    def test_encode(self):
+        """Test PLAIN encoding."""
+        self.assertEqual(self.session.encode('text'), b'text')
+        self.assertEqual(self.session.encode(None), b'')
+
+    def test_decode(self):
+        """Test PLAIN decoding."""
+        self.assertEqual(self.session.decode('text'), b'text')
+        self.assertEqual(self.session.decode(None), b'')
+
+    def test_mechanism_name(self):
+        """Test that the mechanism name matches the constructor."""
+        self.assertEqual(self.session.mechanism_name, MECH)
+
+    def test_properties(self):
+        """Test setting and getting session properties."""
+        self.assertIsNone(self.session.property_fast('AUTHID'))
+        self.assertIsNone(self.session.property_get('AUTHID'))
+        self.session.property_set('AUTHID', 'user1')
+        self.assertEqual(self.session.property_fast('AUTHID'), b'user1')
+        self.assertEqual(self.session.property_get('AUTHID'), b'user1')
+        self.session.property_set_bulk(authid='user2', authzid='user3')
+        self.assertEqual(self.session.property_fast('AUTHID'), b'user2')
+        self.assertEqual(self.session.property_get('AUTHID'), b'user2')
+        self.assertEqual(self.session.property_fast('AUTHZID'), b'user3')
+        self.assertEqual(self.session.property_get('AUTHZID'), b'user3')
+        self.session.property_set('AUTHID', None)
+        self.assertIsNone(self.session.property_fast('AUTHID'))
+        self.assertIsNone(self.session.property_get('AUTHID'))
+
+    def test_step(self):
+        """Test a PLAIN client step."""
+        username = 'username'
+        password = 'password'
+        expect = ('\0%s\0%s' % (username, password)).encode('ascii')
+        self.session.property_set_bulk(authid=username, password=password)
+        output, finished = self.session.step()
+        self.assertTrue(finished)
+        self.assertEqual(output, expect)
+
+    def test_step64(self):
+        """Test a base64-encoded PLAIN client step."""
+        from base64 import b64encode
+        username = 'username'
+        password = 'password'
+        expect = ('\0%s\0%s' % (username, password)).encode('ascii')
+        self.session.property_set_bulk(authid=username, password=password)
+        output, finished = self.session.step64()
+        self.assertTrue(finished)
+        self.assertEqual(output, b64encode(expect).decode('ascii'))
+
+
+class GsaslServerTestCase(GsaslClientTestCase):
+    """Test functions that rely on an initialized server context."""
+
+    def setUp(self):
+        """Set up a server session context."""
+        self.session = Gsasl().server_start(MECH)
+
+    def test_step(self):
+        """Test a PLAIN server step."""
+        output, finished = self.session.step()
+        self.assertFalse(finished)
+        self.assertIsNone(output)
+
+    def test_step64(self):
+        """Test a base64-encoded PLAIN server step."""
+        output, finished = self.session.step64()
+        self.assertFalse(finished)
+        self.assertEqual(output, '')
+
+
+if __name__ == '__main__':
+    main()
-- 
2.1.0




reply via email to

[Prev in Thread] Current Thread [Next in Thread]