Merge branch 'duosecurity:master' into master
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9f9d081
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,33 @@
+name: Duo Unix CI
+
+on: 
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  nix_ci:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        cc: [gcc, clang]
+
+    steps:
+      - name: Install packages
+        run: sudo apt-get update && sudo apt-get install -y autoconf libtool libpam-dev libssl-dev automake python3 cppcheck
+
+      - name: Checkout repo
+        uses: actions/checkout@v2
+
+      - name: Build
+        run: ./bootstrap && ./configure --with-pam --prefix=/usr && make CC=${{ matrix.cc }}
+
+      - name: Run tests
+        run: sudo make check
+
+      - name: Static analysis
+        run: cppcheck --quiet --force -i tests --suppressions-list=.false_positive.txt --error-exitcode=1 .
diff --git a/.gitignore b/.gitignore
index f06e629..c2ec50e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+*.err
 *.la
 *.lo
 *.o
@@ -39,3 +40,6 @@
 /autotools/test-driver
 /duo_unix-*
 test.conf
+__pycache__/
+Vagrantfile
+.isort.cfg
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 87d595d..beb9e40 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,5 @@
-include: 
+---
+include:
   - project: "mirrors/duo_unix_ci"
     ref: "master"
     file: ".gitlab-ci.yml"
diff --git a/CHANGES b/CHANGES
index 5d3ef7f..c7fce33 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,15 @@
+duo_unix-1.12.0:
+- Switched from BSON to JSON as a data interchange format
+- Switched from Cram to python `unittest` for testing
+
+duo_unix-1.11.5:
+- Added support for Debian 11
+- Removed support for Debian 8
+- Removed support for CentOS 6
+- Fixed MOTD display for non-interactive sessions
+- The support tool now also collects the sudo PAM configuration file
+- Updated pinned certificates
+
 duo_unix-1.11.4:
 - Added support for Ubuntu 20.04
 - Added support tool to collect information (e.g. logs and PAM stacks) for debugging purposes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 34b56a2..4d99fda 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,6 +6,9 @@
 ## Opening Issues
 Before opening an issue on the Github project see if your issue falls into the following categories. If it does please direct your issue to those locations instead.
 
+##### I have a feature request
+Please send feature requests to [Duo Support](https://duo.com/support).  It will get tracked properly and our product team will be able to review it.
+
 ##### My issue is related to a security vulnerability in Duo Unix
 Thank you for reporting this! In order to keep our customers safe we ask that you do NOT open an issue on the public Github page but instead contact us directly using our [Security Response Guide](https://duo.com/labs/security-response).
 
diff --git a/LICENSE b/LICENSE
index 52c49c3..87f5ed7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -485,24 +485,6 @@
 
 ---
 
-compat/bson.[ch]
-
-Copyright 2009, 2010 10gen Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
----
-
 pam_duo/pam_extra.[ch]
 
 Adapted from Linux-PAM
diff --git a/README.md b/README.md
index 02c0c2d..eeedb0b 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
 
 - Debian based Systems
 ```
-$ sudo apt-get install autoconf libtool libpam-dev libssl-dev
+$ sudo apt-get install autoconf libtool libpam-dev libssl-dev make
 ```
 
 - RHEL based systems
@@ -96,30 +96,47 @@
 
 To run all the automated tests simply run
 ```
-$ make check
+$ sudo make check
 ```
-
-To run an individual test
+To run an individual test file
 ```
 $ cd tests/
-$ python cram.py login_duo-1.t
+$ python test_login_duo.py
+```
+To run an individual test suite
+```
+$ cd tests/
+$ python test_login_duo.py TestLoginDuoConfig
+```
+To run an individual test case
+```
+$ cd tests/
+$ python test_login_duo.py TestLoginDuoConfig.test_empty_args
 ```
 
-### Cram Tests
+### Python Tests
 
-For Duo Unix we use [Cram](https://bitheap.org/cram/) to do our testing. Each test file typically starts by creating a mock duo service. After we create that service we list commands followed by the expected output of that command.
-If the output matches, then the cram test passes. If not, it fails.
+For Duo Unix we use the python `unittest` library to do our testing. Each suite
+typically starts by creating a mock duo service. After we create that service
+we perform a series of tests to verify that this software is working as
+expected. Although we use the `unittest` library these are not truely "unit tests"
+as manage subprocesses and generally employ blackbox testing. The true "unit tests"
+for Duo Unix are the unity tests.
 
-Example passing test
+### Testing with coverage
+To generate coverate reports you'll need to compile Duo Unix with the `--with-coverage` options.
+Please note that in order to view HTML version of the coverage reports you'll also need to
+install the python package `gcovr`.
+
+To see the testing coverage of the Duo PAM for example you would run the following at the
+repository root.
 ```
-$ echo "Hello World"
-Hello World
+$ ./configure --with-coverage --with-pam
+$ ./collect_coverage.sh
+$ $BROWSER coverage/pam_duo.html
 ```
-Example failing test
-```
-$ echo "Hello World"
-Goodbye World
-```
+Note that configuring Duo Unix --with-coverage disables any compiler optimizations
+to allow the profiler to better match executed instructions with lines of code.
 
 ### Other testing tips
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..16c9604
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,8 @@
+Duo is committed to providing secure software to all our customers and users.  We take all security concerns seriously and ask that any disclosures be handled responsibly.
+
+# Security Policy
+
+## Reporting a Vulnerability
+**Please do not use Github issues or pull requests to report security vulnerabilities.**
+
+If you believe you have found a security vulnerability in Duo software, please follow our response process described at https://duo.com/support/security-and-reliability/security-response.
diff --git a/Vagrantfile.template b/Vagrantfile.template
new file mode 100644
index 0000000..35a2bf8
--- /dev/null
+++ b/Vagrantfile.template
@@ -0,0 +1,73 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# All Vagrant configuration is done below. The "2" in Vagrant.configure
+# configures the configuration version (we support older styles for
+# backwards compatibility). Please don't change it unless you know what
+# you're doing.
+Vagrant.configure("2") do |config|
+  # The most common configuration options are documented and commented below.
+  # For a complete reference, please see the online documentation at
+  # https://docs.vagrantup.com.
+
+  # Every Vagrant development environment requires a box. You can search for
+  # boxes at https://vagrantcloud.com/search.
+  config.vm.box = "ubuntu/focal64"
+
+  # Disable automatic box update checking. If you disable this, then
+  # boxes will only be checked for updates when the user runs
+  # `vagrant box outdated`. This is not recommended.
+  # config.vm.box_check_update = false
+
+  # Create a forwarded port mapping which allows access to a specific port
+  # within the machine from a port on the host machine. In the example below,
+  # accessing "localhost:8080" will access port 80 on the guest machine.
+  # NOTE: This will enable public access to the opened port
+  # config.vm.network "forwarded_port", guest: 80, host: 8080
+
+  # Create a forwarded port mapping which allows access to a specific port
+  # within the machine from a port on the host machine and only allow access
+  # via 127.0.0.1 to disable public access
+  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
+
+  # Create a private network, which allows host-only access to the machine
+  # using a specific IP.
+  # config.vm.network "private_network", ip: "192.168.33.10"
+
+  # Create a public network, which generally matched to bridged network.
+  # Bridged networks make the machine appear as another physical device on
+  # your network.
+  # config.vm.network "public_network"
+
+  # Share an additional folder to the guest VM. The first argument is
+  # the path on the host to the actual folder. The second argument is
+  # the path on the guest to mount the folder. And the optional third
+  # argument is a set of non-required options.
+  config.vm.synced_folder ".", "/vagrant", type: "rsync",
+      rsync__args: ["-rlIcvit", "--exclude-from=.gitignore"],
+      rsync__exclude: [".git" ]
+
+  # Provider-specific configuration so you can fine-tune various
+  # backing providers for Vagrant. These expose provider-specific options.
+  # Example for VirtualBox:
+  #
+  # config.vm.provider "virtualbox" do |vb|
+  #   # Display the VirtualBox GUI when booting the machine
+  #   vb.gui = true
+  #
+  #   # Customize the amount of memory on the VM:
+  #   vb.memory = "1024"
+  # end
+  #
+  # View the documentation for the provider you are using for more
+  # information on available options.
+
+  # Enable provisioning with a shell script. Additional provisioners such as
+  # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
+  # documentation for more information about their specific syntax and use.
+  config.vm.provision "shell", inline: <<-SHELL
+    export DEBIAN_FRONTEND=noninteractive
+    apt-get update
+    apt-get install -y libpam-dev libssl-dev autoconf make gcc libtool python
+  SHELL
+end
diff --git a/collect_coverage.sh b/collect_coverage.sh
new file mode 100755
index 0000000..d4eb857
--- /dev/null
+++ b/collect_coverage.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+# Run this at the root of a Duo Unix directory that has been compiled with coverage
+# reporting turned on
+
+if ! [ -x "$(command -v gcovr)" ]; then
+    echo "Missing gcovr. Please pip install"
+    exit 1
+fi
+
+mkdir -p coverage
+
+# This section is necessary because otherwise coverage files are created with a
+# file mode of 0100 (due to an issue with linking to compat) which causes
+# errors. To "get ahead of this" we are creating the coverage files and setting
+# their file mode to 700 this allows us to have full coverage and avoid errors.
+
+mkdir -p tests/.libs
+GCDA_FILES=(
+    "/vagrant/pam_duo/.libs/pam_duo_private.gcda"
+    "/vagrant/pam_duo/.libs/pam_duo.gcda"
+    "/vagrant/tests/testpam.gcda"
+    "/vagrant/tests/.libs/testpam_preload.gcda"
+    "/vagrant/pam_duo/.libs/pam_duo_private.gcda"
+    "/vagrant/pam_duo/.libs/pam_duo.gcda"
+    "/vagrant/tests/testpam.gcda"
+    "/vagrant/tests/.libs/testpam_preload.gcda"
+    "/vagrant/lib/.libs/http_parser.gcda"
+    "/vagrant/lib/.libs/urlenc.gcda"
+    "/vagrant/lib/.libs/ini.gcda"
+    "/vagrant/lib/.libs/https.gcda"
+    "/vagrant/lib/.libs/duo.gcda"
+)
+
+for i in "${GCDA_FILES[@]}"; do
+   rm -f "$i"; touch "$i"; chmod 700 "$i"
+done
+
+# end weird permission hacking
+
+make check
+gcovr --xml-pretty --exclude-unreachable-branches --print-summary -o coverage/coverage.xml --root .
+
+if [ -f pam_duo/pam_duo.gcno ]; then
+    (
+        cd pam_duo || return
+        gcov pam_duo.c -o .libs
+        gcovr --txt
+        gcovr --html-details pam_duo.html
+        rm -f .libs/*.gcda
+    )
+    mv pam_duo/*.{css,html} coverage
+else
+    echo "No coverage information found for pam_duo.c"
+fi
+if [ -f login_duo/login_duo.gcno ]; then
+    (
+        cd login_duo || return
+        gcov login_duo.c
+        gcovr --txt
+        gcovr --html-details login_duo.html
+        gcovr --xml-pretty --exclude-unreachable-branches --print-summary -o login_duo.xml --root ${CI_PROJECT_DIR}
+        rm -f *.gcda
+    )
+mv login_duo/*.{css,html} coverage
+else
+    echo "No coverage information found for login_duo.c"
+fi
+if [ -f lib/duo.gcno ]; then
+    (
+        cd lib || return
+        gcov duo.c -o .libs
+        gcovr --txt
+        gcovr --html-details duo.html
+        rm -f .libs/*.gcda
+    )
+    mv lib/*.{css,html} coverage
+else
+    echo "No coverage information found for duo.c"
+fi
diff --git a/configure.ac b/configure.ac
index fa35130..5ffded2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7,7 +7,7 @@
 
 # Package, version, bug report address
 AC_INIT([duo_unix],
-	[1.11.4],
+	[1.12.0],
 	[support@duosecurity.com])
 
 # Tells autoconf where to find necessary build scripts and macros.
@@ -46,8 +46,10 @@
 
 # Compiler options
 if test "x$GCC" = "xyes"; then
-   CFLAGS="$CFLAGS -Wall -D_FORTIFY_SOURCE=2 -fPIE"
-   AC_MSG_NOTICE([Adding gcc options: $CFLAGS])
+   if test "x$with_coverage" != "xyes"; then
+      CFLAGS="$CFLAGS -Wall -D_FORTIFY_SOURCE=2 -fPIE"
+      AC_MSG_NOTICE([Adding gcc options: $CFLAGS])
+   fi
 fi
 GGL_CHECK_STACK_PROTECTOR([has_stack_protector=yes], [has_stack_protector=no])
 IS_AIX=no
@@ -58,6 +60,7 @@
         ;;
     *aix*)
         AC_MSG_NOTICE([-fstack-protector disabled on AIX])
+        CFLAGS="$CFLAGS -Wl,-lm"
         has_stack_protector=no
         IS_AIX=yes
         ;;
@@ -109,6 +112,18 @@
      *)                PAM_DIR="/usr/lib/security" ;; # NetBSD, Solaris, AIX, HP-UX
 esac
 
+AC_ARG_WITH(coverage,
+  AS_HELP_STRING([--with-coverage=COV],[build for coverage testing]),
+  [],
+  [ with_coverage=no ]
+)
+AM_CONDITIONAL([COVERAGE], [ test "x$with_coverage" != "xno" ])
+AS_IF([test "x$with_coverage" != "xno"], [
+   CFLAGS="$CFLAGS -O0 --coverage"
+   LFLAGS="$LFLAGS -lgcov --coverage"
+   AC_MSG_NOTICE([--coverage enabled in CFLAGS])
+])
+
 # Check PAM
 AC_ARG_WITH(pam,
   AS_HELP_STRING([--with-pam=DIR],[build PAM module (and optionally override the default install DIR)]),
diff --git a/duo_unix_support/README.md b/duo_unix_support/README.md
index 32376fe..a290bad 100644
--- a/duo_unix_support/README.md
+++ b/duo_unix_support/README.md
@@ -16,6 +16,7 @@
 
 PAM Stacks
     /etc/pam.d/sshd
+    /etc/pam.d/sudo
     /etc/pam.d/passwd
     /etc/pam.d/common-auth
     /etc/pam.d/system-auth
diff --git a/duo_unix_support/duo_unix_support.sh b/duo_unix_support/duo_unix_support.sh
index ad85fb3..4c9c444 100755
--- a/duo_unix_support/duo_unix_support.sh
+++ b/duo_unix_support/duo_unix_support.sh
@@ -152,6 +152,7 @@
 #Different Unix systesm utilize different files, it is alright if not all are gathered.
 COPY_FILES=(
            "/etc/pam.d/sshd"
+           "/etc/pam.d/sudo"
            "/etc/pam.d/common-auth"
            "/etc/pam.d/passwd"
            "/etc/pam.d/system-auth"
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 65299ff..bd2129e 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -2,9 +2,9 @@
 
 noinst_LTLIBRARIES = libduo.la
 
-libduo_la_SOURCES = bson.h bson.c cacert.h duo.c \
+libduo_la_SOURCES = cacert.h duo.c \
 		    http_parser.h http_parser.c https.h https.c ini.h ini.c \
-		    urlenc.h urlenc.c util.c
+		    urlenc.h urlenc.c util.c parson.h parson.c
 libduo_la_LIBADD = @OPENSSL_LDFLAGS@ @OPENSSL_LIBS@
 # http://sourceware.org/autobook/autobook/autobook_91.html
 libduo_la_LDFLAGS = -no-undefined -version-info 3:0:0 -export-symbols-regex '^duo_'
diff --git a/lib/bson.c b/lib/bson.c
deleted file mode 100644
index 2f6c796..0000000
--- a/lib/bson.c
+++ /dev/null
@@ -1,696 +0,0 @@
-/* bson.c */
-
-/*    Copyright 2009, 2010 10gen Inc.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- */
-
-#include "bson.h"
-#include <stdlib.h>
-#include <string.h>
-#include <stdio.h>
-#include <time.h>
-
-const int initialBufferSize = 128;
-
-/* only need one of these */
-static const int zero = 0;
-
-/* big endian is only used for OID generation. little is used everywhere else */
-#ifdef WORDS_BIGENDIAN
-#define bson_little_endian64(out, in) ( bson_swap_endian64(out, in) )
-#define bson_little_endian32(out, in) ( bson_swap_endian32(out, in) )
-#define bson_big_endian64(out, in) ( memcpy(out, in, 8) )
-#define bson_big_endian32(out, in) ( memcpy(out, in, 4) )
-#else
-#define bson_little_endian64(out, in) ( memcpy(out, in, 8) )
-#define bson_little_endian32(out, in) ( memcpy(out, in, 4) )
-#define bson_big_endian64(out, in) ( bson_swap_endian64(out, in) )
-#define bson_big_endian32(out, in) ( bson_swap_endian32(out, in) )
-#endif
-
-static void bson_swap_endian64(void* outp, const void* inp){
-    const char *in = (const char*)inp;
-    char *out = (char*)outp;
-
-    out[0] = in[7];
-    out[1] = in[6];
-    out[2] = in[5];
-    out[3] = in[4];
-    out[4] = in[3];
-    out[5] = in[2];
-    out[6] = in[1];
-    out[7] = in[0];
-
-}
-static void bson_swap_endian32(void* outp, const void* inp){
-    const char *in = (const char*)inp;
-    char *out = (char*)outp;
-
-    out[0] = in[3];
-    out[1] = in[2];
-    out[2] = in[1];
-    out[3] = in[0];
-}
-
-/* ----------------------------
-   READING
-   ------------------------------ */
-
-bson * bson_empty(bson * obj){
-    static char * data = "\005\0\0\0\0";
-    return bson_init(obj, data, 0);
-}
-
-void bson_copy(bson* out, const bson* in){
-    if (!out) return;
-    out->data = bson_malloc(bson_size(in));
-    out->owned = 1;
-    memcpy(out->data, in->data, bson_size(in));
-}
-
-bson * bson_from_buffer(bson * b, bson_buffer * buf){
-    return bson_init(b, bson_buffer_finish(buf), 1);
-}
-
-bson * bson_init( bson * b , char * data , bson_bool_t mine ){
-    b->data = data;
-    b->owned = mine;
-    return b;
-}
-int bson_size(const bson * b ){
-    int i;
-    if ( ! b || ! b->data )
-        return 0;
-    bson_little_endian32(&i, b->data);
-    return i;
-}
-void bson_destroy( bson * b ){
-    if ( b->owned && b->data )
-        free( b->data );
-    b->data = 0;
-    b->owned = 0;
-}
-
-static char hexbyte(char hex){
-    switch (hex){
-        case '0': return 0x0;
-        case '1': return 0x1;
-        case '2': return 0x2;
-        case '3': return 0x3;
-        case '4': return 0x4;
-        case '5': return 0x5;
-        case '6': return 0x6;
-        case '7': return 0x7;
-        case '8': return 0x8;
-        case '9': return 0x9;
-        case 'a': 
-        case 'A': return 0xa;
-        case 'b':
-        case 'B': return 0xb;
-        case 'c':
-        case 'C': return 0xc;
-        case 'd':
-        case 'D': return 0xd;
-        case 'e':
-        case 'E': return 0xe;
-        case 'f':
-        case 'F': return 0xf;
-        default: return 0x0; /* something smarter? */
-    }
-}
-
-void bson_oid_from_string(bson_oid_t* oid, const char* str){
-    int i;
-    for (i=0; i<12; i++){
-        oid->bytes[i] = (hexbyte(str[2*i]) << 4) | hexbyte(str[2*i + 1]);
-    }
-}
-void bson_oid_to_string(const bson_oid_t* oid, char* str){
-    static const char hex[16] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
-    int i;
-    for (i=0; i<12; i++){
-        str[2*i]     = hex[(oid->bytes[i] & 0xf0) >> 4];
-        str[2*i + 1] = hex[ oid->bytes[i] & 0x0f      ];
-    }
-    str[24] = '\0';
-}
-void bson_oid_gen(bson_oid_t* oid){
-    static int incr = 0;
-    static int fuzz = 0;
-    int i = incr++; /*TODO make atomic*/
-    int t = time(NULL);
-
-    /* TODO rand sucks. find something better */
-    if (!fuzz){
-        srand(t);
-        fuzz = rand();
-    }
-    
-    bson_big_endian32(&oid->ints[0], &t);
-    oid->ints[1] = fuzz;
-    bson_big_endian32(&oid->ints[2], &i);
-}
-
-time_t bson_oid_generated_time(bson_oid_t* oid){
-    time_t out;
-    bson_big_endian32(&out, &oid->ints[0]);
-    return out;
-}
-
-void bson_print( bson * b, const size_t maxBufferSize ){
-    bson_print_raw( b->data , 0, maxBufferSize);
-}
-
-void bson_print_raw( const char * data , int depth , const size_t maxBufferSize ){
-    bson_iterator i;
-    const char * key;
-    int temp;
-    char oidhex[25];
-
-    bson_iterator_init( &i , data , maxBufferSize , bson_fatal_msg );
-    while ( bson_iterator_next( &i , bson_fatal_msg) ){
-        bson_type t = bson_iterator_type( &i );
-        if ( t == 0 )
-            break;
-        key = bson_iterator_key( &i );
-        
-        for ( temp=0; temp<=depth; temp++ )
-            printf( "\t" );
-        printf( "%s : %d \t " , key , t );
-        switch ( t ){
-        case bson_int: printf( "%d" , bson_iterator_int( &i ) ); break;
-        case bson_double: printf( "%f" , bson_iterator_double( &i ) ); break;
-        case bson_bool: printf( "%s" , bson_iterator_bool( &i ) ? "true" : "false" ); break;
-        case bson_string: printf( "%s" , bson_iterator_string( &i ) ); break;
-        case bson_null: printf( "null" ); break;
-        case bson_oid: bson_oid_to_string(bson_iterator_oid(&i), oidhex); printf( "%s" , oidhex ); break;
-        case bson_object:
-        case bson_array:
-            printf( "\n" );
-            bson_print_raw( bson_iterator_value( &i ) , depth + 1, maxBufferSize );
-            break;
-        default:
-            fprintf( stderr , "can't print type : %d\n" , t );
-        }
-        printf( "\n" );
-    }
-}
-
-/* ----------------------------
-   ITERATOR
-   ------------------------------ */
-
-void bson_iterator_init( bson_iterator * i , const char * bson , const int bson_size , void (*fatal_error_func)(int, const char*) ){
-    if(bson_size <= 4) {
-        char msg[] = "Invalid BSON response";
-        fatal_error_func(0, msg);
-        return;
-    }
-
-    i->cur = bson + 4;
-    i->first = 1;
-    i->curSize = 4;
-    i->maxBufferSize = bson_size;
-}
-
-bson_type bson_find(bson_iterator* it, const bson* obj, const char* name, const size_t maxBufferSize){
-    bson_iterator_init(it, obj->data, maxBufferSize, bson_fatal_msg);
-    while(bson_iterator_next(it, bson_fatal_msg)){
-        if (strncmp(name, bson_iterator_key(it), (maxBufferSize - it->curSize)) == 0) {
-            break;
-        }
-    }
-    return bson_iterator_type(it);
-}
-
-bson_bool_t bson_iterator_more( const bson_iterator * i ){
-    return *(i->cur);
-}
-
-bson_type bson_iterator_next( bson_iterator * i , void (*fatal_error_func)(int, const char*)){
-    int ds;
-
-    if ( i->first ){
-        i->first = 0;
-        return (bson_type)(*i->cur);
-    }
-    
-    switch ( bson_iterator_type(i) ){
-    case bson_eoo: return bson_eoo; /* don't advance */
-    case bson_undefined:
-    case bson_null: ds = 0; break;
-    case bson_bool: ds = 1; break;
-    case bson_int: ds = 4; break;
-    case bson_long:
-    case bson_double:
-    case bson_timestamp:
-    case bson_date: ds = 8; break;
-    case bson_oid: ds = 12; break;
-    case bson_string:
-    case bson_symbol:
-    case bson_code: ds = 4 + bson_iterator_int_raw(i); break;
-    case bson_bindata: ds = 5 + bson_iterator_int_raw(i); break;
-    case bson_object:
-    case bson_array:
-    case bson_codewscope: ds = bson_iterator_int_raw(i); break;
-    case bson_dbref: ds = 4+12 + bson_iterator_int_raw(i); break;
-    case bson_regex:
-        {
-            const char * s = bson_iterator_value(i);
-            const char * p = s;
-            p += strlen(p)+1;
-            p += strlen(p)+1;
-            ds = p-s;
-            break;
-        }
-
-    default: 
-        {
-            char msg[] = "unknown type: 000000000000";
-            bson_numstr(msg+14, (unsigned)(i->cur[0]));
-            fatal_error_func(0, msg);
-            return 0;
-        }
-    }
-    /* lenOfValue = BSON_type + key length (up to the maxBufferSize) + key terminating null byte + value size and metadata */
-    size_t lenOfValue = 1 + strnlen( i->cur + 1, (i->maxBufferSize - i->curSize - 1)) + 1 + ds;
-    if((lenOfValue + i->curSize) >= i->maxBufferSize) {
-        char msg[] = "Invalid BSON response";
-        fatal_error_func(0, msg);
-        return 0;
-    }
-    i->curSize += lenOfValue;
-    i->cur += lenOfValue;
-    return (bson_type)(*i->cur);
-}
-
-bson_type bson_iterator_type( const bson_iterator * i ){
-    return (bson_type)i->cur[0];
-}
-const char * bson_iterator_key( const bson_iterator * i ){
-    return i->cur + 1;
-}
-const char * bson_iterator_value( const bson_iterator * i ){
-    const char * t = i->cur + 1;
-    t += strlen( t ) + 1;
-    return t;
-}
-
-/* types */
-
-int bson_iterator_int_raw( const bson_iterator * i ){
-    int out;
-    bson_little_endian32(&out, bson_iterator_value( i ));
-    return out;
-}
-double bson_iterator_double_raw( const bson_iterator * i ){
-    double out;
-    bson_little_endian64(&out, bson_iterator_value( i ));
-    return out;
-}
-int64_t bson_iterator_long_raw( const bson_iterator * i ){
-    int64_t out;
-    bson_little_endian64(&out, bson_iterator_value( i ));
-    return out;
-}
-
-bson_bool_t bson_iterator_bool_raw( const bson_iterator * i ){
-    return bson_iterator_value( i )[0];
-}
-
-bson_oid_t * bson_iterator_oid( const bson_iterator * i ){
-    return (bson_oid_t*)bson_iterator_value(i);
-}
-
-int bson_iterator_int( const bson_iterator * i ){
-    switch (bson_iterator_type(i)){
-        case bson_int: return bson_iterator_int_raw(i);
-        case bson_long: return bson_iterator_long_raw(i);
-        case bson_double: return bson_iterator_double_raw(i);
-        default: return 0;
-    }
-}
-double bson_iterator_double( const bson_iterator * i ){
-    switch (bson_iterator_type(i)){
-        case bson_int: return bson_iterator_int_raw(i);
-        case bson_long: return bson_iterator_long_raw(i);
-        case bson_double: return bson_iterator_double_raw(i);
-        default: return 0;
-    }
-}
-int64_t bson_iterator_long( const bson_iterator * i ){
-    switch (bson_iterator_type(i)){
-        case bson_int: return bson_iterator_int_raw(i);
-        case bson_long: return bson_iterator_long_raw(i);
-        case bson_double: return bson_iterator_double_raw(i);
-        default: return 0;
-    }
-}
-
-bson_bool_t bson_iterator_bool( const bson_iterator * i ){
-    switch (bson_iterator_type(i)){
-        case bson_bool: return bson_iterator_bool_raw(i);
-        case bson_int: return bson_iterator_int_raw(i) != 0;
-        case bson_long: return bson_iterator_long_raw(i) != 0;
-        case bson_double: return bson_iterator_double_raw(i) != 0;
-        case bson_eoo:
-        case bson_null: return 0;
-        default: return 1;
-    }
-}
-
-const char * bson_iterator_string( const bson_iterator * i ){
-    return bson_iterator_value( i ) + 4;
-}
-int bson_iterator_string_len( const bson_iterator * i ){
-    return bson_iterator_int_raw( i );
-}
-
-const char * bson_iterator_code( const bson_iterator * i ){
-    switch (bson_iterator_type(i)){
-        case bson_string:
-        case bson_code: return bson_iterator_value(i) + 4;
-        case bson_codewscope: return bson_iterator_value(i) + 8;
-        default: return NULL;
-    }
-}
-
-void bson_iterator_code_scope(const bson_iterator * i, bson * scope){
-    if (bson_iterator_type(i) == bson_codewscope){
-        int code_len;
-        bson_little_endian32(&code_len, bson_iterator_value(i)+4);
-        bson_init(scope, (void*)(bson_iterator_value(i)+8+code_len), 0);
-    }else{
-        bson_empty(scope);
-    }
-}
-
-bson_date_t bson_iterator_date(const bson_iterator * i){
-    return bson_iterator_long_raw(i);
-}
-
-time_t bson_iterator_time_t(const bson_iterator * i){
-    return bson_iterator_date(i) / 1000;
-}
-
-int bson_iterator_bin_len( const bson_iterator * i ){
-    return bson_iterator_int_raw( i );
-}
-
-char bson_iterator_bin_type( const bson_iterator * i ){
-    return bson_iterator_value(i)[4];
-}
-const char * bson_iterator_bin_data( const bson_iterator * i ){
-    return bson_iterator_value( i ) + 5;
-}
-
-const char * bson_iterator_regex( const bson_iterator * i ){
-    return bson_iterator_value( i );
-}
-const char * bson_iterator_regex_opts( const bson_iterator * i ){
-    const char* p = bson_iterator_value( i );
-    return p + strlen(p) + 1;
-
-}
-
-void bson_iterator_subobject(const bson_iterator * i, bson * sub){
-    bson_init(sub, (char*)bson_iterator_value(i), 0);
-}
-void bson_iterator_subiterator(const bson_iterator * i, bson_iterator * sub, const int maxBufferSize){
-    bson_iterator_init(sub, bson_iterator_value(i), maxBufferSize, bson_fatal_msg);
-}
-
-/* ----------------------------
-   BUILDING
-   ------------------------------ */
-
-bson_buffer * bson_buffer_init( bson_buffer * b ){
-    b->buf = (char*)bson_malloc( initialBufferSize );
-    b->bufSize = initialBufferSize;
-    b->cur = b->buf + 4;
-    b->finished = 0;
-    b->stackPos = 0;
-    return b;
-}
-
-void bson_append_byte( bson_buffer * b , char c ){
-    b->cur[0] = c;
-    b->cur++;
-}
-void bson_append( bson_buffer * b , const void * data , int len ){
-    memcpy( b->cur , data , len );
-    b->cur += len;
-}
-void bson_append32(bson_buffer * b, const void * data){
-    bson_little_endian32(b->cur, data);
-    b->cur += 4;
-}
-void bson_append64(bson_buffer * b, const void * data){
-    bson_little_endian64(b->cur, data);
-    b->cur += 8;
-}
-
-bson_buffer * bson_ensure_space( bson_buffer * b , const int bytesNeeded ){
-    int pos = b->cur - b->buf;
-    char * orig = b->buf;
-    int new_size;
-
-    if (b->finished)
-        bson_fatal_msg(!!b->buf, "trying to append to finished buffer");
-
-    if (pos + bytesNeeded <= b->bufSize)
-        return b;
-
-    new_size = 1.5 * (b->bufSize + bytesNeeded);
-    b->buf = realloc(b->buf, new_size);
-    if (!b->buf)
-        bson_fatal_msg(!!b->buf, "realloc() failed");
-
-    b->bufSize = new_size;
-    b->cur += b->buf - orig;
-
-    return b;
-}
-
-char * bson_buffer_finish( bson_buffer * b ){
-    int i;
-    if ( ! b->finished ){
-        if ( ! bson_ensure_space( b , 1 ) ) return 0;
-        bson_append_byte( b , 0 );
-        i = b->cur - b->buf;
-        bson_little_endian32(b->buf, &i);
-        b->finished = 1;
-    }
-    return b->buf;
-}
-
-void bson_buffer_destroy( bson_buffer * b ){
-    free( b->buf );
-    b->buf = 0;
-    b->cur = 0;
-    b->finished = 1;
-}
-
-static bson_buffer * bson_append_estart( bson_buffer * b , int type , const char * name , const int dataSize ){
-    const int sl = strlen(name) + 1;
-    if ( ! bson_ensure_space( b , 1 + sl + dataSize ) )
-        return 0;
-    bson_append_byte( b , (char)type );
-    bson_append( b , name , sl );
-    return b;
-}
-
-/* ----------------------------
-   BUILDING TYPES
-   ------------------------------ */
-
-bson_buffer * bson_append_int( bson_buffer * b , const char * name , const int i ){
-    if ( ! bson_append_estart( b , bson_int , name , 4 ) ) return 0;
-    bson_append32( b , &i );
-    return b;
-}
-bson_buffer * bson_append_long( bson_buffer * b , const char * name , const int64_t i ){
-    if ( ! bson_append_estart( b , bson_long , name , 8 ) ) return 0;
-    bson_append64( b , &i );
-    return b;
-}
-bson_buffer * bson_append_double( bson_buffer * b , const char * name , const double d ){
-    if ( ! bson_append_estart( b , bson_double , name , 8 ) ) return 0;
-    bson_append64( b , &d );
-    return b;
-}
-bson_buffer * bson_append_bool( bson_buffer * b , const char * name , const bson_bool_t i ){
-    if ( ! bson_append_estart( b , bson_bool , name , 1 ) ) return 0;
-    bson_append_byte( b , i != 0 );
-    return b;
-}
-bson_buffer * bson_append_null( bson_buffer * b , const char * name ){
-    if ( ! bson_append_estart( b , bson_null , name , 0 ) ) return 0;
-    return b;
-}
-bson_buffer * bson_append_undefined( bson_buffer * b , const char * name ){
-    if ( ! bson_append_estart( b , bson_undefined , name , 0 ) ) return 0;
-    return b;
-}
-bson_buffer * bson_append_string_base( bson_buffer * b , const char * name , const char * value , bson_type type){
-    int sl = strlen( value ) + 1;
-    if ( ! bson_append_estart( b , type , name , 4 + sl ) ) return 0;
-    bson_append32( b , &sl);
-    bson_append( b , value , sl );
-    return b;
-}
-bson_buffer * bson_append_string( bson_buffer * b , const char * name , const char * value ){
-    return bson_append_string_base(b, name, value, bson_string);
-}
-bson_buffer * bson_append_symbol( bson_buffer * b , const char * name , const char * value ){
-    return bson_append_string_base(b, name, value, bson_symbol);
-}
-bson_buffer * bson_append_code( bson_buffer * b , const char * name , const char * value ){
-    return bson_append_string_base(b, name, value, bson_code);
-}
-
-bson_buffer * bson_append_code_w_scope( bson_buffer * b , const char * name , const char * code , const bson * scope){
-    int sl = strlen(code) + 1;
-    int size = 4 + 4 + sl + bson_size(scope);
-    if (!bson_append_estart(b, bson_codewscope, name, size)) return 0;
-    bson_append32(b, &size);
-    bson_append32(b, &sl);
-    bson_append(b, code, sl);
-    bson_append(b, scope->data, bson_size(scope));
-    return b;
-}
-
-bson_buffer * bson_append_binary( bson_buffer * b, const char * name, char type, const char * str, int len ){
-    if ( ! bson_append_estart( b , bson_bindata , name , 4+1+len ) ) return 0;
-    bson_append32(b, &len);
-    bson_append_byte(b, type);
-    bson_append(b, str, len);
-    return b;
-}
-bson_buffer * bson_append_oid( bson_buffer * b , const char * name , const bson_oid_t * oid ){
-    if ( ! bson_append_estart( b , bson_oid , name , 12 ) ) return 0;
-    bson_append( b , oid , 12 );
-    return b;
-}
-bson_buffer * bson_append_new_oid( bson_buffer * b , const char * name ){
-    bson_oid_t oid;
-    bson_oid_gen(&oid);
-    return bson_append_oid(b, name, &oid);
-}
-
-bson_buffer * bson_append_regex( bson_buffer * b , const char * name , const char * pattern, const char * opts ){
-    const int plen = strlen(pattern)+1;
-    const int olen = strlen(opts)+1;
-    if ( ! bson_append_estart( b , bson_regex , name , plen + olen ) ) return 0;
-    bson_append( b , pattern , plen );
-    bson_append( b , opts , olen );
-    return b;
-}
-
-bson_buffer * bson_append_bson( bson_buffer * b , const char * name , const bson* bson){
-    if ( ! bson_append_estart( b , bson_object , name , bson_size(bson) ) ) return 0;
-    bson_append( b , bson->data , bson_size(bson) );
-    return b;
-}
-
-bson_buffer * bson_append_element( bson_buffer * b, const char * name_or_null, const bson_iterator* elem){
-    bson_iterator next = *elem;
-    int size;
-
-    bson_iterator_next(&next, bson_fatal_msg);
-    size = next.cur - elem->cur;
-
-    if (name_or_null == NULL){
-        bson_ensure_space(b, size);
-        bson_append(b, elem->cur, size);
-    }else{
-        int data_size = size - 1 - strlen(bson_iterator_key(elem));
-        bson_append_estart(b, elem->cur[0], name_or_null, data_size);
-        bson_append(b, name_or_null, strlen(name_or_null));
-        bson_append(b, bson_iterator_value(elem), data_size);
-    }
-
-    return b;
-}
-
-bson_buffer * bson_append_date( bson_buffer * b , const char * name , bson_date_t millis ){
-    if ( ! bson_append_estart( b , bson_date , name , 8 ) ) return 0;
-    bson_append64( b , &millis );
-    return b;
-}
-
-bson_buffer * bson_append_time_t( bson_buffer * b , const char * name , time_t secs){
-    return bson_append_date(b, name, (bson_date_t)secs * 1000);
-}
-
-bson_buffer * bson_append_start_object( bson_buffer * b , const char * name ){
-    if ( ! bson_append_estart( b , bson_object , name , 5 ) ) return 0;
-    b->stack[ b->stackPos++ ] = b->cur - b->buf;
-    bson_append32( b , &zero );
-    return b;
-}
-
-bson_buffer * bson_append_start_array( bson_buffer * b , const char * name ){
-    if ( ! bson_append_estart( b , bson_array , name , 5 ) ) return 0;
-    b->stack[ b->stackPos++ ] = b->cur - b->buf;
-    bson_append32( b , &zero );
-    return b;
-}
-
-bson_buffer * bson_append_finish_object( bson_buffer * b ){
-    char * start;
-    int i;
-    if ( ! bson_ensure_space( b , 1 ) ) return 0;
-    bson_append_byte( b , 0 );
-    
-    start = b->buf + b->stack[ --b->stackPos ];
-    i = b->cur - start;
-    bson_little_endian32(start, &i);
-
-    return b;
-}
-
-void* bson_malloc(int size){
-    void* p = malloc(size);
-    bson_fatal_msg(!!p, "malloc() failed");
-    return p;
-}
-
-static bson_err_handler err_handler = NULL;
-
-bson_err_handler set_bson_err_handler(bson_err_handler func){
-    bson_err_handler old = err_handler;
-    err_handler = func;
-    return old;
-}
-
-void bson_fatal( int ok ){
-    bson_fatal_msg(ok, "");
-}
-
-void bson_fatal_msg( int ok , const char* msg){
-    if (ok)
-        return;
-
-    if (err_handler){
-        err_handler(msg);
-    }
-
-    fprintf( stderr , "error: %s\n" , msg );
-    exit(-5);
-}
-
-void bson_numstr(char* str, int i){
-    sprintf(str,"%d", i);
-}
diff --git a/lib/bson.h b/lib/bson.h
deleted file mode 100644
index 326f3f3..0000000
--- a/lib/bson.h
+++ /dev/null
@@ -1,227 +0,0 @@
-/* bson.h */
-
-/*    Copyright 2009, 2010 10gen Inc.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- */
-
-#ifndef _BSON_H_
-#define _BSON_H_
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-#if defined(HAVE_INTTYPES_H)
-#include <inttypes.h>
-#elif defined(HAVE_STDINT_H)
-#include <stdint.h>
-#elif defined(HAVE_UNISTD_H)
-#include <unistd.h>
-#endif
-
-#include <time.h>
-
-typedef enum {
-    bson_eoo=0 ,
-    bson_double=1,
-    bson_string=2,
-    bson_object=3,
-    bson_array=4,
-    bson_bindata=5,
-    bson_undefined=6,
-    bson_oid=7,
-    bson_bool=8,
-    bson_date=9,
-    bson_null=10,
-    bson_regex=11,
-    bson_dbref=12, /* deprecated */
-    bson_code=13,
-    bson_symbol=14,
-    bson_codewscope=15,
-    bson_int = 16,
-    bson_timestamp = 17,
-    bson_long = 18
-} bson_type;
-
-typedef int bson_bool_t;
-
-typedef struct {
-    char * data;
-    bson_bool_t owned;
-} bson;
-
-typedef struct {
-    const char * cur;
-    bson_bool_t first;
-    int curSize;
-    int maxBufferSize;
-} bson_iterator;
-
-typedef struct {
-    char * buf;
-    char * cur;
-    int bufSize;
-    bson_bool_t finished;
-    int stack[32];
-    int stackPos;
-} bson_buffer;
-
-#pragma pack(1)
-typedef union{
-    char bytes[12];
-    int ints[3];
-} bson_oid_t;
-#pragma pack()
-
-typedef int64_t bson_date_t; /* milliseconds since epoch UTC */
-
-/* ----------------------------
-   READING
-   ------------------------------ */
-
-
-bson * bson_empty(bson * obj); /* returns pointer to static empty bson object */
-void bson_copy(bson* out, const bson* in); /* puts data in new buffer. NOOP if out==NULL */
-bson * bson_from_buffer(bson * b, bson_buffer * buf);
-bson * bson_init( bson * b , char * data , bson_bool_t mine );
-int bson_size(const bson * b );
-void bson_destroy( bson * b );
-
-void bson_print( bson * b, const size_t maxBufferSize );
-void bson_print_raw( const char * bson , int depth, const size_t maxBufferSize );
-
-/* advances iterator to named field */
-/* returns bson_eoo (which is false) if field not found */
-bson_type bson_find(bson_iterator* it, const bson* obj, const char* name, const size_t maxBufferSize);
-
-void bson_iterator_init( bson_iterator * i , const char * bson , const int bson_size , void (*fatal_func)(int, const char*));
-
-/* more returns true for eoo. best to loop with bson_iterator_next(&it) */
-bson_bool_t bson_iterator_more( const bson_iterator * i );
-bson_type bson_iterator_next( bson_iterator * i , void (*fatal_func)(int, const char*));
-
-bson_type bson_iterator_type( const bson_iterator * i );
-const char * bson_iterator_key( const bson_iterator * i );
-const char * bson_iterator_value( const bson_iterator * i );
-
-/* these convert to the right type (return 0 if non-numeric) */
-double bson_iterator_double( const bson_iterator * i );
-int bson_iterator_int( const bson_iterator * i );
-int64_t bson_iterator_long( const bson_iterator * i );
-
-/* false: boolean false, 0 in any type, or null */
-/* true: anything else (even empty strings and objects) */
-bson_bool_t bson_iterator_bool( const bson_iterator * i );
-
-/* these assume you are using the right type */
-double bson_iterator_double_raw( const bson_iterator * i );
-int bson_iterator_int_raw( const bson_iterator * i );
-int64_t bson_iterator_long_raw( const bson_iterator * i );
-bson_bool_t bson_iterator_bool_raw( const bson_iterator * i );
-bson_oid_t* bson_iterator_oid( const bson_iterator * i );
-
-/* these can also be used with bson_code and bson_symbol*/
-const char * bson_iterator_string( const bson_iterator * i );
-int bson_iterator_string_len( const bson_iterator * i );
-
-/* works with bson_code, bson_codewscope, and bson_string */
-/* returns NULL for everything else */
-const char * bson_iterator_code(const bson_iterator * i);
-
-/* calls bson_empty on scope if not a bson_codewscope */
-void bson_iterator_code_scope(const bson_iterator * i, bson * scope);
-
-/* both of these only work with bson_date */
-bson_date_t bson_iterator_date(const bson_iterator * i);
-time_t bson_iterator_time_t(const bson_iterator * i);
-
-int bson_iterator_bin_len( const bson_iterator * i );
-char bson_iterator_bin_type( const bson_iterator * i );
-const char * bson_iterator_bin_data( const bson_iterator * i );
-
-const char * bson_iterator_regex( const bson_iterator * i );
-const char * bson_iterator_regex_opts( const bson_iterator * i );
-
-/* these work with bson_object and bson_array */
-void bson_iterator_subobject(const bson_iterator * i, bson * sub);
-void bson_iterator_subiterator(const bson_iterator * i, bson_iterator * sub, const int maxBufferSize);
-
-/* str must be at least 24 hex chars + null byte */
-void bson_oid_from_string(bson_oid_t* oid, const char* str);
-void bson_oid_to_string(const bson_oid_t* oid, char* str);
-void bson_oid_gen(bson_oid_t* oid);
-
-time_t bson_oid_generated_time(bson_oid_t* oid); /* Gives the time the OID was created */
-
-/* ----------------------------
-   BUILDING
-   ------------------------------ */
-
-bson_buffer * bson_buffer_init( bson_buffer * b );
-bson_buffer * bson_ensure_space( bson_buffer * b , const int bytesNeeded );
-
-/**
- * @return the raw data.  you either should free this OR call bson_destroy not both
- */
-char * bson_buffer_finish( bson_buffer * b );
-void bson_buffer_destroy( bson_buffer * b );
-
-bson_buffer * bson_append_oid( bson_buffer * b , const char * name , const bson_oid_t* oid );
-bson_buffer * bson_append_new_oid( bson_buffer * b , const char * name );
-bson_buffer * bson_append_int( bson_buffer * b , const char * name , const int i );
-bson_buffer * bson_append_long( bson_buffer * b , const char * name , const int64_t i );
-bson_buffer * bson_append_double( bson_buffer * b , const char * name , const double d );
-bson_buffer * bson_append_string( bson_buffer * b , const char * name , const char * str );
-bson_buffer * bson_append_symbol( bson_buffer * b , const char * name , const char * str );
-bson_buffer * bson_append_code( bson_buffer * b , const char * name , const char * str );
-bson_buffer * bson_append_code_w_scope( bson_buffer * b , const char * name , const char * code , const bson * scope);
-bson_buffer * bson_append_binary( bson_buffer * b, const char * name, char type, const char * str, int len );
-bson_buffer * bson_append_bool( bson_buffer * b , const char * name , const bson_bool_t v );
-bson_buffer * bson_append_null( bson_buffer * b , const char * name );
-bson_buffer * bson_append_undefined( bson_buffer * b , const char * name );
-bson_buffer * bson_append_regex( bson_buffer * b , const char * name , const char * pattern, const char * opts );
-bson_buffer * bson_append_bson( bson_buffer * b , const char * name , const bson* bson);
-bson_buffer * bson_append_element( bson_buffer * b, const char * name_or_null, const bson_iterator* elem);
-
-/* these both append a bson_date */
-bson_buffer * bson_append_date(bson_buffer * b, const char * name, bson_date_t millis);
-bson_buffer * bson_append_time_t(bson_buffer * b, const char * name, time_t secs);
-
-bson_buffer * bson_append_start_object( bson_buffer * b , const char * name );
-bson_buffer * bson_append_start_array( bson_buffer * b , const char * name );
-bson_buffer * bson_append_finish_object( bson_buffer * b );
-
-void bson_numstr(char* str, int i);
-void bson_incnumstr(char* str);
-
-
-/* ------------------------------
-   ERROR HANDLING - also used in mongo code
-   ------------------------------ */
-
-void * bson_malloc(int size); /* checks return value */
-
-/* bson_err_handlers shouldn't return!!! */
-typedef void(*bson_err_handler)(const char* errmsg);
-
-/* returns old handler or NULL */
-/* default handler prints error then exits with failure*/
-bson_err_handler set_bson_err_handler(bson_err_handler func);
-
-
-
-/* does nothing is ok != 0 */
-void bson_fatal( int ok );
-void bson_fatal_msg( int ok, const char* msg );
-
-#endif
diff --git a/lib/cacert.h b/lib/cacert.h
index 2aaf7dc..1f32a9a 100644
--- a/lib/cacert.h
+++ b/lib/cacert.h
@@ -149,7 +149,146 @@
 "f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n"\
 "-----END CERTIFICATE-----\n"
 
+/* 
+ * subject=C = US, O = Amazon, CN = Amazon Root CA 1
+ */
+
+#define AMAZON_CA1_PEM \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n"\
+"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n"\
+"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n"\
+"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJV\n"\
+"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXJ\n"\
+"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n"\
+"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qW\n"\
+"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n"\
+"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n"\
+"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQM\n"\
+"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n"\
+"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n"\
+"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n"\
+"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUS\n"\
+"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vV\n"\
+"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n"\
+"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpY\n"\
+"rqXRfboQnoZsG4q5WTP468SQvvG5\n"\
+"-----END CERTIFICATE-----\n"
+
+/*
+ * subject=C = US, O = Amazon, CN = Amazon Root CA 2
+ */
+
+#define AMAZON_CA2_PEM \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF\n"\
+"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n"\
+"b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL\n"\
+"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n"\
+"b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK\n"\
+"gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ\n"\
+"W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg\n"\
+"1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K\n"\
+"8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r\n"\
+"2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me\n"\
+"z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR\n"\
+"8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj\n"\
+"mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz\n"\
+"7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6\n"\
+"+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI\n"\
+"0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB\n"\
+"Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm\n"\
+"UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2\n"\
+"LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY\n"\
+"+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS\n"\
+"k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl\n"\
+"7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm\n"\
+"btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl\n"\
+"urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+\n"\
+"fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63\n"\
+"n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE\n"\
+"76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H\n"\
+"9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT\n"\
+"4PsJYGw=\n"\
+"-----END CERTIFICATE-----\n"
+
+/* 
+ * subject=C = US, O = Amazon, CN = Amazon Root CA 3
+ */
+
+#define AMAZON_CA3_PEM \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5\n"\
+"MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\n"\
+"Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\n"\
+"A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\n"\
+"Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl\n"\
+"ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j\n"\
+"QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr\n"\
+"ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr\n"\
+"BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM\n"\
+"YyRIHN8wfdVoOw==\n"\
+"-----END CERTIFICATE-----\n"
+
+/*
+ * subject=C = US, O = Amazon, CN = Amazon Root CA 4
+ */
+
+#define AMAZON_CA4_PEM \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5\n"\
+"MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\n"\
+"Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\n"\
+"A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\n"\
+"Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi\n"\
+"9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk\n"\
+"M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB\n"\
+"/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB\n"\
+"MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw\n"\
+"CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW\n"\
+"1KyLa2tJElMzrdfkviT8tQp21KW8EA==\n"\
+"-----END CERTIFICATE-----\n"
+
+/*
+ * subject=C = BM, O = QuoVadis Limited, CN = QuoVadis Root CA 2
+ */
+
+#define QUOVADIS_CA2_PEM \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\n"\
+"GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\n"\
+"b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV\n"\
+"BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\n"\
+"YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa\n"\
+"GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg\n"\
+"Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J\n"\
+"WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB\n"\
+"rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n"\
+"+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1\n"\
+"ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i\n"\
+"Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz\n"\
+"PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og\n"\
+"/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH\n"\
+"oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI\n"\
+"yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud\n"\
+"EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2\n"\
+"A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\n"\
+"MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT\n"\
+"ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f\n"\
+"BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn\n"\
+"g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl\n"\
+"fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K\n"\
+"WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha\n"\
+"B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc\n"\
+"hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR\n"\
+"TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\n"\
+"mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z\n"\
+"ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y\n"\
+"4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza\n"\
+"8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u\n"\
+"-----END CERTIFICATE-----\n"
+
 #define CACERT_PEM \
-    DIGICERT1_PEM DIGICERT2_PEM DIGICERT3_PEM TW1_PEM TW2_PEM
+    DIGICERT1_PEM DIGICERT2_PEM DIGICERT3_PEM TW1_PEM TW2_PEM AMAZON_CA1_PEM AMAZON_CA2_PEM AMAZON_CA3_PEM AMAZON_CA4_PEM QUOVADIS_CA2_PEM
 
 #endif /* CACERT_H */
diff --git a/lib/duo.c b/lib/duo.c
index 3b45c8c..18db8d4 100644
--- a/lib/duo.c
+++ b/lib/duo.c
@@ -31,8 +31,8 @@
 #include <openssl/ssl.h>
 
 #include "util.h"
-#include "bson.h"
 #include "duo.h"
+#include "parson.h"
 #include "duo_private.h"
 #include "ini.h"
 #include "urlenc.h"
@@ -153,16 +153,17 @@
         duo_reset(ctx);
         free(ctx->host);
 
+        // We need to add 1 here for the terminating \0 byte which strlen doesn't include
         if (ctx->ikey != NULL) {
-            duo_zero_free(ctx->ikey, strlen(ctx->ikey));
+            duo_zero_free(ctx->ikey, strlen(ctx->ikey) + 1);
             ctx->ikey = NULL;
         }
         if (ctx->skey != NULL) {
-            duo_zero_free(ctx->skey, strlen(ctx->skey));
+            duo_zero_free(ctx->skey, strlen(ctx->skey) + 1);
             ctx->skey = NULL;
         }
         if (ctx->useragent != NULL) {
-            duo_zero_free(ctx->useragent, strlen(ctx->useragent));
+            duo_zero_free(ctx->useragent, strlen(ctx->useragent) + 1);
             ctx->useragent = NULL;
         }
 
@@ -282,46 +283,62 @@
     return duo_add_optional_param(ctx, "failmode", failmode_str);
 }
 
-#define _BSON_FIND(ctx, it, obj, name, type) do {           \
-    if (bson_find(it, obj, name, ctx->body_len) != type) {             \
-        _duo_seterr(ctx, "BSON missing valid '%s'", name);  \
-        return (DUO_SERVER_ERROR);              \
-    }                               \
-} while (0)
+#define _JSON_FIND_OBJECT(out_obj, in_obj, name, json_value) do { \
+  out_obj = json_object_get_object(in_obj, name); \
+  if (out_obj == NULL) { \
+        _duo_seterr(ctx, "JSON missing valid '%s'", name);  \
+        _JSON_VALUE_FREE(json_value); \
+        return (DUO_SERVER_ERROR); \
+  } \
+} while(0)
+
+#define _JSON_FIND_STRING(buf, json_obj, name, json_value) do { \
+  buf = json_object_get_string(json_obj, name); \
+  if (buf == NULL) { \
+        _duo_seterr(ctx, "JSON missing valid '%s'", name);  \
+        _JSON_VALUE_FREE(json_value); \
+        return (DUO_SERVER_ERROR); \
+  } \
+} while(0)
+
+# define _JSON_VALUE_FREE(value) do { \
+  json_value_free(value); \
+  value = NULL; \
+} while(0)
 
 static duo_code_t
-_duo_bson_response(struct duo_ctx *ctx, bson *resp)
-{
-    bson obj;
-    bson_iterator it;
-    duo_code_t ret;
-    const char *p;
-    int code;
+_duo_json_response(struct duo_ctx *ctx) {
+    JSON_Value *json;
+    JSON_Object *json_obj;
+    char *p;
+    int code = DUO_SERVER_ERROR;
 
-    bson_init(&obj, (char *)ctx->body, 0);
-
-    ret = DUO_SERVER_ERROR;
-    if (ctx->body_len <= 0 || bson_size(&obj) > ctx->body_len) {
-        _duo_seterr(ctx, "invalid BSON response");
-        return (ret);
+    json = json_parse_string(ctx->body);
+    if(json == NULL) {
+        _duo_seterr(ctx, "invalid JSON response");
+        return (DUO_SERVER_ERROR);
     }
-    _BSON_FIND(ctx, &it, &obj, "stat", bson_string);
-    p = bson_iterator_string(&it);
+    json_obj = json_value_get_object(json);
 
+    _JSON_FIND_STRING(p, json_obj, "stat", json);
     if (strcasecmp(p, "OK") == 0) {
-        _BSON_FIND(ctx, &it, &obj, "response", bson_object);
-        if (resp) {
-            bson_iterator_subobject(&it, resp);
-        }
-        ret = DUO_OK;
-    } else if (strcasecmp(p, "FAIL") == 0) {
-        _BSON_FIND(ctx, &it, &obj, "code", bson_int);
-        code = bson_iterator_int(&it);
-        _BSON_FIND(ctx, &it, &obj, "message", bson_string);
-        _duo_seterr(ctx, "%d: %s", code, bson_iterator_string(&it));
-        ret = DUO_FAIL;
+        code = DUO_OK;
     }
-    return (ret);
+    if (strcasecmp(p, "FAIL") == 0) {
+        char *message;
+        code = json_object_get_number(json_obj, "code");
+        // json_object_get_number will return 0 if "code" not found
+        if (code == 0) {
+               _duo_seterr(ctx, "JSON missing valid 'code'");
+               _JSON_VALUE_FREE(json);
+               return (DUO_SERVER_ERROR);
+        }
+        _JSON_FIND_STRING(message, json_obj, "message", json);
+        _duo_seterr(ctx, "%d: %s", code, message);
+        code = DUO_FAIL;
+    }
+    _JSON_VALUE_FREE(json);
+    return code;
 }
 
 static duo_code_t
@@ -382,12 +399,13 @@
 }
 
 duo_code_t
-_duo_preauth(struct duo_ctx *ctx, bson *obj, const char *username,
+_duo_preauth(struct duo_ctx *ctx, const char *username,
     const char *client_ip, const int failmode)
 {
-    bson_iterator it;
     duo_code_t ret;
     const char *p;
+    JSON_Value *json;
+    JSON_Object *json_obj;
 
     /* Check preauth result */
     if (duo_add_param(ctx, "user", username) != DUO_OK) {
@@ -406,111 +424,139 @@
         return (DUO_LIB_ERROR);
     }
 
-    if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/preauth.bson", ctx->https_timeout)) != DUO_OK ||
-        (ret = _duo_bson_response(ctx, obj)) != DUO_OK) {
+    if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/preauth.json", ctx->https_timeout)) != DUO_OK ||
+         (ret = _duo_json_response(ctx)) != DUO_OK)
+    {
         return (ret);
     }
-    _BSON_FIND(ctx, &it, obj, "result", bson_string);
-    p = bson_iterator_string(&it);
 
-    if (strcasecmp(p, "auth") != 0) {
-        _BSON_FIND(ctx, &it, obj, "status", bson_string);
+    json = json_parse_string(ctx->body);
+    json_obj = json_value_get_object(json);
+    JSON_Object *response;
+    _JSON_FIND_OBJECT(response, json_obj, "response", json);
+    _JSON_FIND_STRING(p, response, "result", json);
+    if (p == NULL) {
+        _duo_seterr(ctx, "JSON invalid 'result': %s", p);
+        ret = DUO_SERVER_ERROR;
+    } else if (strcasecmp(p, "auth") != 0) {
+        char *output;
+        _JSON_FIND_STRING(output, response, "status", json);
         if (strcasecmp(p, "allow") == 0) {
-                        _duo_seterr(ctx, "%s", bson_iterator_string(&it));
+                        _duo_seterr(ctx, "%s", output);
             ret = DUO_OK;
         } else if (strcasecmp(p, "deny") == 0) {
-            _duo_seterr(ctx, "%s", bson_iterator_string(&it));
+            _duo_seterr(ctx, "%s", output);
             if (ctx->conv_status != NULL) {
-                ctx->conv_status(ctx->conv_arg,
-                    bson_iterator_string(&it));
+                ctx->conv_status(ctx->conv_arg, output);
             }
             ret = DUO_ABORT;
         } else if (strcasecmp(p, "enroll") == 0) {
             if (ctx->conv_status != NULL) {
-                ctx->conv_status(ctx->conv_arg,
-                    bson_iterator_string(&it));
+                ctx->conv_status(ctx->conv_arg, output);
             }
             _duo_seterr(ctx, "User enrollment required");
             ret = DUO_ABORT;
         } else {
-            _duo_seterr(ctx, "BSON invalid 'result': %s", p);
+            _duo_seterr(ctx, "JSON invalid 'result': %s", p);
             ret = DUO_SERVER_ERROR;
         }
-        return (ret);
+    } else {
+        ret = DUO_CONTINUE;
     }
-    return (DUO_CONTINUE);
+    _JSON_VALUE_FREE(json);
+    return (ret);
 }
 
 duo_code_t
-_duo_prompt(struct duo_ctx *ctx, bson *obj, int flags, char *buf,
-    size_t sz, const char **p)
+_duo_prompt(struct duo_ctx *ctx, int flags, char *buf,
+    size_t sz,  char *p, size_t sp)
 {
-    bson_iterator it;
     char *pos, *passcode;
 
     passcode = getenv(DUO_ENV_VAR_NAME);
 
     if ((flags & DUO_FLAG_ENV) && (passcode != NULL)) {
-        *p = passcode;
+        if (strlcpy(p, passcode, sp) >= sp) {
+            return (DUO_LIB_ERROR);
+        }
         if (ctx->conv_status != NULL) {
             ctx->conv_status(ctx->conv_arg, ENV_VAR_MSG);
         }
+        return (DUO_CONTINUE);
     } else if ((flags & DUO_FLAG_AUTO) != 0) {
         /* Find default OOB factor for automatic login */
-        _BSON_FIND(ctx, &it, obj, "factors", bson_object);
-        bson_iterator_subobject(&it, obj);
+        JSON_Value *json = json_parse_string(ctx->body);
+        JSON_Object *json_obj = json_value_get_object(json);
+        JSON_Object *response;
+        JSON_Object *factors;
+        _JSON_FIND_OBJECT(response, json_obj, "response", json);
+        _JSON_FIND_OBJECT(factors, response, "factors", json);
 
-        if (bson_find(&it, obj, "default", ctx->body_len) != bson_string) {
-            _duo_seterr(ctx, "No default factor found for automatic login");
-            return (DUO_ABORT);
-        }
-        *p = bson_iterator_string(&it);
+        const char* default_factor;
+        _JSON_FIND_STRING(default_factor, factors, "default", json);
         if (ctx->conv_status) {
-            if ((pos = strstr(*p, "push"))) {
+            if ((pos = strstr(default_factor, "push"))) {
                 ctx->conv_status(ctx->conv_arg, AUTOPUSH_MSG);
-            } else if ((pos = strstr(*p, "phone"))) {
+            } else if ((pos = strstr(default_factor, "phone"))) {
                 ctx->conv_status(ctx->conv_arg, AUTOPHONE_MSG);
             } else {
                 ctx->conv_status(ctx->conv_arg, AUTODEFAULT_MSG);
             }
         }
+        if (strlcpy(p, default_factor, sp) >= sp) {
+            _JSON_VALUE_FREE(json);
+            return (DUO_LIB_ERROR);
+        } else {
+            _JSON_VALUE_FREE(json);
+            return (DUO_CONTINUE);
+        }
     } else {
         /* Prompt user for factor choice / token */
         if (ctx->conv_prompt == NULL) {
             _duo_seterr(ctx, "No prompt function set");
             return (DUO_CLIENT_ERROR);
         }
-        _BSON_FIND(ctx, &it, obj, "prompt", bson_string);
-        *p = bson_iterator_string(&it);
+        JSON_Value *json = json_parse_string(ctx->body);
+        JSON_Object *json_obj = json_value_get_object(json);
+        JSON_Object *response;
+        _JSON_FIND_OBJECT(response, json_obj, "response", json);
 
-        if (ctx->conv_prompt(ctx->conv_arg, *p, buf, sz) == NULL) {
+        const char* prompt;
+        _JSON_FIND_STRING(prompt, response, "prompt", json);
+
+        if (ctx->conv_prompt(ctx->conv_arg, prompt, buf, sz) == NULL) {
             _duo_seterr(ctx, "Error gathering user response");
+            _JSON_VALUE_FREE(json);
             return (DUO_ABORT);
         }
         strtok(buf, "\r\n");
 
-        _BSON_FIND(ctx, &it, obj, "factors", bson_object);
-        bson_iterator_subobject(&it, obj);
+        JSON_Object *factors;
+        _JSON_FIND_OBJECT(factors, response, "factors", json);
 
-        if (bson_find(&it, obj, buf, ctx->body_len) == bson_string) {
-            *p = bson_iterator_string(&it);
-        } else {
-            *p = buf;
+        // buf might not exist in factors JSON_Object, like if the user input
+        // a passcode
+        const char *factor_str = json_object_get_string(factors, buf);
+        if (factor_str == NULL) {
+            factor_str = buf;
         }
+        if (strlcpy(p, factor_str, sp) >= sp) {
+            _JSON_VALUE_FREE(json);
+            return (DUO_LIB_ERROR);
+        }
+        _JSON_VALUE_FREE(json);
+        return (DUO_CONTINUE);
     }
-    return (DUO_CONTINUE);
 }
 
 duo_code_t
 duo_login(struct duo_ctx *ctx, const char *username,
     const char *client_ip, int flags, const char *command, const int failmode)
 {
-    bson obj;
-    bson_iterator it;
     duo_code_t ret;
     char buf[256];
     char *pushinfo = NULL;
-    const char *p;
+    char p[256];
     int i;
     const char *local_ip;
 
@@ -520,7 +566,7 @@
     }
 
     /* Check preauth status */
-    if ((ret = _duo_preauth(ctx, &obj, username, client_ip, failmode)) != DUO_CONTINUE) {
+    if ((ret = _duo_preauth(ctx, username, client_ip, failmode)) != DUO_CONTINUE) {
         if(ret == DUO_SERVER_ERROR || ret == DUO_CONN_ERROR || ret == DUO_CLIENT_ERROR) {
             return (failmode == DUO_FAIL_SAFE) ? (DUO_FAIL_SAFE_ALLOW) : (DUO_FAIL_SECURE_DENY);
         }
@@ -528,7 +574,7 @@
     }
 
     /* Handle factor selection */
-    if ((ret = _duo_prompt(ctx, &obj, flags, buf, sizeof(buf), &p)) != DUO_CONTINUE) {
+    if ((ret = _duo_prompt(ctx, flags, buf, sizeof(buf), p, sizeof(p))) != DUO_CONTINUE) {
         return (ret);
     }
 
@@ -563,36 +609,48 @@
      * the call is asynchronous, because async calls should return
      * immediately.
      */
-    if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/auth.bson",
+    if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/auth.json",
                    flags & DUO_FLAG_SYNC ? DUO_NO_TIMEOUT : ctx->https_timeout)) != DUO_OK ||
-        (ret = _duo_bson_response(ctx, &obj)) != DUO_OK) {
+         (ret = _duo_json_response(ctx)) != DUO_OK) {
         return (ret);
     }
 
     /* Handle sync status */
     if ((flags & DUO_FLAG_SYNC) != 0) {
-        _BSON_FIND(ctx, &it, &obj, "status", bson_string);
+        JSON_Value *json = json_parse_string(ctx->body);
+        JSON_Object *json_obj = json_value_get_object(json);
+        JSON_Object *json_response;
+        _JSON_FIND_OBJECT(json_response, json_obj, "response", json);
+        const char *status;
+        _JSON_FIND_STRING(status, json_response, "status", json);
         if (ctx->conv_status != NULL) {
             ctx->conv_status(ctx->conv_arg,
-                bson_iterator_string(&it));
+            status);
         }
-        _BSON_FIND(ctx, &it, &obj, "result", bson_string);
-        p = bson_iterator_string(&it);
+        const char* result;
+        _JSON_FIND_STRING(result, json_response, "result", json);
 
-        if (strcasecmp(p, "allow") == 0) {
+        if (strcasecmp(result, "allow") == 0) {
             ret = DUO_OK;
-        } else if (strcasecmp(p, "deny") == 0) {
+        } else if (strcasecmp(result, "deny") == 0) {
             ret = DUO_FAIL;
         } else {
-            _duo_seterr(ctx, "BSON invalid 'result': %s", p);
+            _duo_seterr(ctx, "JSON invalid 'result': %s", result);
             ret = DUO_SERVER_ERROR;
         }
+        _JSON_VALUE_FREE(json);
         return (ret);
     }
     /* Async status - long-poll on txid */
-    _BSON_FIND(ctx, &it, &obj, "txid", bson_string);
-    p = bson_iterator_string(&it);
-    if (strlcpy(buf, p, sizeof(buf)) >= sizeof(buf)) {
+    JSON_Value *json = json_parse_string(ctx->body);
+    JSON_Object *json_obj = json_value_get_object(json);
+    JSON_Object *json_response;
+    _JSON_FIND_OBJECT(json_response, json_obj, "response", json);
+
+    const char* txid;
+    _JSON_FIND_STRING(txid, json_response, "txid", json);
+    if (strlcpy(buf, txid, sizeof(buf)) >= sizeof(buf)) {
+        _JSON_VALUE_FREE(json);
         return (DUO_LIB_ERROR);
     }
     /* XXX newline between prompt and async status lines */
@@ -604,30 +662,39 @@
     for (i = 0; i < 20; i++) {
         if ((ret = duo_add_param(ctx, "txid", buf)) != DUO_OK ||
             (ret = duo_call(ctx, "GET",
-            DUO_API_VERSION "/status.bson", DUO_NO_TIMEOUT)) != DUO_OK ||
-            (ret = _duo_bson_response(ctx, &obj)) != DUO_OK) {
+            DUO_API_VERSION "/status.json", DUO_NO_TIMEOUT)) != DUO_OK ||
+            (ret = _duo_json_response(ctx)) != DUO_OK) {
             break;
         }
-        if (bson_find(&it, &obj, "status", ctx->body_len) == bson_string) {
+
+        JSON_Value *json_new = json_parse_string(ctx->body);
+        JSON_Object *json_obj_new = json_value_get_object(json_new);
+        JSON_Object *json_response_new;
+        _JSON_FIND_OBJECT(json_response_new, json_obj_new, "response", json);
+        const char *status_json_obj;
+        _JSON_FIND_STRING(status_json_obj, json_response_new, "status", json);
+        if (status_json_obj != NULL) {
             if (ctx->conv_status != NULL) {
-                ctx->conv_status(ctx->conv_arg,
-                    bson_iterator_string(&it));
+                ctx->conv_status(ctx->conv_arg, status_json_obj);
             }
         }
-        if (bson_find(&it, &obj, "result", ctx->body_len) == bson_string) {
-            p = bson_iterator_string(&it);
 
-            if (strcasecmp(p, "allow") == 0) {
+        //We might not have 'result' defined but we don't want to quit the program
+        //if it's not in our object yet
+        const char* result = json_object_get_string(json_response_new, "result");
+        if (result != NULL) {
+            if (strcasecmp(result, "allow") == 0) {
                 ret = DUO_OK;
-            } else if (strcasecmp(p, "deny") == 0) {
+            } else if (strcasecmp(result, "deny") == 0) {
                 ret = DUO_FAIL;
             } else {
-                _duo_seterr(ctx, "BSON invalid 'result': %s",
-                    p);
+                _duo_seterr(ctx, "JSON invalid 'result': %s",
+                    result);
                 ret = DUO_SERVER_ERROR;
             }
             break;
         }
     }
+    _JSON_VALUE_FREE(json);
     return (ret);
 }
diff --git a/lib/https.c b/lib/https.c
index d3429e4..003935e 100644
--- a/lib/https.c
+++ b/lib/https.c
@@ -714,9 +714,11 @@
     while (BIO_flush(req->cbio) != 1) {
         if ((n = _BIO_wait(req->cbio, -1)) != 1) {
             ctx.errstr = n ? _SSL_strerror() : "Write timed out";
+            free(qs);
             return (HTTPS_ERR_SERVER);
         }
     }
+    free(qs);
     return (HTTPS_OK);
 }
 
diff --git a/lib/ini.c b/lib/ini.c
index 909a2fc..ec542ca 100644
--- a/lib/ini.c
+++ b/lib/ini.c
@@ -81,10 +81,15 @@
         if (*prev_name && *start && start > line) {
             /* Non-black line with leading whitespace, treat as continuation
                of previous name's value (as per Python ConfigParser). */
-	    if (!handler(user, section, prev_name, start) && !error) {
+            value = lskip(start);
+            /* Skip line if this line is a comment with whitespace infront */
+            if (*value == ';') {
+                continue;
+            }
+            if (!handler(user, section, prev_name, start) && !error) {
                 error = lineno;
-		break;
-	    }
+                break;
+            }
         }
         else
 #endif
@@ -102,7 +107,7 @@
             else if (!error) {
                 /* No ']' found on section line */
                 error = lineno;
-		break;
+                break;
             }
         }
         else if (*start && *start != ';') {
@@ -121,13 +126,13 @@
                 strncpy0(prev_name, name, sizeof(prev_name));
                 if (!handler(user, section, name, value) && !error) {
                     error = lineno;
-		    break;
-		}
+                    break;
+                }
             }
             else if (!error) {
                 /* No '=' found on name=value line */
                 error = lineno;
-		break;
+                break;
             }
         }
     }
diff --git a/lib/parson.c b/lib/parson.c
new file mode 100644
index 0000000..617ce5e
--- /dev/null
+++ b/lib/parson.c
@@ -0,0 +1,2424 @@
+/*
+ SPDX-License-Identifier: MIT
+
+ Parson 1.2.1 ( http://kgabis.github.com/parson/ )
+ Copyright (c) 2012 - 2021 Krzysztof Gabis
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+#ifdef _MSC_VER
+#ifndef _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_WARNINGS
+#endif /* _CRT_SECURE_NO_WARNINGS */
+#endif /* _MSC_VER */
+
+#include "parson.h"
+
+#define PARSON_IMPL_VERSION_MAJOR 1
+#define PARSON_IMPL_VERSION_MINOR 2
+#define PARSON_IMPL_VERSION_PATCH 1
+
+#if (PARSON_VERSION_MAJOR != PARSON_IMPL_VERSION_MAJOR)\
+|| (PARSON_VERSION_MINOR != PARSON_IMPL_VERSION_MINOR)\
+|| (PARSON_VERSION_PATCH != PARSON_IMPL_VERSION_PATCH)
+#error "parson version mismatch between parson.c and parson.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <math.h>
+#include <errno.h>
+
+/* Apparently sscanf is not implemented in some "standard" libraries, so don't use it, if you
+ * don't have to. */
+#ifdef sscanf
+#undef sscanf
+#define sscanf THINK_TWICE_ABOUT_USING_SSCANF
+#endif
+
+/* strcpy is unsafe */
+#ifdef strcpy
+#undef strcpy
+#endif
+#define strcpy USE_MEMCPY_INSTEAD_OF_STRCPY
+
+#define STARTING_CAPACITY 16
+#define MAX_NESTING       2048
+
+#define FLOAT_FORMAT "%1.17g" /* do not increase precision without incresing NUM_BUF_SIZE */
+#define NUM_BUF_SIZE 64 /* double printed with "%1.17g" shouldn't be longer than 25 bytes so let's be paranoid and use 64 */
+
+#define SIZEOF_TOKEN(a)       (sizeof(a) - 1)
+#define SKIP_CHAR(str)        ((*str)++)
+#define SKIP_WHITESPACES(str) while (isspace((unsigned char)(**str))) { SKIP_CHAR(str); }
+#define MAX(a, b)             ((a) > (b) ? (a) : (b))
+
+#undef malloc
+#undef free
+
+#if defined(isnan) && defined(isinf)
+#define IS_NUMBER_INVALID(x) (isnan((x)) || isinf((x)))
+#else
+#define IS_NUMBER_INVALID(x) (((x) * 0.0) != 0.0)
+#endif
+
+#define OBJECT_INVALID_IX ((size_t)-1)
+
+static JSON_Malloc_Function parson_malloc = malloc;
+static JSON_Free_Function parson_free = free;
+
+static int parson_escape_slashes = 1;
+
+#define IS_CONT(b) (((unsigned char)(b) & 0xC0) == 0x80) /* is utf-8 continuation byte */
+
+typedef int parson_bool_t;
+
+#define PARSON_TRUE 1
+#define PARSON_FALSE 0
+
+typedef struct json_string {
+    char *chars;
+    size_t length;
+} JSON_String;
+
+/* Type definitions */
+typedef union json_value_value {
+    JSON_String  string;
+    double       number;
+    JSON_Object *object;
+    JSON_Array  *array;
+    int          boolean;
+    int          null;
+} JSON_Value_Value;
+
+struct json_value_t {
+    JSON_Value      *parent;
+    JSON_Value_Type  type;
+    JSON_Value_Value value;
+};
+
+struct json_object_t {
+    JSON_Value    *wrapping_value;
+    size_t        *cells;
+    unsigned long *hashes;
+    char         **names;
+    JSON_Value   **values;
+    size_t        *cell_ixs;
+    size_t         count;
+    size_t         item_capacity;
+    size_t         cell_capacity;
+};
+
+struct json_array_t {
+    JSON_Value  *wrapping_value;
+    JSON_Value **items;
+    size_t       count;
+    size_t       capacity;
+};
+
+/* Various */
+static char * read_file(const char *filename);
+static void   remove_comments(char *string, const char *start_token, const char *end_token);
+static char * parson_strndup(const char *string, size_t n);
+static char * parson_strdup(const char *string);
+static int    hex_char_to_int(char c);
+static JSON_Status parse_utf16_hex(const char *string, unsigned int *result);
+static int         num_bytes_in_utf8_sequence(unsigned char c);
+static JSON_Status   verify_utf8_sequence(const unsigned char *string, int *len);
+static parson_bool_t is_valid_utf8(const char *string, size_t string_len);
+static parson_bool_t is_decimal(const char *string, size_t length);
+static unsigned long hash_string(const char *string, size_t n);
+
+/* JSON Object */
+static JSON_Object * json_object_make(JSON_Value *wrapping_value);
+static JSON_Status   json_object_init(JSON_Object *object, size_t capacity);
+static void          json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values);
+static JSON_Status   json_object_grow_and_rehash(JSON_Object *object);
+static size_t        json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found);
+static JSON_Status   json_object_add(JSON_Object *object, char *name, JSON_Value *value);
+static JSON_Value  * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len);
+static JSON_Status   json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value);
+static JSON_Status   json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value);
+static void          json_object_free(JSON_Object *object);
+
+/* JSON Array */
+static JSON_Array * json_array_make(JSON_Value *wrapping_value);
+static JSON_Status  json_array_add(JSON_Array *array, JSON_Value *value);
+static JSON_Status  json_array_resize(JSON_Array *array, size_t new_capacity);
+static void         json_array_free(JSON_Array *array);
+
+/* JSON Value */
+static JSON_Value * json_value_init_string_no_copy(char *string, size_t length);
+static const JSON_String * json_value_get_string_desc(const JSON_Value *value);
+
+/* Parser */
+static JSON_Status   skip_quotes(const char **string);
+static JSON_Status   parse_utf16(const char **unprocessed, char **processed);
+static char *        process_string(const char *input, size_t input_len, size_t *output_len);
+static char *        get_quoted_string(const char **string, size_t *output_string_len);
+static JSON_Value *  parse_object_value(const char **string, size_t nesting);
+static JSON_Value *  parse_array_value(const char **string, size_t nesting);
+static JSON_Value *  parse_string_value(const char **string);
+static JSON_Value *  parse_boolean_value(const char **string);
+static JSON_Value *  parse_number_value(const char **string);
+static JSON_Value *  parse_null_value(const char **string);
+static JSON_Value *  parse_value(const char **string, size_t nesting);
+
+/* Serialization */
+static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf);
+static int json_serialize_string(const char *string, size_t len, char *buf);
+static int append_indent(char *buf, int level);
+static int append_string(char *buf, const char *string);
+
+/* Various */
+static char * read_file(const char * filename) {
+    FILE *fp = fopen(filename, "r");
+    size_t size_to_read = 0;
+    size_t size_read = 0;
+    long pos;
+    char *file_contents;
+    if (!fp) {
+        return NULL;
+    }
+    fseek(fp, 0L, SEEK_END);
+    pos = ftell(fp);
+    if (pos < 0) {
+        fclose(fp);
+        return NULL;
+    }
+    size_to_read = pos;
+    rewind(fp);
+    file_contents = (char*)parson_malloc(sizeof(char) * (size_to_read + 1));
+    if (!file_contents) {
+        fclose(fp);
+        return NULL;
+    }
+    size_read = fread(file_contents, 1, size_to_read, fp);
+    if (size_read == 0 || ferror(fp)) {
+        fclose(fp);
+        parson_free(file_contents);
+        return NULL;
+    }
+    fclose(fp);
+    file_contents[size_read] = '\0';
+    return file_contents;
+}
+
+static void remove_comments(char *string, const char *start_token, const char *end_token) {
+    parson_bool_t in_string = PARSON_FALSE, escaped = PARSON_FALSE;
+    size_t i;
+    char *ptr = NULL, current_char;
+    size_t start_token_len = strlen(start_token);
+    size_t end_token_len = strlen(end_token);
+    if (start_token_len == 0 || end_token_len == 0) {
+        return;
+    }
+    while ((current_char = *string) != '\0') {
+        if (current_char == '\\' && !escaped) {
+            escaped = PARSON_TRUE;
+            string++;
+            continue;
+        } else if (current_char == '\"' && !escaped) {
+            in_string = !in_string;
+        } else if (!in_string && strncmp(string, start_token, start_token_len) == 0) {
+            for(i = 0; i < start_token_len; i++) {
+                string[i] = ' ';
+            }
+            string = string + start_token_len;
+            ptr = strstr(string, end_token);
+            if (!ptr) {
+                return;
+            }
+            for (i = 0; i < (ptr - string) + end_token_len; i++) {
+                string[i] = ' ';
+            }
+            string = ptr + end_token_len - 1;
+        }
+        escaped = PARSON_FALSE;
+        string++;
+    }
+}
+
+static char * parson_strndup(const char *string, size_t n) {
+    /* We expect the caller has validated that 'n' fits within the input buffer. */
+    char *output_string = (char*)parson_malloc(n + 1);
+    if (!output_string) {
+        return NULL;
+    }
+    output_string[n] = '\0';
+    memcpy(output_string, string, n);
+    return output_string;
+}
+
+static char * parson_strdup(const char *string) {
+    return parson_strndup(string, strlen(string));
+}
+
+static int hex_char_to_int(char c) {
+    if (c >= '0' && c <= '9') {
+        return c - '0';
+    } else if (c >= 'a' && c <= 'f') {
+        return c - 'a' + 10;
+    } else if (c >= 'A' && c <= 'F') {
+        return c - 'A' + 10;
+    }
+    return -1;
+}
+
+static JSON_Status parse_utf16_hex(const char *s, unsigned int *result) {
+    int x1, x2, x3, x4;
+    if (s[0] == '\0' || s[1] == '\0' || s[2] == '\0' || s[3] == '\0') {
+        return JSONFailure;
+    }
+    x1 = hex_char_to_int(s[0]);
+    x2 = hex_char_to_int(s[1]);
+    x3 = hex_char_to_int(s[2]);
+    x4 = hex_char_to_int(s[3]);
+    if (x1 == -1 || x2 == -1 || x3 == -1 || x4 == -1) {
+        return JSONFailure;
+    }
+    *result = (unsigned int)((x1 << 12) | (x2 << 8) | (x3 << 4) | x4);
+    return JSONSuccess;
+}
+
+static int num_bytes_in_utf8_sequence(unsigned char c) {
+    if (c == 0xC0 || c == 0xC1 || c > 0xF4 || IS_CONT(c)) {
+        return 0;
+    } else if ((c & 0x80) == 0) {    /* 0xxxxxxx */
+        return 1;
+    } else if ((c & 0xE0) == 0xC0) { /* 110xxxxx */
+        return 2;
+    } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx */
+        return 3;
+    } else if ((c & 0xF8) == 0xF0) { /* 11110xxx */
+        return 4;
+    }
+    return 0; /* won't happen */
+}
+
+static JSON_Status verify_utf8_sequence(const unsigned char *string, int *len) {
+    unsigned int cp = 0;
+    *len = num_bytes_in_utf8_sequence(string[0]);
+
+    if (*len == 1) {
+        cp = string[0];
+    } else if (*len == 2 && IS_CONT(string[1])) {
+        cp = string[0] & 0x1F;
+        cp = (cp << 6) | (string[1] & 0x3F);
+    } else if (*len == 3 && IS_CONT(string[1]) && IS_CONT(string[2])) {
+        cp = ((unsigned char)string[0]) & 0xF;
+        cp = (cp << 6) | (string[1] & 0x3F);
+        cp = (cp << 6) | (string[2] & 0x3F);
+    } else if (*len == 4 && IS_CONT(string[1]) && IS_CONT(string[2]) && IS_CONT(string[3])) {
+        cp = string[0] & 0x7;
+        cp = (cp << 6) | (string[1] & 0x3F);
+        cp = (cp << 6) | (string[2] & 0x3F);
+        cp = (cp << 6) | (string[3] & 0x3F);
+    } else {
+        return JSONFailure;
+    }
+
+    /* overlong encodings */
+    if ((cp < 0x80    && *len > 1) ||
+        (cp < 0x800   && *len > 2) ||
+        (cp < 0x10000 && *len > 3)) {
+        return JSONFailure;
+    }
+
+    /* invalid unicode */
+    if (cp > 0x10FFFF) {
+        return JSONFailure;
+    }
+
+    /* surrogate halves */
+    if (cp >= 0xD800 && cp <= 0xDFFF) {
+        return JSONFailure;
+    }
+
+    return JSONSuccess;
+}
+
+static int is_valid_utf8(const char *string, size_t string_len) {
+    int len = 0;
+    const char *string_end =  string + string_len;
+    while (string < string_end) {
+        if (verify_utf8_sequence((const unsigned char*)string, &len) != JSONSuccess) {
+            return PARSON_FALSE;
+        }
+        string += len;
+    }
+    return PARSON_TRUE;
+}
+
+static parson_bool_t is_decimal(const char *string, size_t length) {
+    if (length > 1 && string[0] == '0' && string[1] != '.') {
+        return PARSON_FALSE;
+    }
+    if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.') {
+        return PARSON_FALSE;
+    }
+    while (length--) {
+        if (strchr("xX", string[length])) {
+            return PARSON_FALSE;
+        }
+    }
+    return PARSON_TRUE;
+}
+
+static unsigned long hash_string(const char *string, size_t n) {
+#ifdef PARSON_FORCE_HASH_COLLISIONS
+    (void)string;
+    (void)n;
+    return 0;
+#else
+    unsigned long hash = 5381;
+    unsigned char c;
+    size_t i = 0;
+    for (i = 0; i < n; i++) {
+        c = string[i];
+        if (c == '\0') {
+            break;
+        }
+        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
+    }
+    return hash;
+#endif
+}
+
+/* JSON Object */
+static JSON_Object * json_object_make(JSON_Value *wrapping_value) {
+    JSON_Status res = JSONFailure;
+    JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object));
+    if (new_obj == NULL) {
+        return NULL;
+    }
+    new_obj->wrapping_value = wrapping_value;
+    res = json_object_init(new_obj, 0);
+    if (res != JSONSuccess) {
+        parson_free(new_obj);
+        return NULL;
+    }
+    return new_obj;
+}
+
+static JSON_Status json_object_init(JSON_Object *object, size_t capacity) {
+    unsigned int i = 0;
+
+    object->cells = NULL;
+    object->names = NULL;
+    object->values = NULL;
+    object->cell_ixs = NULL;
+    object->hashes = NULL;
+
+    object->count = 0;
+    object->cell_capacity = capacity;
+    object->item_capacity = (unsigned int)(capacity * 0.7f);
+
+    if (capacity == 0) {
+        return JSONSuccess;
+    }
+
+    object->cells = (size_t*)parson_malloc(object->cell_capacity * sizeof(*object->cells));
+    object->names = (char**)parson_malloc(object->item_capacity * sizeof(*object->names));
+    object->values = (JSON_Value**)parson_malloc(object->item_capacity * sizeof(*object->values));
+    object->cell_ixs = (size_t*)parson_malloc(object->item_capacity * sizeof(*object->cell_ixs));
+    object->hashes = (unsigned long*)parson_malloc(object->item_capacity * sizeof(*object->hashes));
+    if (object->cells == NULL
+        || object->names == NULL
+        || object->values == NULL
+        || object->cell_ixs == NULL
+        || object->hashes == NULL) {
+        goto error;
+    }
+    for (i = 0; i < object->cell_capacity; i++) {
+        object->cells[i] = OBJECT_INVALID_IX;
+    }
+    return JSONSuccess;
+error:
+    parson_free(object->cells);
+    parson_free(object->names);
+    parson_free(object->values);
+    parson_free(object->cell_ixs);
+    parson_free(object->hashes);
+    return JSONFailure;
+}
+
+static void json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values) {
+    unsigned int i = 0;
+    for (i = 0; i < object->count; i++) {
+        if (free_keys) {
+            parson_free(object->names[i]);
+        }
+        if (free_values) {
+            json_value_free(object->values[i]);
+        }
+    }
+
+    object->count = 0;
+    object->item_capacity = 0;
+    object->cell_capacity = 0;
+
+    parson_free(object->cells);
+    parson_free(object->names);
+    parson_free(object->values);
+    parson_free(object->cell_ixs);
+    parson_free(object->hashes);
+
+    object->cells = NULL;
+    object->names = NULL;
+    object->values = NULL;
+    object->cell_ixs = NULL;
+    object->hashes = NULL;
+}
+
+static JSON_Status json_object_grow_and_rehash(JSON_Object *object) {
+    JSON_Value *wrapping_value = NULL;
+    JSON_Object new_object;
+    char *key = NULL;
+    JSON_Value *value = NULL;
+    unsigned int i = 0;
+    size_t new_capacity = MAX(object->cell_capacity * 2, STARTING_CAPACITY);
+    JSON_Status res = json_object_init(&new_object, new_capacity);
+    if (res != JSONSuccess) {
+        return JSONFailure;
+    }
+
+    wrapping_value = json_object_get_wrapping_value(object);
+    new_object.wrapping_value = wrapping_value;
+
+    for (i = 0; i < object->count; i++) {
+        key = object->names[i];
+        value = object->values[i];
+        res = json_object_add(&new_object, key, value);
+        if (res != JSONSuccess) {
+            json_object_deinit(&new_object, PARSON_FALSE, PARSON_FALSE);
+            return JSONFailure;
+        }
+        value->parent = wrapping_value;
+    }
+    json_object_deinit(object, PARSON_FALSE, PARSON_FALSE);
+    *object = new_object;
+    return JSONSuccess;
+}
+
+static size_t json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found) {
+    size_t cell_ix = hash & (object->cell_capacity - 1);
+    size_t cell = 0;
+    size_t ix = 0;
+    unsigned int i = 0;
+    unsigned long hash_to_check = 0;
+    const char *key_to_check = NULL;
+    size_t key_to_check_len = 0;
+
+    *out_found = PARSON_FALSE;
+
+    for (i = 0; i < object->cell_capacity; i++) {
+        ix = (cell_ix + i) & (object->cell_capacity - 1);
+        cell = object->cells[ix];
+        if (cell == OBJECT_INVALID_IX) {
+            return ix;
+        }
+        hash_to_check = object->hashes[cell];
+        if (hash != hash_to_check) {
+            continue;
+        }
+        key_to_check = object->names[cell];
+        key_to_check_len = strlen(key_to_check);
+        if (key_to_check_len == key_len && strncmp(key, key_to_check, key_len) == 0) {
+            *out_found = PARSON_TRUE;
+            return ix;
+        }
+    }
+    return OBJECT_INVALID_IX;
+}
+
+static JSON_Status json_object_add(JSON_Object *object, char *name, JSON_Value *value) {
+    unsigned long hash = 0;
+    parson_bool_t found = PARSON_FALSE;
+    size_t cell_ix = 0;
+    JSON_Status res = JSONFailure;
+
+    if (!object || !name || !value) {
+        return JSONFailure;
+    }
+
+    hash = hash_string(name, strlen(name));
+    found = PARSON_FALSE;
+    cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found);
+    if (found) {
+        return JSONFailure;
+    }
+
+    if (object->count >= object->item_capacity) {
+        res = json_object_grow_and_rehash(object);
+        if (res != JSONSuccess) {
+            return JSONFailure;
+        }
+        cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found);
+    }
+
+    object->names[object->count] = name;
+    object->cells[cell_ix] = object->count;
+    object->values[object->count] = value;
+    object->cell_ixs[object->count] = cell_ix;
+    object->hashes[object->count] = hash;
+    object->count++;
+    value->parent = json_object_get_wrapping_value(object);
+
+    return JSONSuccess;
+}
+
+static JSON_Value * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len) {
+    unsigned long hash = 0;
+    parson_bool_t found = PARSON_FALSE;
+    unsigned long cell_ix = 0;
+    size_t item_ix = 0;
+    if (!object || !name) {
+        return NULL;
+    }
+    hash = hash_string(name, name_len);
+    found = PARSON_FALSE;
+    cell_ix = json_object_get_cell_ix(object, name, name_len, hash, &found);
+    if (!found) {
+        return NULL;
+    }
+    item_ix = object->cells[cell_ix];
+    return object->values[item_ix];
+}
+
+static JSON_Status json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) {
+    unsigned long hash = 0;
+    parson_bool_t found = PARSON_FALSE;
+    size_t cell = 0;
+    size_t item_ix = 0;
+    size_t last_item_ix = 0;
+    size_t i = 0;
+    size_t j = 0;
+    size_t x = 0;
+    size_t k = 0;
+    JSON_Value *val = NULL;
+
+    if (object == NULL) {
+        return JSONFailure;
+    }
+
+    hash = hash_string(name, strlen(name));
+    found = PARSON_FALSE;
+    cell = json_object_get_cell_ix(object, name, strlen(name), hash, &found);
+    if (!found) {
+        return JSONFailure;
+    }
+
+    item_ix = object->cells[cell];
+    if (free_value) {
+        val = object->values[item_ix];
+        json_value_free(val);
+        val = NULL;
+    }
+
+    parson_free(object->names[item_ix]);
+    last_item_ix = object->count - 1;
+    if (item_ix < last_item_ix) {
+        object->names[item_ix] = object->names[last_item_ix];
+        object->values[item_ix] = object->values[last_item_ix];
+        object->cell_ixs[item_ix] = object->cell_ixs[last_item_ix];
+        object->hashes[item_ix] = object->hashes[last_item_ix];
+        object->cells[object->cell_ixs[item_ix]] = item_ix;
+    }
+    object->count--;
+
+    i = cell;
+    j = i;
+    for (x = 0; x < (object->cell_capacity - 1); x++) {
+        j = (j + 1) & (object->cell_capacity - 1);
+        if (object->cells[j] == OBJECT_INVALID_IX) {
+            break;
+        }
+        k = object->hashes[object->cells[j]] & (object->cell_capacity - 1);
+        if ((j > i && (k <= i || k > j))
+         || (j < i && (k <= i && k > j))) {
+            object->cell_ixs[object->cells[j]] = i;
+            object->cells[i] = object->cells[j];
+            i = j;
+        }
+    }
+    object->cells[i] = OBJECT_INVALID_IX;
+    return JSONSuccess;
+}
+
+static JSON_Status json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) {
+    JSON_Value *temp_value = NULL;
+    JSON_Object *temp_object = NULL;
+    const char *dot_pos = strchr(name, '.');
+    if (!dot_pos) {
+        return json_object_remove_internal(object, name, free_value);
+    }
+    temp_value = json_object_getn_value(object, name, dot_pos - name);
+    if (json_value_get_type(temp_value) != JSONObject) {
+        return JSONFailure;
+    }
+    temp_object = json_value_get_object(temp_value);
+    return json_object_dotremove_internal(temp_object, dot_pos + 1, free_value);
+}
+
+static void json_object_free(JSON_Object *object) {
+    json_object_deinit(object, PARSON_TRUE, PARSON_TRUE);
+    parson_free(object);
+}
+
+/* JSON Array */
+static JSON_Array * json_array_make(JSON_Value *wrapping_value) {
+    JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array));
+    if (new_array == NULL) {
+        return NULL;
+    }
+    new_array->wrapping_value = wrapping_value;
+    new_array->items = (JSON_Value**)NULL;
+    new_array->capacity = 0;
+    new_array->count = 0;
+    return new_array;
+}
+
+static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value) {
+    if (array->count >= array->capacity) {
+        size_t new_capacity = MAX(array->capacity * 2, STARTING_CAPACITY);
+        if (json_array_resize(array, new_capacity) != JSONSuccess) {
+            return JSONFailure;
+        }
+    }
+    value->parent = json_array_get_wrapping_value(array);
+    array->items[array->count] = value;
+    array->count++;
+    return JSONSuccess;
+}
+
+static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity) {
+    JSON_Value **new_items = NULL;
+    if (new_capacity == 0) {
+        return JSONFailure;
+    }
+    new_items = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*));
+    if (new_items == NULL) {
+        return JSONFailure;
+    }
+    if (array->items != NULL && array->count > 0) {
+        memcpy(new_items, array->items, array->count * sizeof(JSON_Value*));
+    }
+    parson_free(array->items);
+    array->items = new_items;
+    array->capacity = new_capacity;
+    return JSONSuccess;
+}
+
+static void json_array_free(JSON_Array *array) {
+    size_t i;
+    for (i = 0; i < array->count; i++) {
+        json_value_free(array->items[i]);
+    }
+    parson_free(array->items);
+    parson_free(array);
+}
+
+/* JSON Value */
+static JSON_Value * json_value_init_string_no_copy(char *string, size_t length) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONString;
+    new_value->value.string.chars = string;
+    new_value->value.string.length = length;
+    return new_value;
+}
+
+/* Parser */
+static JSON_Status skip_quotes(const char **string) {
+    if (**string != '\"') {
+        return JSONFailure;
+    }
+    SKIP_CHAR(string);
+    while (**string != '\"') {
+        if (**string == '\0') {
+            return JSONFailure;
+        } else if (**string == '\\') {
+            SKIP_CHAR(string);
+            if (**string == '\0') {
+                return JSONFailure;
+            }
+        }
+        SKIP_CHAR(string);
+    }
+    SKIP_CHAR(string);
+    return JSONSuccess;
+}
+
+static JSON_Status parse_utf16(const char **unprocessed, char **processed) {
+    unsigned int cp, lead, trail;
+    char *processed_ptr = *processed;
+    const char *unprocessed_ptr = *unprocessed;
+    JSON_Status status = JSONFailure;
+    unprocessed_ptr++; /* skips u */
+    status = parse_utf16_hex(unprocessed_ptr, &cp);
+    if (status != JSONSuccess) {
+        return JSONFailure;
+    }
+    if (cp < 0x80) {
+        processed_ptr[0] = (char)cp; /* 0xxxxxxx */
+    } else if (cp < 0x800) {
+        processed_ptr[0] = ((cp >> 6) & 0x1F) | 0xC0; /* 110xxxxx */
+        processed_ptr[1] = ((cp)      & 0x3F) | 0x80; /* 10xxxxxx */
+        processed_ptr += 1;
+    } else if (cp < 0xD800 || cp > 0xDFFF) {
+        processed_ptr[0] = ((cp >> 12) & 0x0F) | 0xE0; /* 1110xxxx */
+        processed_ptr[1] = ((cp >> 6)  & 0x3F) | 0x80; /* 10xxxxxx */
+        processed_ptr[2] = ((cp)       & 0x3F) | 0x80; /* 10xxxxxx */
+        processed_ptr += 2;
+    } else if (cp >= 0xD800 && cp <= 0xDBFF) { /* lead surrogate (0xD800..0xDBFF) */
+        lead = cp;
+        unprocessed_ptr += 4; /* should always be within the buffer, otherwise previous sscanf would fail */
+        if (*unprocessed_ptr++ != '\\' || *unprocessed_ptr++ != 'u') {
+            return JSONFailure;
+        }
+        status = parse_utf16_hex(unprocessed_ptr, &trail);
+        if (status != JSONSuccess || trail < 0xDC00 || trail > 0xDFFF) { /* valid trail surrogate? (0xDC00..0xDFFF) */
+            return JSONFailure;
+        }
+        cp = ((((lead - 0xD800) & 0x3FF) << 10) | ((trail - 0xDC00) & 0x3FF)) + 0x010000;
+        processed_ptr[0] = (((cp >> 18) & 0x07) | 0xF0); /* 11110xxx */
+        processed_ptr[1] = (((cp >> 12) & 0x3F) | 0x80); /* 10xxxxxx */
+        processed_ptr[2] = (((cp >> 6)  & 0x3F) | 0x80); /* 10xxxxxx */
+        processed_ptr[3] = (((cp)       & 0x3F) | 0x80); /* 10xxxxxx */
+        processed_ptr += 3;
+    } else { /* trail surrogate before lead surrogate */
+        return JSONFailure;
+    }
+    unprocessed_ptr += 3;
+    *processed = processed_ptr;
+    *unprocessed = unprocessed_ptr;
+    return JSONSuccess;
+}
+
+
+/* Copies and processes passed string up to supplied length.
+Example: "\u006Corem ipsum" -> lorem ipsum */
+static char* process_string(const char *input, size_t input_len, size_t *output_len) {
+    const char *input_ptr = input;
+    size_t initial_size = (input_len + 1) * sizeof(char);
+    size_t final_size = 0;
+    char *output = NULL, *output_ptr = NULL, *resized_output = NULL;
+    output = (char*)parson_malloc(initial_size);
+    if (output == NULL) {
+        goto error;
+    }
+    output_ptr = output;
+    while ((*input_ptr != '\0') && (size_t)(input_ptr - input) < input_len) {
+        if (*input_ptr == '\\') {
+            input_ptr++;
+            switch (*input_ptr) {
+                case '\"': *output_ptr = '\"'; break;
+                case '\\': *output_ptr = '\\'; break;
+                case '/':  *output_ptr = '/';  break;
+                case 'b':  *output_ptr = '\b'; break;
+                case 'f':  *output_ptr = '\f'; break;
+                case 'n':  *output_ptr = '\n'; break;
+                case 'r':  *output_ptr = '\r'; break;
+                case 't':  *output_ptr = '\t'; break;
+                case 'u':
+                    if (parse_utf16(&input_ptr, &output_ptr) != JSONSuccess) {
+                        goto error;
+                    }
+                    break;
+                default:
+                    goto error;
+            }
+        } else if ((unsigned char)*input_ptr < 0x20) {
+            goto error; /* 0x00-0x19 are invalid characters for json string (http://www.ietf.org/rfc/rfc4627.txt) */
+        } else {
+            *output_ptr = *input_ptr;
+        }
+        output_ptr++;
+        input_ptr++;
+    }
+    *output_ptr = '\0';
+    /* resize to new length */
+    final_size = (size_t)(output_ptr-output) + 1;
+    /* todo: don't resize if final_size == initial_size */
+    resized_output = (char*)parson_malloc(final_size);
+    if (resized_output == NULL) {
+        goto error;
+    }
+    memcpy(resized_output, output, final_size);
+    *output_len = final_size - 1;
+    parson_free(output);
+    return resized_output;
+error:
+    parson_free(output);
+    return NULL;
+}
+
+/* Return processed contents of a string between quotes and
+   skips passed argument to a matching quote. */
+static char * get_quoted_string(const char **string, size_t *output_string_len) {
+    const char *string_start = *string;
+    size_t input_string_len = 0;
+    JSON_Status status = skip_quotes(string);
+    if (status != JSONSuccess) {
+        return NULL;
+    }
+    input_string_len = *string - string_start - 2; /* length without quotes */
+    return process_string(string_start + 1, input_string_len, output_string_len);
+}
+
+static JSON_Value * parse_value(const char **string, size_t nesting) {
+    if (nesting > MAX_NESTING) {
+        return NULL;
+    }
+    SKIP_WHITESPACES(string);
+    switch (**string) {
+        case '{':
+            return parse_object_value(string, nesting + 1);
+        case '[':
+            return parse_array_value(string, nesting + 1);
+        case '\"':
+            return parse_string_value(string);
+        case 'f': case 't':
+            return parse_boolean_value(string);
+        case '-':
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+            return parse_number_value(string);
+        case 'n':
+            return parse_null_value(string);
+        default:
+            return NULL;
+    }
+}
+
+static JSON_Value * parse_object_value(const char **string, size_t nesting) {
+    JSON_Status status = JSONFailure;
+    JSON_Value *output_value = NULL, *new_value = NULL;
+    JSON_Object *output_object = NULL;
+    char *new_key = NULL;
+
+    output_value = json_value_init_object();
+    if (output_value == NULL) {
+        return NULL;
+    }
+    if (**string != '{') {
+        json_value_free(output_value);
+        return NULL;
+    }
+    output_object = json_value_get_object(output_value);
+    SKIP_CHAR(string);
+    SKIP_WHITESPACES(string);
+    if (**string == '}') { /* empty object */
+        SKIP_CHAR(string);
+        return output_value;
+    }
+    while (**string != '\0') {
+        size_t key_len = 0;
+        new_key = get_quoted_string(string, &key_len);
+        /* We do not support key names with embedded \0 chars */
+        if (!new_key) {
+            json_value_free(output_value);
+            return NULL;
+        }
+        if (key_len != strlen(new_key)) {
+            parson_free(new_key);
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_WHITESPACES(string);
+        if (**string != ':') {
+            parson_free(new_key);
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_CHAR(string);
+        new_value = parse_value(string, nesting);
+        if (new_value == NULL) {
+            parson_free(new_key);
+            json_value_free(output_value);
+            return NULL;
+        }
+        status = json_object_add(output_object, new_key, new_value);
+        if (status != JSONSuccess) {
+            parson_free(new_key);
+            json_value_free(new_value);
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_WHITESPACES(string);
+        if (**string != ',') {
+            break;
+        }
+        SKIP_CHAR(string);
+        SKIP_WHITESPACES(string);
+    }
+    SKIP_WHITESPACES(string);
+    if (**string != '}') {
+        json_value_free(output_value);
+        return NULL;
+    }
+    SKIP_CHAR(string);
+    return output_value;
+}
+
+static JSON_Value * parse_array_value(const char **string, size_t nesting) {
+    JSON_Value *output_value = NULL, *new_array_value = NULL;
+    JSON_Array *output_array = NULL;
+    output_value = json_value_init_array();
+    if (output_value == NULL) {
+        return NULL;
+    }
+    if (**string != '[') {
+        json_value_free(output_value);
+        return NULL;
+    }
+    output_array = json_value_get_array(output_value);
+    SKIP_CHAR(string);
+    SKIP_WHITESPACES(string);
+    if (**string == ']') { /* empty array */
+        SKIP_CHAR(string);
+        return output_value;
+    }
+    while (**string != '\0') {
+        new_array_value = parse_value(string, nesting);
+        if (new_array_value == NULL) {
+            json_value_free(output_value);
+            return NULL;
+        }
+        if (json_array_add(output_array, new_array_value) != JSONSuccess) {
+            json_value_free(new_array_value);
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_WHITESPACES(string);
+        if (**string != ',') {
+            break;
+        }
+        SKIP_CHAR(string);
+        SKIP_WHITESPACES(string);
+    }
+    SKIP_WHITESPACES(string);
+    if (**string != ']' || /* Trim array after parsing is over */
+        json_array_resize(output_array, json_array_get_count(output_array)) != JSONSuccess) {
+            json_value_free(output_value);
+            return NULL;
+    }
+    SKIP_CHAR(string);
+    return output_value;
+}
+
+static JSON_Value * parse_string_value(const char **string) {
+    JSON_Value *value = NULL;
+    size_t new_string_len = 0;
+    char *new_string = get_quoted_string(string, &new_string_len);
+    if (new_string == NULL) {
+        return NULL;
+    }
+    value = json_value_init_string_no_copy(new_string, new_string_len);
+    if (value == NULL) {
+        parson_free(new_string);
+        return NULL;
+    }
+    return value;
+}
+
+static JSON_Value * parse_boolean_value(const char **string) {
+    size_t true_token_size = SIZEOF_TOKEN("true");
+    size_t false_token_size = SIZEOF_TOKEN("false");
+    if (strncmp("true", *string, true_token_size) == 0) {
+        *string += true_token_size;
+        return json_value_init_boolean(1);
+    } else if (strncmp("false", *string, false_token_size) == 0) {
+        *string += false_token_size;
+        return json_value_init_boolean(0);
+    }
+    return NULL;
+}
+
+static JSON_Value * parse_number_value(const char **string) {
+    char *end;
+    double number = 0;
+    errno = 0;
+    number = strtod(*string, &end);
+    if (errno == ERANGE && (number <= -HUGE_VAL || number >= HUGE_VAL)) {
+        return NULL;
+    }
+    if ((errno && errno != ERANGE) || !is_decimal(*string, end - *string)) {
+        return NULL;
+    }
+    *string = end;
+    return json_value_init_number(number);
+}
+
+static JSON_Value * parse_null_value(const char **string) {
+    size_t token_size = SIZEOF_TOKEN("null");
+    if (strncmp("null", *string, token_size) == 0) {
+        *string += token_size;
+        return json_value_init_null();
+    }
+    return NULL;
+}
+
+/* Serialization */
+#define APPEND_STRING(str) do { written = append_string(buf, (str));\
+                                if (written < 0) { return -1; }\
+                                if (buf != NULL) { buf += written; }\
+                                written_total += written; } while(0)
+
+#define APPEND_INDENT(level) do { written = append_indent(buf, (level));\
+                                  if (written < 0) { return -1; }\
+                                  if (buf != NULL) { buf += written; }\
+                                  written_total += written; } while(0)
+
+static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf)
+{
+    const char *key = NULL, *string = NULL;
+    JSON_Value *temp_value = NULL;
+    JSON_Array *array = NULL;
+    JSON_Object *object = NULL;
+    size_t i = 0, count = 0;
+    double num = 0.0;
+    int written = -1, written_total = 0;
+    size_t len = 0;
+
+    switch (json_value_get_type(value)) {
+        case JSONArray:
+            array = json_value_get_array(value);
+            count = json_array_get_count(array);
+            APPEND_STRING("[");
+            if (count > 0 && is_pretty) {
+                APPEND_STRING("\n");
+            }
+            for (i = 0; i < count; i++) {
+                if (is_pretty) {
+                    APPEND_INDENT(level+1);
+                }
+                temp_value = json_array_get_value(array, i);
+                written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
+                if (written < 0) {
+                    return -1;
+                }
+                if (buf != NULL) {
+                    buf += written;
+                }
+                written_total += written;
+                if (i < (count - 1)) {
+                    APPEND_STRING(",");
+                }
+                if (is_pretty) {
+                    APPEND_STRING("\n");
+                }
+            }
+            if (count > 0 && is_pretty) {
+                APPEND_INDENT(level);
+            }
+            APPEND_STRING("]");
+            return written_total;
+        case JSONObject:
+            object = json_value_get_object(value);
+            count  = json_object_get_count(object);
+            APPEND_STRING("{");
+            if (count > 0 && is_pretty) {
+                APPEND_STRING("\n");
+            }
+            for (i = 0; i < count; i++) {
+                key = json_object_get_name(object, i);
+                if (key == NULL) {
+                    return -1;
+                }
+                if (is_pretty) {
+                    APPEND_INDENT(level+1);
+                }
+                /* We do not support key names with embedded \0 chars */
+                written = json_serialize_string(key, strlen(key), buf);
+                if (written < 0) {
+                    return -1;
+                }
+                if (buf != NULL) {
+                    buf += written;
+                }
+                written_total += written;
+                APPEND_STRING(":");
+                if (is_pretty) {
+                    APPEND_STRING(" ");
+                }
+                temp_value = json_object_get_value_at(object, i);
+                written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
+                if (written < 0) {
+                    return -1;
+                }
+                if (buf != NULL) {
+                    buf += written;
+                }
+                written_total += written;
+                if (i < (count - 1)) {
+                    APPEND_STRING(",");
+                }
+                if (is_pretty) {
+                    APPEND_STRING("\n");
+                }
+            }
+            if (count > 0 && is_pretty) {
+                APPEND_INDENT(level);
+            }
+            APPEND_STRING("}");
+            return written_total;
+        case JSONString:
+            string = json_value_get_string(value);
+            if (string == NULL) {
+                return -1;
+            }
+            len = json_value_get_string_len(value);
+            written = json_serialize_string(string, len, buf);
+            if (written < 0) {
+                return -1;
+            }
+            if (buf != NULL) {
+                buf += written;
+            }
+            written_total += written;
+            return written_total;
+        case JSONBoolean:
+            if (json_value_get_boolean(value)) {
+                APPEND_STRING("true");
+            } else {
+                APPEND_STRING("false");
+            }
+            return written_total;
+        case JSONNumber:
+            num = json_value_get_number(value);
+            if (buf != NULL) {
+                num_buf = buf;
+            }
+            written = sprintf(num_buf, FLOAT_FORMAT, num);
+            if (written < 0) {
+                return -1;
+            }
+            if (buf != NULL) {
+                buf += written;
+            }
+            written_total += written;
+            return written_total;
+        case JSONNull:
+            APPEND_STRING("null");
+            return written_total;
+        case JSONError:
+            return -1;
+        default:
+            return -1;
+    }
+}
+
+static int json_serialize_string(const char *string, size_t len, char *buf) {
+    size_t i = 0;
+    char c = '\0';
+    int written = -1, written_total = 0;
+    APPEND_STRING("\"");
+    for (i = 0; i < len; i++) {
+        c = string[i];
+        switch (c) {
+            case '\"': APPEND_STRING("\\\""); break;
+            case '\\': APPEND_STRING("\\\\"); break;
+            case '\b': APPEND_STRING("\\b"); break;
+            case '\f': APPEND_STRING("\\f"); break;
+            case '\n': APPEND_STRING("\\n"); break;
+            case '\r': APPEND_STRING("\\r"); break;
+            case '\t': APPEND_STRING("\\t"); break;
+            case '\x00': APPEND_STRING("\\u0000"); break;
+            case '\x01': APPEND_STRING("\\u0001"); break;
+            case '\x02': APPEND_STRING("\\u0002"); break;
+            case '\x03': APPEND_STRING("\\u0003"); break;
+            case '\x04': APPEND_STRING("\\u0004"); break;
+            case '\x05': APPEND_STRING("\\u0005"); break;
+            case '\x06': APPEND_STRING("\\u0006"); break;
+            case '\x07': APPEND_STRING("\\u0007"); break;
+            /* '\x08' duplicate: '\b' */
+            /* '\x09' duplicate: '\t' */
+            /* '\x0a' duplicate: '\n' */
+            case '\x0b': APPEND_STRING("\\u000b"); break;
+            /* '\x0c' duplicate: '\f' */
+            /* '\x0d' duplicate: '\r' */
+            case '\x0e': APPEND_STRING("\\u000e"); break;
+            case '\x0f': APPEND_STRING("\\u000f"); break;
+            case '\x10': APPEND_STRING("\\u0010"); break;
+            case '\x11': APPEND_STRING("\\u0011"); break;
+            case '\x12': APPEND_STRING("\\u0012"); break;
+            case '\x13': APPEND_STRING("\\u0013"); break;
+            case '\x14': APPEND_STRING("\\u0014"); break;
+            case '\x15': APPEND_STRING("\\u0015"); break;
+            case '\x16': APPEND_STRING("\\u0016"); break;
+            case '\x17': APPEND_STRING("\\u0017"); break;
+            case '\x18': APPEND_STRING("\\u0018"); break;
+            case '\x19': APPEND_STRING("\\u0019"); break;
+            case '\x1a': APPEND_STRING("\\u001a"); break;
+            case '\x1b': APPEND_STRING("\\u001b"); break;
+            case '\x1c': APPEND_STRING("\\u001c"); break;
+            case '\x1d': APPEND_STRING("\\u001d"); break;
+            case '\x1e': APPEND_STRING("\\u001e"); break;
+            case '\x1f': APPEND_STRING("\\u001f"); break;
+            case '/':
+                if (parson_escape_slashes) {
+                    APPEND_STRING("\\/");  /* to make json embeddable in xml\/html */
+                } else {
+                    APPEND_STRING("/");
+                }
+                break;
+            default:
+                if (buf != NULL) {
+                    buf[0] = c;
+                    buf += 1;
+                }
+                written_total += 1;
+                break;
+        }
+    }
+    APPEND_STRING("\"");
+    return written_total;
+}
+
+static int append_indent(char *buf, int level) {
+    int i;
+    int written = -1, written_total = 0;
+    for (i = 0; i < level; i++) {
+        APPEND_STRING("    ");
+    }
+    return written_total;
+}
+
+static int append_string(char *buf, const char *string) {
+    if (buf == NULL) {
+        return (int)strlen(string);
+    }
+    return sprintf(buf, "%s", string);
+}
+
+#undef APPEND_STRING
+#undef APPEND_INDENT
+
+/* Parser API */
+JSON_Value * json_parse_file(const char *filename) {
+    char *file_contents = read_file(filename);
+    JSON_Value *output_value = NULL;
+    if (file_contents == NULL) {
+        return NULL;
+    }
+    output_value = json_parse_string(file_contents);
+    parson_free(file_contents);
+    return output_value;
+}
+
+JSON_Value * json_parse_file_with_comments(const char *filename) {
+    char *file_contents = read_file(filename);
+    JSON_Value *output_value = NULL;
+    if (file_contents == NULL) {
+        return NULL;
+    }
+    output_value = json_parse_string_with_comments(file_contents);
+    parson_free(file_contents);
+    return output_value;
+}
+
+JSON_Value * json_parse_string(const char *string) {
+    if (string == NULL) {
+        return NULL;
+    }
+    if (string[0] == '\xEF' && string[1] == '\xBB' && string[2] == '\xBF') {
+        string = string + 3; /* Support for UTF-8 BOM */
+    }
+    return parse_value((const char**)&string, 0);
+}
+
+JSON_Value * json_parse_string_with_comments(const char *string) {
+    JSON_Value *result = NULL;
+    char *string_mutable_copy = NULL, *string_mutable_copy_ptr = NULL;
+    string_mutable_copy = parson_strdup(string);
+    if (string_mutable_copy == NULL) {
+        return NULL;
+    }
+    remove_comments(string_mutable_copy, "/*", "*/");
+    remove_comments(string_mutable_copy, "//", "\n");
+    string_mutable_copy_ptr = string_mutable_copy;
+    result = parse_value((const char**)&string_mutable_copy_ptr, 0);
+    parson_free(string_mutable_copy);
+    return result;
+}
+
+/* JSON Object API */
+
+JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) {
+    if (object == NULL || name == NULL) {
+        return NULL;
+    }
+    return json_object_getn_value(object, name, strlen(name));
+}
+
+const char * json_object_get_string(const JSON_Object *object, const char *name) {
+    return json_value_get_string(json_object_get_value(object, name));
+}
+
+size_t json_object_get_string_len(const JSON_Object *object, const char *name) {
+    return json_value_get_string_len(json_object_get_value(object, name));
+}
+
+double json_object_get_number(const JSON_Object *object, const char *name) {
+    return json_value_get_number(json_object_get_value(object, name));
+}
+
+JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) {
+    return json_value_get_object(json_object_get_value(object, name));
+}
+
+JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) {
+    return json_value_get_array(json_object_get_value(object, name));
+}
+
+int json_object_get_boolean(const JSON_Object *object, const char *name) {
+    return json_value_get_boolean(json_object_get_value(object, name));
+}
+
+JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) {
+    const char *dot_position = strchr(name, '.');
+    if (!dot_position) {
+        return json_object_get_value(object, name);
+    }
+    object = json_value_get_object(json_object_getn_value(object, name, dot_position - name));
+    return json_object_dotget_value(object, dot_position + 1);
+}
+
+const char * json_object_dotget_string(const JSON_Object *object, const char *name) {
+    return json_value_get_string(json_object_dotget_value(object, name));
+}
+
+size_t json_object_dotget_string_len(const JSON_Object *object, const char *name) {
+    return json_value_get_string_len(json_object_dotget_value(object, name));
+}
+
+double json_object_dotget_number(const JSON_Object *object, const char *name) {
+    return json_value_get_number(json_object_dotget_value(object, name));
+}
+
+JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) {
+    return json_value_get_object(json_object_dotget_value(object, name));
+}
+
+JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) {
+    return json_value_get_array(json_object_dotget_value(object, name));
+}
+
+int json_object_dotget_boolean(const JSON_Object *object, const char *name) {
+    return json_value_get_boolean(json_object_dotget_value(object, name));
+}
+
+size_t json_object_get_count(const JSON_Object *object) {
+    return object ? object->count : 0;
+}
+
+const char * json_object_get_name(const JSON_Object *object, size_t index) {
+    if (object == NULL || index >= json_object_get_count(object)) {
+        return NULL;
+    }
+    return object->names[index];
+}
+
+JSON_Value * json_object_get_value_at(const JSON_Object *object, size_t index) {
+    if (object == NULL || index >= json_object_get_count(object)) {
+        return NULL;
+    }
+    return object->values[index];
+}
+
+JSON_Value *json_object_get_wrapping_value(const JSON_Object *object) {
+    if (!object) {
+        return NULL;
+    }
+    return object->wrapping_value;
+}
+
+int json_object_has_value (const JSON_Object *object, const char *name) {
+    return json_object_get_value(object, name) != NULL;
+}
+
+int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) {
+    JSON_Value *val = json_object_get_value(object, name);
+    return val != NULL && json_value_get_type(val) == type;
+}
+
+int json_object_dothas_value (const JSON_Object *object, const char *name) {
+    return json_object_dotget_value(object, name) != NULL;
+}
+
+int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) {
+    JSON_Value *val = json_object_dotget_value(object, name);
+    return val != NULL && json_value_get_type(val) == type;
+}
+
+/* JSON Array API */
+JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) {
+    if (array == NULL || index >= json_array_get_count(array)) {
+        return NULL;
+    }
+    return array->items[index];
+}
+
+const char * json_array_get_string(const JSON_Array *array, size_t index) {
+    return json_value_get_string(json_array_get_value(array, index));
+}
+
+size_t json_array_get_string_len(const JSON_Array *array, size_t index) {
+    return json_value_get_string_len(json_array_get_value(array, index));
+}
+
+double json_array_get_number(const JSON_Array *array, size_t index) {
+    return json_value_get_number(json_array_get_value(array, index));
+}
+
+JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) {
+    return json_value_get_object(json_array_get_value(array, index));
+}
+
+JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) {
+    return json_value_get_array(json_array_get_value(array, index));
+}
+
+int json_array_get_boolean(const JSON_Array *array, size_t index) {
+    return json_value_get_boolean(json_array_get_value(array, index));
+}
+
+size_t json_array_get_count(const JSON_Array *array) {
+    return array ? array->count : 0;
+}
+
+JSON_Value * json_array_get_wrapping_value(const JSON_Array *array) {
+    if (!array) {
+        return NULL;
+    }
+    return array->wrapping_value;
+}
+
+/* JSON Value API */
+JSON_Value_Type json_value_get_type(const JSON_Value *value) {
+    return value ? value->type : JSONError;
+}
+
+JSON_Object * json_value_get_object(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONObject ? value->value.object : NULL;
+}
+
+JSON_Array * json_value_get_array(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONArray ? value->value.array : NULL;
+}
+
+static const JSON_String * json_value_get_string_desc(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONString ? &value->value.string : NULL;
+}
+
+const char * json_value_get_string(const JSON_Value *value) {
+    const JSON_String *str = json_value_get_string_desc(value);
+    return str ? str->chars : NULL;
+}
+
+size_t json_value_get_string_len(const JSON_Value *value) {
+    const JSON_String *str = json_value_get_string_desc(value);
+    return str ? str->length : 0;
+}
+
+double json_value_get_number(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONNumber ? value->value.number : 0;
+}
+
+int json_value_get_boolean(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1;
+}
+
+JSON_Value * json_value_get_parent (const JSON_Value *value) {
+    return value ? value->parent : NULL;
+}
+
+void json_value_free(JSON_Value *value) {
+    switch (json_value_get_type(value)) {
+        case JSONObject:
+            json_object_free(value->value.object);
+            break;
+        case JSONString:
+            parson_free(value->value.string.chars);
+            break;
+        case JSONArray:
+            json_array_free(value->value.array);
+            break;
+        default:
+            break;
+    }
+    parson_free(value);
+}
+
+JSON_Value * json_value_init_object(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONObject;
+    new_value->value.object = json_object_make(new_value);
+    if (!new_value->value.object) {
+        parson_free(new_value);
+        return NULL;
+    }
+    return new_value;
+}
+
+JSON_Value * json_value_init_array(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONArray;
+    new_value->value.array = json_array_make(new_value);
+    if (!new_value->value.array) {
+        parson_free(new_value);
+        return NULL;
+    }
+    return new_value;
+}
+
+JSON_Value * json_value_init_string(const char *string) {
+    if (string == NULL) {
+        return NULL;
+    }
+    return json_value_init_string_with_len(string, strlen(string));
+}
+
+JSON_Value * json_value_init_string_with_len(const char *string, size_t length) {
+    char *copy = NULL;
+    JSON_Value *value;
+    if (string == NULL) {
+        return NULL;
+    }
+    if (!is_valid_utf8(string, length)) {
+        return NULL;
+    }
+    copy = parson_strndup(string, length);
+    if (copy == NULL) {
+        return NULL;
+    }
+    value = json_value_init_string_no_copy(copy, length);
+    if (value == NULL) {
+        parson_free(copy);
+    }
+    return value;
+}
+
+JSON_Value * json_value_init_number(double number) {
+    JSON_Value *new_value = NULL;
+    if (IS_NUMBER_INVALID(number)) {
+        return NULL;
+    }
+    new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (new_value == NULL) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONNumber;
+    new_value->value.number = number;
+    return new_value;
+}
+
+JSON_Value * json_value_init_boolean(int boolean) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONBoolean;
+    new_value->value.boolean = boolean ? 1 : 0;
+    return new_value;
+}
+
+JSON_Value * json_value_init_null(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value) {
+        return NULL;
+    }
+    new_value->parent = NULL;
+    new_value->type = JSONNull;
+    return new_value;
+}
+
+JSON_Value * json_value_deep_copy(const JSON_Value *value) {
+    size_t i = 0;
+    JSON_Value *return_value = NULL, *temp_value_copy = NULL, *temp_value = NULL;
+    const JSON_String *temp_string = NULL;
+    const char *temp_key = NULL;
+    char *temp_string_copy = NULL;
+    JSON_Array *temp_array = NULL, *temp_array_copy = NULL;
+    JSON_Object *temp_object = NULL, *temp_object_copy = NULL;
+    JSON_Status res = JSONFailure;
+    char *key_copy = NULL;
+
+    switch (json_value_get_type(value)) {
+        case JSONArray:
+            temp_array = json_value_get_array(value);
+            return_value = json_value_init_array();
+            if (return_value == NULL) {
+                return NULL;
+            }
+            temp_array_copy = json_value_get_array(return_value);
+            for (i = 0; i < json_array_get_count(temp_array); i++) {
+                temp_value = json_array_get_value(temp_array, i);
+                temp_value_copy = json_value_deep_copy(temp_value);
+                if (temp_value_copy == NULL) {
+                    json_value_free(return_value);
+                    return NULL;
+                }
+                if (json_array_add(temp_array_copy, temp_value_copy) != JSONSuccess) {
+                    json_value_free(return_value);
+                    json_value_free(temp_value_copy);
+                    return NULL;
+                }
+            }
+            return return_value;
+        case JSONObject:
+            temp_object = json_value_get_object(value);
+            return_value = json_value_init_object();
+            if (!return_value) {
+                return NULL;
+            }
+            temp_object_copy = json_value_get_object(return_value);
+            for (i = 0; i < json_object_get_count(temp_object); i++) {
+                temp_key = json_object_get_name(temp_object, i);
+                temp_value = json_object_get_value(temp_object, temp_key);
+                temp_value_copy = json_value_deep_copy(temp_value);
+                if (!temp_value_copy) {
+                    json_value_free(return_value);
+                    return NULL;
+                }
+                key_copy = parson_strdup(temp_key);
+                if (!key_copy) {
+                    json_value_free(temp_value_copy);
+                    json_value_free(return_value);
+                    return NULL;
+                }
+                res = json_object_add(temp_object_copy, key_copy, temp_value_copy);
+                if (res != JSONSuccess) {
+                    parson_free(key_copy);
+                    json_value_free(temp_value_copy);
+                    json_value_free(return_value);
+                    return NULL;
+                }
+            }
+            return return_value;
+        case JSONBoolean:
+            return json_value_init_boolean(json_value_get_boolean(value));
+        case JSONNumber:
+            return json_value_init_number(json_value_get_number(value));
+        case JSONString:
+            temp_string = json_value_get_string_desc(value);
+            if (temp_string == NULL) {
+                return NULL;
+            }
+            temp_string_copy = parson_strndup(temp_string->chars, temp_string->length);
+            if (temp_string_copy == NULL) {
+                return NULL;
+            }
+            return_value = json_value_init_string_no_copy(temp_string_copy, temp_string->length);
+            if (return_value == NULL) {
+                parson_free(temp_string_copy);
+            }
+            return return_value;
+        case JSONNull:
+            return json_value_init_null();
+        case JSONError:
+            return NULL;
+        default:
+            return NULL;
+    }
+}
+
+size_t json_serialization_size(const JSON_Value *value) {
+    char num_buf[NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */
+    int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_FALSE, num_buf);
+    return res < 0 ? 0 : (size_t)(res) + 1;
+}
+
+JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) {
+    int written = -1;
+    size_t needed_size_in_bytes = json_serialization_size(value);
+    if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) {
+        return JSONFailure;
+    }
+    written = json_serialize_to_buffer_r(value, buf, 0, PARSON_FALSE, NULL);
+    if (written < 0) {
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename) {
+    JSON_Status return_code = JSONSuccess;
+    FILE *fp = NULL;
+    char *serialized_string = json_serialize_to_string(value);
+    if (serialized_string == NULL) {
+        return JSONFailure;
+    }
+    fp = fopen(filename, "w");
+    if (fp == NULL) {
+        json_free_serialized_string(serialized_string);
+        return JSONFailure;
+    }
+    if (fputs(serialized_string, fp) == EOF) {
+        return_code = JSONFailure;
+    }
+    if (fclose(fp) == EOF) {
+        return_code = JSONFailure;
+    }
+    json_free_serialized_string(serialized_string);
+    return return_code;
+}
+
+char * json_serialize_to_string(const JSON_Value *value) {
+    JSON_Status serialization_result = JSONFailure;
+    size_t buf_size_bytes = json_serialization_size(value);
+    char *buf = NULL;
+    if (buf_size_bytes == 0) {
+        return NULL;
+    }
+    buf = (char*)parson_malloc(buf_size_bytes);
+    if (buf == NULL) {
+        return NULL;
+    }
+    serialization_result = json_serialize_to_buffer(value, buf, buf_size_bytes);
+    if (serialization_result != JSONSuccess) {
+        json_free_serialized_string(buf);
+        return NULL;
+    }
+    return buf;
+}
+
+size_t json_serialization_size_pretty(const JSON_Value *value) {
+    char num_buf[NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */
+    int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_TRUE, num_buf);
+    return res < 0 ? 0 : (size_t)(res) + 1;
+}
+
+JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) {
+    int written = -1;
+    size_t needed_size_in_bytes = json_serialization_size_pretty(value);
+    if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) {
+        return JSONFailure;
+    }
+    written = json_serialize_to_buffer_r(value, buf, 0, PARSON_TRUE, NULL);
+    if (written < 0) {
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename) {
+    JSON_Status return_code = JSONSuccess;
+    FILE *fp = NULL;
+    char *serialized_string = json_serialize_to_string_pretty(value);
+    if (serialized_string == NULL) {
+        return JSONFailure;
+    }
+    fp = fopen(filename, "w");
+    if (fp == NULL) {
+        json_free_serialized_string(serialized_string);
+        return JSONFailure;
+    }
+    if (fputs(serialized_string, fp) == EOF) {
+        return_code = JSONFailure;
+    }
+    if (fclose(fp) == EOF) {
+        return_code = JSONFailure;
+    }
+    json_free_serialized_string(serialized_string);
+    return return_code;
+}
+
+char * json_serialize_to_string_pretty(const JSON_Value *value) {
+    JSON_Status serialization_result = JSONFailure;
+    size_t buf_size_bytes = json_serialization_size_pretty(value);
+    char *buf = NULL;
+    if (buf_size_bytes == 0) {
+        return NULL;
+    }
+    buf = (char*)parson_malloc(buf_size_bytes);
+    if (buf == NULL) {
+        return NULL;
+    }
+    serialization_result = json_serialize_to_buffer_pretty(value, buf, buf_size_bytes);
+    if (serialization_result != JSONSuccess) {
+        json_free_serialized_string(buf);
+        return NULL;
+    }
+    return buf;
+}
+
+void json_free_serialized_string(char *string) {
+    parson_free(string);
+}
+
+JSON_Status json_array_remove(JSON_Array *array, size_t ix) {
+    size_t to_move_bytes = 0;
+    if (array == NULL || ix >= json_array_get_count(array)) {
+        return JSONFailure;
+    }
+    json_value_free(json_array_get_value(array, ix));
+    to_move_bytes = (json_array_get_count(array) - 1 - ix) * sizeof(JSON_Value*);
+    memmove(array->items + ix, array->items + ix + 1, to_move_bytes);
+    array->count -= 1;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_value(JSON_Array *array, size_t ix, JSON_Value *value) {
+    if (array == NULL || value == NULL || value->parent != NULL || ix >= json_array_get_count(array)) {
+        return JSONFailure;
+    }
+    json_value_free(json_array_get_value(array, ix));
+    value->parent = json_array_get_wrapping_value(array);
+    array->items[ix] = value;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_replace_value(array, i, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len) {
+    JSON_Value *value = json_value_init_string_with_len(string, len);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_replace_value(array, i, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_replace_value(array, i, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_replace_value(array, i, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_null(JSON_Array *array, size_t i) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_replace_value(array, i, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_clear(JSON_Array *array) {
+    size_t i = 0;
+    if (array == NULL) {
+        return JSONFailure;
+    }
+    for (i = 0; i < json_array_get_count(array); i++) {
+        json_value_free(json_array_get_value(array, i));
+    }
+    array->count = 0;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value) {
+    if (array == NULL || value == NULL || value->parent != NULL) {
+        return JSONFailure;
+    }
+    return json_array_add(array, value);
+}
+
+JSON_Status json_array_append_string(JSON_Array *array, const char *string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_append_value(array, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len) {
+    JSON_Value *value = json_value_init_string_with_len(string, len);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_append_value(array, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_number(JSON_Array *array, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_append_value(array, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_boolean(JSON_Array *array, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_append_value(array, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_null(JSON_Array *array) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_array_append_value(array, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value) {
+    unsigned long hash = 0;
+    parson_bool_t found = PARSON_FALSE;
+    size_t cell_ix = 0;
+    size_t item_ix = 0;
+    JSON_Value *old_value = NULL;
+    char *key_copy = NULL;
+
+    if (!object || !name || !value || value->parent) {
+        return JSONFailure;
+    }
+    hash = hash_string(name, strlen(name));
+    found = PARSON_FALSE;
+    cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found);
+    if (found) {
+        item_ix = object->cells[cell_ix];
+        old_value = object->values[item_ix];
+        json_value_free(old_value);
+        object->values[item_ix] = value;
+        value->parent = json_object_get_wrapping_value(object);
+        return JSONSuccess;
+    }
+    if (object->count >= object->item_capacity) {
+        JSON_Status res = json_object_grow_and_rehash(object);
+        if (res != JSONSuccess) {
+            return JSONFailure;
+        }
+        cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found);
+    }
+    key_copy = parson_strdup(name);
+    if (!key_copy) {
+        return JSONFailure;
+    }
+    object->names[object->count] = key_copy;
+    object->cells[cell_ix] = object->count;
+    object->values[object->count] = value;
+    object->cell_ixs[object->count] = cell_ix;
+    object->hashes[object->count] = hash;
+    object->count++;
+    value->parent = json_object_get_wrapping_value(object);
+    return JSONSuccess;
+}
+
+JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string) {
+    JSON_Value *value = json_value_init_string(string);
+    JSON_Status status = json_object_set_value(object, name, value);
+    if (status != JSONSuccess) {
+        json_value_free(value);
+    }
+    return status;
+}
+
+JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) {
+    JSON_Value *value = json_value_init_string_with_len(string, len);
+    JSON_Status status = json_object_set_value(object, name, value);
+    if (status != JSONSuccess) {
+        json_value_free(value);
+    }
+    return status;
+}
+
+JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    JSON_Status status = json_object_set_value(object, name, value);
+    if (status != JSONSuccess) {
+        json_value_free(value);
+    }
+    return status;
+}
+
+JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    JSON_Status status = json_object_set_value(object, name, value);
+    if (status != JSONSuccess) {
+        json_value_free(value);
+    }
+    return status;
+}
+
+JSON_Status json_object_set_null(JSON_Object *object, const char *name) {
+    JSON_Value *value = json_value_init_null();
+    JSON_Status status = json_object_set_value(object, name, value);
+    if (status != JSONSuccess) {
+        json_value_free(value);
+    }
+    return status;
+}
+
+JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value) {
+    const char *dot_pos = NULL;
+    JSON_Value *temp_value = NULL, *new_value = NULL;
+    JSON_Object *temp_object = NULL, *new_object = NULL;
+    JSON_Status status = JSONFailure;
+    size_t name_len = 0;
+    char *name_copy = NULL;
+    
+    if (object == NULL || name == NULL || value == NULL) {
+        return JSONFailure;
+    }
+    dot_pos = strchr(name, '.');
+    if (dot_pos == NULL) {
+        return json_object_set_value(object, name, value);
+    }
+    name_len = dot_pos - name;
+    temp_value = json_object_getn_value(object, name, name_len);
+    if (temp_value) {
+        /* Don't overwrite existing non-object (unlike json_object_set_value, but it shouldn't be changed at this point) */
+        if (json_value_get_type(temp_value) != JSONObject) {
+            return JSONFailure;
+        }
+        temp_object = json_value_get_object(temp_value);
+        return json_object_dotset_value(temp_object, dot_pos + 1, value);
+    }
+    new_value = json_value_init_object();
+    if (new_value == NULL) {
+        return JSONFailure;
+    }
+    new_object = json_value_get_object(new_value);
+    status = json_object_dotset_value(new_object, dot_pos + 1, value);
+    if (status != JSONSuccess) {
+        json_value_free(new_value);
+        return JSONFailure;
+    }
+    name_copy = parson_strndup(name, name_len);
+    if (!name_copy) {
+        json_object_dotremove_internal(new_object, dot_pos + 1, 0);
+        json_value_free(new_value);
+        return JSONFailure;
+    }
+    status = json_object_add(object, name_copy, new_value);
+    if (status != JSONSuccess) {
+        parson_free(name_copy);
+        json_object_dotremove_internal(new_object, dot_pos + 1, 0);
+        json_value_free(new_value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_object_dotset_value(object, name, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) {
+    JSON_Value *value = json_value_init_string_with_len(string, len);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_object_dotset_value(object, name, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_object_dotset_value(object, name, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_object_dotset_value(object, name, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_null(JSON_Object *object, const char *name) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL) {
+        return JSONFailure;
+    }
+    if (json_object_dotset_value(object, name, value) != JSONSuccess) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_remove(JSON_Object *object, const char *name) {
+    return json_object_remove_internal(object, name, PARSON_TRUE);
+}
+
+JSON_Status json_object_dotremove(JSON_Object *object, const char *name) {
+    return json_object_dotremove_internal(object, name, PARSON_TRUE);
+}
+
+JSON_Status json_object_clear(JSON_Object *object) {
+    size_t i = 0;
+    if (object == NULL) {
+        return JSONFailure;
+    }
+    for (i = 0; i < json_object_get_count(object); i++) {
+        parson_free(object->names[i]);
+        json_value_free(object->values[i]);
+    }
+    object->count = 0;
+    return JSONSuccess;
+}
+
+JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value) {
+    JSON_Value *temp_schema_value = NULL, *temp_value = NULL;
+    JSON_Array *schema_array = NULL, *value_array = NULL;
+    JSON_Object *schema_object = NULL, *value_object = NULL;
+    JSON_Value_Type schema_type = JSONError, value_type = JSONError;
+    const char *key = NULL;
+    size_t i = 0, count = 0;
+    if (schema == NULL || value == NULL) {
+        return JSONFailure;
+    }
+    schema_type = json_value_get_type(schema);
+    value_type = json_value_get_type(value);
+    if (schema_type != value_type && schema_type != JSONNull) { /* null represents all values */
+        return JSONFailure;
+    }
+    switch (schema_type) {
+        case JSONArray:
+            schema_array = json_value_get_array(schema);
+            value_array = json_value_get_array(value);
+            count = json_array_get_count(schema_array);
+            if (count == 0) {
+                return JSONSuccess; /* Empty array allows all types */
+            }
+            /* Get first value from array, rest is ignored */
+            temp_schema_value = json_array_get_value(schema_array, 0);
+            for (i = 0; i < json_array_get_count(value_array); i++) {
+                temp_value = json_array_get_value(value_array, i);
+                if (json_validate(temp_schema_value, temp_value) != JSONSuccess) {
+                    return JSONFailure;
+                }
+            }
+            return JSONSuccess;
+        case JSONObject:
+            schema_object = json_value_get_object(schema);
+            value_object = json_value_get_object(value);
+            count = json_object_get_count(schema_object);
+            if (count == 0) {
+                return JSONSuccess; /* Empty object allows all objects */
+            } else if (json_object_get_count(value_object) < count) {
+                return JSONFailure; /* Tested object mustn't have less name-value pairs than schema */
+            }
+            for (i = 0; i < count; i++) {
+                key = json_object_get_name(schema_object, i);
+                temp_schema_value = json_object_get_value(schema_object, key);
+                temp_value = json_object_get_value(value_object, key);
+                if (temp_value == NULL) {
+                    return JSONFailure;
+                }
+                if (json_validate(temp_schema_value, temp_value) != JSONSuccess) {
+                    return JSONFailure;
+                }
+            }
+            return JSONSuccess;
+        case JSONString: case JSONNumber: case JSONBoolean: case JSONNull:
+            return JSONSuccess; /* equality already tested before switch */
+        case JSONError: default:
+            return JSONFailure;
+    }
+}
+
+int json_value_equals(const JSON_Value *a, const JSON_Value *b) {
+    JSON_Object *a_object = NULL, *b_object = NULL;
+    JSON_Array *a_array = NULL, *b_array = NULL;
+    const JSON_String *a_string = NULL, *b_string = NULL;
+    const char *key = NULL;
+    size_t a_count = 0, b_count = 0, i = 0;
+    JSON_Value_Type a_type, b_type;
+    a_type = json_value_get_type(a);
+    b_type = json_value_get_type(b);
+    if (a_type != b_type) {
+        return PARSON_FALSE;
+    }
+    switch (a_type) {
+        case JSONArray:
+            a_array = json_value_get_array(a);
+            b_array = json_value_get_array(b);
+            a_count = json_array_get_count(a_array);
+            b_count = json_array_get_count(b_array);
+            if (a_count != b_count) {
+                return PARSON_FALSE;
+            }
+            for (i = 0; i < a_count; i++) {
+                if (!json_value_equals(json_array_get_value(a_array, i),
+                                       json_array_get_value(b_array, i))) {
+                    return PARSON_FALSE;
+                }
+            }
+            return PARSON_TRUE;
+        case JSONObject:
+            a_object = json_value_get_object(a);
+            b_object = json_value_get_object(b);
+            a_count = json_object_get_count(a_object);
+            b_count = json_object_get_count(b_object);
+            if (a_count != b_count) {
+                return PARSON_FALSE;
+            }
+            for (i = 0; i < a_count; i++) {
+                key = json_object_get_name(a_object, i);
+                if (!json_value_equals(json_object_get_value(a_object, key),
+                                       json_object_get_value(b_object, key))) {
+                    return PARSON_FALSE;
+                }
+            }
+            return PARSON_TRUE;
+        case JSONString:
+            a_string = json_value_get_string_desc(a);
+            b_string = json_value_get_string_desc(b);
+            if (a_string == NULL || b_string == NULL) {
+                return PARSON_FALSE; /* shouldn't happen */
+            }
+            return a_string->length == b_string->length &&
+                   memcmp(a_string->chars, b_string->chars, a_string->length) == 0;
+        case JSONBoolean:
+            return json_value_get_boolean(a) == json_value_get_boolean(b);
+        case JSONNumber:
+            return fabs(json_value_get_number(a) - json_value_get_number(b)) < 0.000001; /* EPSILON */
+        case JSONError:
+            return PARSON_TRUE;
+        case JSONNull:
+            return PARSON_TRUE;
+        default:
+            return PARSON_TRUE;
+    }
+}
+
+JSON_Value_Type json_type(const JSON_Value *value) {
+    return json_value_get_type(value);
+}
+
+JSON_Object * json_object (const JSON_Value *value) {
+    return json_value_get_object(value);
+}
+
+JSON_Array * json_array(const JSON_Value *value) {
+    return json_value_get_array(value);
+}
+
+const char * json_string(const JSON_Value *value) {
+    return json_value_get_string(value);
+}
+
+size_t json_string_len(const JSON_Value *value) {
+    return json_value_get_string_len(value);
+}
+
+double json_number(const JSON_Value *value) {
+    return json_value_get_number(value);
+}
+
+int json_boolean(const JSON_Value *value) {
+    return json_value_get_boolean(value);
+}
+
+void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun) {
+    parson_malloc = malloc_fun;
+    parson_free = free_fun;
+}
+
+void json_set_escape_slashes(int escape_slashes) {
+    parson_escape_slashes = escape_slashes;
+}
diff --git a/lib/parson.h b/lib/parson.h
new file mode 100644
index 0000000..beeca4c
--- /dev/null
+++ b/lib/parson.h
@@ -0,0 +1,256 @@
+/*
+ SPDX-License-Identifier: MIT
+
+ Parson 1.2.1 ( http://kgabis.github.com/parson/ )
+ Copyright (c) 2012 - 2021 Krzysztof Gabis
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#ifndef parson_parson_h
+#define parson_parson_h
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define PARSON_VERSION_MAJOR 1
+#define PARSON_VERSION_MINOR 2
+#define PARSON_VERSION_PATCH 1
+
+#define PARSON_VERSION_STRING "1.2.1"
+
+#include <stddef.h>   /* size_t */
+
+/* Types and enums */
+typedef struct json_object_t JSON_Object;
+typedef struct json_array_t  JSON_Array;
+typedef struct json_value_t  JSON_Value;
+
+enum json_value_type {
+    JSONError   = -1,
+    JSONNull    = 1,
+    JSONString  = 2,
+    JSONNumber  = 3,
+    JSONObject  = 4,
+    JSONArray   = 5,
+    JSONBoolean = 6
+};
+typedef int JSON_Value_Type;
+
+enum json_result_t {
+    JSONSuccess = 0,
+    JSONFailure = -1
+};
+typedef int JSON_Status;
+
+typedef void * (*JSON_Malloc_Function)(size_t);
+typedef void   (*JSON_Free_Function)(void *);
+
+/* Call only once, before calling any other function from parson API. If not called, malloc and free
+   from stdlib will be used for all allocations */
+void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun);
+
+/* Sets if slashes should be escaped or not when serializing JSON. By default slashes are escaped.
+ This function sets a global setting and is not thread safe. */
+void json_set_escape_slashes(int escape_slashes);
+
+/* Parses first JSON value in a file, returns NULL in case of error */
+JSON_Value * json_parse_file(const char *filename);
+
+/* Parses first JSON value in a file and ignores comments (/ * * / and //),
+   returns NULL in case of error */
+JSON_Value * json_parse_file_with_comments(const char *filename);
+
+/*  Parses first JSON value in a string, returns NULL in case of error */
+JSON_Value * json_parse_string(const char *string);
+
+/*  Parses first JSON value in a string and ignores comments (/ * * / and //),
+    returns NULL in case of error */
+JSON_Value * json_parse_string_with_comments(const char *string);
+
+/* Serialization */
+size_t      json_serialization_size(const JSON_Value *value); /* returns 0 on fail */
+JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes);
+JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename);
+char *      json_serialize_to_string(const JSON_Value *value);
+
+/* Pretty serialization */
+size_t      json_serialization_size_pretty(const JSON_Value *value); /* returns 0 on fail */
+JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes);
+JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename);
+char *      json_serialize_to_string_pretty(const JSON_Value *value);
+
+void        json_free_serialized_string(char *string); /* frees string from json_serialize_to_string and json_serialize_to_string_pretty */
+
+/* Comparing */
+int  json_value_equals(const JSON_Value *a, const JSON_Value *b);
+
+/* Validation
+   This is *NOT* JSON Schema. It validates json by checking if object have identically
+   named fields with matching types.
+   For example schema {"name":"", "age":0} will validate
+   {"name":"Joe", "age":25} and {"name":"Joe", "age":25, "gender":"m"},
+   but not {"name":"Joe"} or {"name":"Joe", "age":"Cucumber"}.
+   In case of arrays, only first value in schema is checked against all values in tested array.
+   Empty objects ({}) validate all objects, empty arrays ([]) validate all arrays,
+   null validates values of every type.
+ */
+JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value);
+
+/*
+ * JSON Object
+ */
+JSON_Value  * json_object_get_value  (const JSON_Object *object, const char *name);
+const char  * json_object_get_string (const JSON_Object *object, const char *name);
+size_t        json_object_get_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */
+JSON_Object * json_object_get_object (const JSON_Object *object, const char *name);
+JSON_Array  * json_object_get_array  (const JSON_Object *object, const char *name);
+double        json_object_get_number (const JSON_Object *object, const char *name); /* returns 0 on fail */
+int           json_object_get_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */
+
+/* dotget functions enable addressing values with dot notation in nested objects,
+ just like in structs or c++/java/c# objects (e.g. objectA.objectB.value).
+ Because valid names in JSON can contain dots, some values may be inaccessible
+ this way. */
+JSON_Value  * json_object_dotget_value  (const JSON_Object *object, const char *name);
+const char  * json_object_dotget_string (const JSON_Object *object, const char *name);
+size_t        json_object_dotget_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */
+JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name);
+JSON_Array  * json_object_dotget_array  (const JSON_Object *object, const char *name);
+double        json_object_dotget_number (const JSON_Object *object, const char *name); /* returns 0 on fail */
+int           json_object_dotget_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */
+
+/* Functions to get available names */
+size_t        json_object_get_count   (const JSON_Object *object);
+const char  * json_object_get_name    (const JSON_Object *object, size_t index);
+JSON_Value  * json_object_get_value_at(const JSON_Object *object, size_t index);
+JSON_Value  * json_object_get_wrapping_value(const JSON_Object *object);
+
+/* Functions to check if object has a value with a specific name. Returned value is 1 if object has
+ * a value and 0 if it doesn't. dothas functions behave exactly like dotget functions. */
+int json_object_has_value        (const JSON_Object *object, const char *name);
+int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type);
+
+int json_object_dothas_value        (const JSON_Object *object, const char *name);
+int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type);
+
+/* Creates new name-value pair or frees and replaces old value with a new one.
+ * json_object_set_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value);
+JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string);
+JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len);  /* length shouldn't include last null character */
+JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number);
+JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean);
+JSON_Status json_object_set_null(JSON_Object *object, const char *name);
+
+/* Works like dotget functions, but creates whole hierarchy if necessary.
+ * json_object_dotset_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value);
+JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string);
+JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len); /* length shouldn't include last null character */
+JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number);
+JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean);
+JSON_Status json_object_dotset_null(JSON_Object *object, const char *name);
+
+/* Frees and removes name-value pair */
+JSON_Status json_object_remove(JSON_Object *object, const char *name);
+
+/* Works like dotget function, but removes name-value pair only on exact match. */
+JSON_Status json_object_dotremove(JSON_Object *object, const char *key);
+
+/* Removes all name-value pairs in object */
+JSON_Status json_object_clear(JSON_Object *object);
+
+/*
+ *JSON Array
+ */
+JSON_Value  * json_array_get_value  (const JSON_Array *array, size_t index);
+const char  * json_array_get_string (const JSON_Array *array, size_t index);
+size_t        json_array_get_string_len(const JSON_Array *array, size_t index); /* doesn't account for last null character */
+JSON_Object * json_array_get_object (const JSON_Array *array, size_t index);
+JSON_Array  * json_array_get_array  (const JSON_Array *array, size_t index);
+double        json_array_get_number (const JSON_Array *array, size_t index); /* returns 0 on fail */
+int           json_array_get_boolean(const JSON_Array *array, size_t index); /* returns -1 on fail */
+size_t        json_array_get_count  (const JSON_Array *array);
+JSON_Value  * json_array_get_wrapping_value(const JSON_Array *array);
+
+/* Frees and removes value at given index, does nothing and returns JSONFailure if index doesn't exist.
+ * Order of values in array may change during execution.  */
+JSON_Status json_array_remove(JSON_Array *array, size_t i);
+
+/* Frees and removes from array value at given index and replaces it with given one.
+ * Does nothing and returns JSONFailure if index doesn't exist.
+ * json_array_replace_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_array_replace_value(JSON_Array *array, size_t i, JSON_Value *value);
+JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string);
+JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len); /* length shouldn't include last null character */
+JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number);
+JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean);
+JSON_Status json_array_replace_null(JSON_Array *array, size_t i);
+
+/* Frees and removes all values from array */
+JSON_Status json_array_clear(JSON_Array *array);
+
+/* Appends new value at the end of array.
+ * json_array_append_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value);
+JSON_Status json_array_append_string(JSON_Array *array, const char *string);
+JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len); /* length shouldn't include last null character */
+JSON_Status json_array_append_number(JSON_Array *array, double number);
+JSON_Status json_array_append_boolean(JSON_Array *array, int boolean);
+JSON_Status json_array_append_null(JSON_Array *array);
+
+/*
+ *JSON Value
+ */
+JSON_Value * json_value_init_object (void);
+JSON_Value * json_value_init_array  (void);
+JSON_Value * json_value_init_string (const char *string); /* copies passed string */
+JSON_Value * json_value_init_string_with_len(const char *string, size_t length); /* copies passed string, length shouldn't include last null character */
+JSON_Value * json_value_init_number (double number);
+JSON_Value * json_value_init_boolean(int boolean);
+JSON_Value * json_value_init_null   (void);
+JSON_Value * json_value_deep_copy   (const JSON_Value *value);
+void         json_value_free        (JSON_Value *value);
+
+JSON_Value_Type json_value_get_type   (const JSON_Value *value);
+JSON_Object *   json_value_get_object (const JSON_Value *value);
+JSON_Array  *   json_value_get_array  (const JSON_Value *value);
+const char  *   json_value_get_string (const JSON_Value *value);
+size_t          json_value_get_string_len(const JSON_Value *value); /* doesn't account for last null character */
+double          json_value_get_number (const JSON_Value *value);
+int             json_value_get_boolean(const JSON_Value *value);
+JSON_Value  *   json_value_get_parent (const JSON_Value *value);
+
+/* Same as above, but shorter */
+JSON_Value_Type json_type   (const JSON_Value *value);
+JSON_Object *   json_object (const JSON_Value *value);
+JSON_Array  *   json_array  (const JSON_Value *value);
+const char  *   json_string (const JSON_Value *value);
+size_t          json_string_len(const JSON_Value *value); /* doesn't account for last null character */
+double          json_number (const JSON_Value *value);
+int             json_boolean(const JSON_Value *value);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/login_duo/login_duo.c b/login_duo/login_duo.c
index a54e4a2..68c8392 100644
--- a/login_duo/login_duo.c
+++ b/login_duo/login_duo.c
@@ -167,7 +167,7 @@
         }
         return (EXIT_FAILURE);
     }
-    
+
 
 #ifdef OPENSSL_FIPS
     /*
@@ -179,12 +179,12 @@
      * example, when integrating directly with the OpenSSL FIPS Object Module).
      */
     if(!FIPS_mode_set(cfg.fips_mode)) {
-        /* The smallest size buff can be according to the openssl docs */ 
+        /* The smallest size buff can be according to the openssl docs */
         char buff[256];
         int error = ERR_get_error();
         ERR_error_string_n(error, buff, sizeof(buff));
         duo_syslog(LOG_ERR, "Unable to start fips_mode: %s", buff);
-	 
+
        return (EXIT_FAILURE);
     }
 #else
@@ -195,6 +195,13 @@
 #endif
 
     prompts = cfg.prompts;
+
+    /* Detect non-interactive sessions */
+    if ((p = getenv("SSH_ORIGINAL_COMMAND")) != NULL ||
+        !isatty(STDIN_FILENO)) {
+        headless = 1;
+    }
+
     /* Check group membership. */
     matched = duo_check_groups(pw, cfg.groups, cfg.groups_cnt);
     if (matched == -1) {
@@ -202,6 +209,10 @@
         return (EXIT_FAILURE);
     } else if (matched == 0) {
         duo_syslog(LOG_INFO, "User %s bypassed Duo 2FA due to user's UNIX group", duouser);
+        /* Print out /etc/motd file if user is bypassed due to their group */
+        if (cfg.motd && !headless) {
+            _print_motd();
+        }
         close_config(&cfg);
         return (EXIT_SUCCESS);
     }
@@ -249,13 +260,11 @@
     }
 
     /* Special handling for non-interactive sessions */
-    if ((p = getenv("SSH_ORIGINAL_COMMAND")) != NULL ||
-        !isatty(STDIN_FILENO)) {
+    if (headless) {
         /* Try to support automatic one-shot login */
         duo_set_conv_funcs(duo, NULL, NULL, NULL);
         flags = (DUO_FLAG_SYNC|DUO_FLAG_AUTO);
         prompts = 1;
-        headless = 1;
     } else if (cfg.autopush) { /* Special handling for autopush */
         duo_set_conv_funcs(duo, NULL, __autopush_status_fn, NULL);
         flags = (DUO_FLAG_SYNC|DUO_FLAG_AUTO);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 1d22322..842025a 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,9 +1,7 @@
-TESTS_ENVIRONMENT = env BUILDDIR=$(abs_top_builddir) $(PYTHON) $(top_srcdir)/tests/cram.py
+TESTS_ENVIRONMENT = env BUILDDIR=$(abs_top_builddir) $(PYTHON)
 
-# Preserve ordering; login_duo-0.t does some setup
-TESTS = login_duo-0.t login_duo-1.t login_duo-2.t login_duo-3.t login_duo-4.t login_duo-5.t login_duo-6.t login_duo-7.t
-TESTS += groups-0.t groups-1.t groups-2.t mocklogin_duo-0.t mocklogin_duo-1.t util-0.t test_crypto-0.t
-PAM_TESTS = pam_duo-0.t pam_duo-1.t pam_duo-2.t pam_duo-3.t pam_duo-4.t pam_duo-5.t pam_duo-6.t pam_duo-7.t
+TESTS = test_login_duo.py test_crypto.py test_duo_split_at.py
+PAM_TESTS = test_pam_duo.py
 
 check_LTLIBRARIES = libgroups_preload.la
 libgroups_preload_la_SOURCES = groups_preload.c
@@ -24,4 +22,4 @@
 testpam_LDADD = -lpam
 endif
 
-EXTRA_DIST = bson/codec.py bson/__init__.py certs confs cram.py fips_scanner.sh is_fips_supported.sh groups.py login_duo.py mockduo.py mocklogin_duo.py paths.py pexpect.py testpam.py $(TESTS) $(PAM_TESTS)
+EXTRA_DIST = certs confs cram.py fips_scanner.sh is_fips_supported.sh groups.py login_duo.py mockduo.py mocklogin_duo.py paths.py pexpect.py testpam.py $(TESTS) $(PAM_TESTS) common_suites.py mockduo_context.py config.py
diff --git a/tests/bson/__init__.py b/tests/bson/__init__.py
deleted file mode 100644
index 4965aaa..0000000
--- a/tests/bson/__init__.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/python -OOOO
-# vim: set fileencoding=utf8 shiftwidth=4 tabstop=4 textwidth=80 foldmethod=marker :
-# Copyright (c) 2010, Kou Man Tong. All rights reserved.
-# For licensing, see LICENSE file included in the package.
-"""
-BSON serialization and deserialization logic.
-Specifications taken from: http://bsonspec.org/#/specification
-The following types are unsupported, because for data exchange purposes, they're
-over-engineered:
-	0x06 (Undefined)
-	0x07 (ObjectId)
-	0x09 (UTC datetime - Sorry, but Python's datetime module sucks.
-		datetime.now() has no timezone? Seriously?! Simple timestamps will save
-		you a lot of trouble)
-	0x0b (Regex - Exactly which flavor do you want? Better let higher level
-		programmers make that decision.)
-	0x0c (DBPointer)
-	0x0d (JavaScript code)
-	0x0e (Symbol)
-	0x0f (JS w/ scope)
-	0x11 (MongoDB-specific timestamp)
-
-For binaries, only the default 0x0 type is supported.
-"""
-
-from codec import *
-__all__ = ["loads", "dumps"]
-
-# {{{ Public API
-def dumps(obj):
-	"""
-	Given a dict, outputs a BSON string.
-	"""
-	return encode_document(obj)
-
-def loads(data):
-	"""
-	Given a BSON string, outputs a dict.
-	"""
-	return decode_document(data, 0)[1]
-# }}}
diff --git a/tests/bson/codec.py b/tests/bson/codec.py
deleted file mode 100644
index 7253229..0000000
--- a/tests/bson/codec.py
+++ /dev/null
@@ -1,184 +0,0 @@
-#!/usr/bin/python -OOOO
-# vim: set fileencoding=utf8 shiftwidth=4 tabstop=4 textwidth=80 foldmethod=marker :
-# Copyright (c) 2010, Kou Man Tong. All rights reserved.
-# For licensing, see LICENSE file included in the package.
-"""
-Base codec functions for bson.
-"""
-import struct
-import cStringIO
-
-# {{{ Private Logic
-def encode_string(value):
-	value = value.encode("utf8")
-	length = len(value)
-	return struct.pack("<i%dsb" % (length,), length + 1, value, 0)
-
-def decode_string(data, base):
-	length = struct.unpack("<i", data[base:base + 4])[0]
-	value = data[base + 4: base + 4 + length - 1]
-	value = value.decode("utf8")
-	return (base + 4 + length, value)
-
-def encode_cstring(value):
-	if isinstance(value, unicode):
-		value = value.encode("utf8")
-	return value + "\x00"
-
-def decode_cstring(data, base):
-	buf = cStringIO.StringIO()
-	length = 0
-	for character in data[base:]:
-		length += 1
-		if character == "\x00":
-			break
-		buf.write(character)
-	return (base + length, buf.getvalue().decode("utf8"))
-
-def encode_binary(value):
-	length = len(value)
-	return struct.pack("<ib", length, 0) + value
-
-def decode_binary(data, base):
-	length, binary_type = struct.unpack("<ib", data[base:base + 5])
-	return (base + 5 + length, data[base + 5:base + 5 + length])
-
-def encode_double(value):
-	return struct.pack("<d", value)
-
-def decode_double(data, base):
-	return (base + 8, struct.unpack("<d", data[base: base + 8])[0])
-
-
-ELEMENT_TYPES = {
-		0x01 : "double",
-		0x02 : "string",
-		0x03 : "document",
-		0x04 : "array",
-		0x05 : "binary",
-		0x08 : "boolean",
-		0x0A : "none",
-		0x10 : "int32",
-		0x12 : "int64"
-	}
-
-def encode_double_element(name, value):
-	return "\x01" + encode_cstring(name) + encode_double(value)
-
-def decode_double_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	base, value = decode_double(data, base)
-	return (base, name, value)
-
-def encode_string_element(name, value):
-	return "\x02" + encode_cstring(name) + encode_string(value)
-
-def decode_string_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	base, value = decode_string(data, base)
-	return (base, name, value)
-
-def encode_document(obj):
-	buf = cStringIO.StringIO()
-	for name in obj:
-		value = obj[name]
-		if isinstance(value, float):
-			buf.write(encode_double_element(name, value))
-		elif isinstance(value, unicode):
-			buf.write(encode_string_element(name, value))
-		elif isinstance(value, dict):
-			buf.write(encode_document_element(name, value))
-		elif isinstance(value, list) or isinstance(value, tuple):
-			buf.write(encode_array_element(name, value))
-		elif isinstance(value, str):
-			buf.write(encode_string_element(name, value))
-		elif isinstance(value, bool):
-			buf.write(encode_boolean_element(name, value))
-		elif value is None:
-			buf.write(encode_none_element(name, value))
-		elif isinstance(value, int):
-			buf.write(encode_int32_element(name, value))
-		elif isinstance(value, long):
-			buf.write(encode_int64_element(name, value))
-	e_list = buf.getvalue()
-	e_list_length = len(e_list)
-	return struct.pack("<i%dsb" % (e_list_length,), e_list_length + 4 + 1,
-			e_list, 0)
-
-def decode_element(data, base):
-	element_type = struct.unpack("<b", data[base:base + 1])[0]
-	element_description = ELEMENT_TYPES[element_type]
-	decode_func = globals()["decode_" + element_description + "_element"]
-	return decode_func(data, base)
-
-def decode_document(data, base):
-	length = struct.unpack("<i", data[base:base + 4])[0]
-	end_point = base + length
-	base += 4
-	retval = {}
-	while base < end_point - 1:
-		base, name, value = decode_element(data, base)
-		retval[name] = value
-	return (end_point, retval)
-
-def encode_document_element(name, value):
-	return "\x03" + encode_cstring(name) + encode_document(value)
-
-def decode_document_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	base, value = decode_document(data, base)
-	return (base, name, value)
-
-def encode_array_element(name, value):
-	return "\x04" + encode_cstring(name) + \
-		encode_document(dict([(str(i), value[i]) for i in xrange(0, len(value))]))
-
-def decode_array_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	base, value = decode_document(data, base)
-	keys = value.keys()
-	keys.sort()
-	retval = []
-	for i in keys:
-		retval.append(value[i])
-	return (base, name, retval)
-
-def encode_binary_element(name, value):
-	return "\x05" + encode_cstring(name) + encode_binary(value)
-
-def decode_binary_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	base, value = decode_binary(data, base)
-	return (base, name, value)
-
-def encode_boolean_element(name, value):
-	return "\x08" + encode_cstring(name) + struct.pack("<b", value)
-
-def decode_boolean_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	value = not not struct.unpack("<b", data[base:base + 1])[0]
-	return (base + 1, name, value)
-
-def encode_none_element(name, value):
-	return "\x0a" + encode_cstring(name)
-
-def decode_none_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	return (base, name, None)
-
-def encode_int32_element(name, value):
-	return "\x10" + encode_cstring(name) + struct.pack("<i", value)
-
-def decode_int32_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	value = struct.unpack("<i", data[base:base + 4])[0]
-	return (base + 4, name, value)
-
-def encode_int64_element(name, value):
-	return "\x12" + encode_cstring(name) + struct.pack("<q", value)
-
-def decode_int64_element(data, base):
-	base, name = decode_cstring(data, base + 1)
-	value = struct.unpack("<q", data[base:base + 8])[0]
-	return (base + 8, name, value)
-# }}}
diff --git a/tests/common_suites.py b/tests/common_suites.py
new file mode 100644
index 0000000..47a03f1
--- /dev/null
+++ b/tests/common_suites.py
@@ -0,0 +1,703 @@
+import os
+import subprocess
+import unittest
+
+import pexpect
+from config import (
+    BAD_CORRUPT_CONF,
+    BAD_CORRUPT_SECURE_CONF,
+    BAD_EMPTY_CONF,
+    BAD_HEADER_CONF,
+    BAD_MISSING_VALUES_CONF,
+    MOCKDUO_AUTOPUSH,
+    MOCKDUO_AUTOPUSH_SECURE,
+    MOCKDUO_BADKEYS,
+    MOCKDUO_BADKEYS_FAILSECURE,
+    MOCKDUO_CONF,
+    MOCKDUO_EXTRA_SPACE,
+    MOCKDUO_FAILSECURE,
+    MOCKDUO_FAILSECURE_BAD_CERT,
+    MOCKDUO_FALLBACK,
+    MOCKDUO_FIPS,
+    MOCKDUO_NOVERIFY,
+    MOCKDUO_PROMPTS_1,
+    MOCKDUO_PROXY,
+    TESTCONF,
+    TempConfig,
+)
+from mockduo_context import NORMAL_CERT, SELFSIGNED_CERT, WRONGHOST_CERT, MockDuo
+
+TESTDIR = os.path.realpath(os.path.dirname(__file__))
+
+def fips_available():
+    returncode = subprocess.call(
+        [os.path.join(TESTDIR, "is_fips_supported.sh")],
+        stdout=subprocess.PIPE,
+    )
+    return returncode == 0
+
+
+class CommonTestCase(unittest.TestCase):
+    def call_binary(self, *args, **kwargs):
+        raise NotImplementedError
+
+
+# suite class just prevents the inner test cases from being run
+class CommonSuites:
+    class Configuration(CommonTestCase):
+        def test_missing_config_file(self):
+            """Missing conf file"""
+            result = self.call_binary(["-d", "-c", "/nonexistent", "true"])
+            self.assertRegex(
+                result["stderr"][0],
+                r"Couldn't open /nonexistent: No such file or directory",
+            )
+
+        def test_bad_permissions_on_conf_file(self):
+            """Bad permissions on conf file"""
+            with TempConfig(TESTCONF) as temp:
+                os.chmod(temp.name, 0o644)
+                result = self.call_binary(["-d", "-c", temp.name, "true"])
+                self.assertRegex(
+                    result["stderr"][0],
+                    "{name} must be readable only by user '.*'".format(name=temp.name),
+                )
+
+        def test_bad_configuration_files(self):
+            """Bad configuration files"""
+            for config in [
+                BAD_EMPTY_CONF,
+                BAD_HEADER_CONF,
+                BAD_MISSING_VALUES_CONF,
+            ]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(["-d", "-c", temp.name, "true"])
+                    self.assertRegex(
+                        result["stderr"][0],
+                        "Missing host, ikey, or skey in {name}".format(name=temp.name),
+                    )
+
+        def test_corrupt_configuration_file_failsafe(self):
+            with TempConfig(BAD_CORRUPT_CONF) as temp:
+                result = self.call_binary(["-d", "-c", temp.name, "true"])
+                self.assertRegex(
+                    result["stderr"][0], "Parse error in {name}".format(name=temp.name)
+                )
+                self.assertEqual(result["returncode"], 0)
+
+        def test_corrupt_configuration_file_failsecure(self):
+            with TempConfig(BAD_CORRUPT_SECURE_CONF) as temp:
+                result = self.call_binary(["-d", "-c", temp.name, "true"])
+                self.assertRegex(
+                    result["stderr"][0], "Parse error in {name}".format(name=temp.name)
+                )
+                self.assertEqual(result["returncode"], 1)
+
+    class DuoDown(CommonTestCase):
+        def test_mockduo_down(self):
+            """mockduo down"""
+            with TempConfig(TESTCONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Failsafe Duo login for 'whatever'.*: Couldn't connect to .* Failed to connect",
+                )
+
+        def test_down_fail_secure(self):
+            """Test that binary fails secure if Duo is down"""
+            # Weirdly this requires a bad cert. I think this may have been caused by some
+            # file path confusion in the original cram test
+            with TempConfig(MOCKDUO_FAILSECURE_BAD_CERT) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0], r"Couldn't open Duo API handle for .*"
+                )
+                self.assertEqual(result["returncode"], 1)
+
+    class DuoSelfSignedCert(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(SELFSIGNED_CERT) as p:
+                return super(CommonSuites.DuoSelfSignedCert, self).run(result)
+
+        def test_invalid_cert(self):
+            """Invalid cert"""
+            for config in [MOCKDUO_CONF, MOCKDUO_FAILSECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"{failmode} Duo login for .* Couldn't connect to .*: certificate verify failed".format(
+                            failmode=config.failmode_as_prefix()
+                        ),
+                    )
+                    if config.get("failmode", None) == "secure":
+                        self.assertEqual(result["returncode"], 1)
+
+        def test_self_signed_with_noverify(self):
+            """With noverify"""
+            with TempConfig(MOCKDUO_NOVERIFY) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+    class DuoBadCN(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(WRONGHOST_CERT):
+                return super(CommonSuites.DuoBadCN, self).run(result)
+
+        def test_wrong_hostname(self):
+            """Wrong hostname"""
+            for config in [MOCKDUO_CONF, MOCKDUO_FAILSECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"{failmode} Duo login for .*: Couldn't connect to .*: Certificate name validation failed".format(
+                            failmode=config.failmode_as_prefix()
+                        ),
+                    )
+                    if config.get("failmode", None) == "secure":
+                        self.assertEqual(result["returncode"], 1)
+
+        def test_failsecure(self):
+            """Test wrong hostname with fail secure"""
+            with TempConfig(MOCKDUO_FAILSECURE) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Failsecure Duo login for .*: Couldn't connect to .*: Certificate name validation failed",
+                )
+
+        def test_noverify(self):
+            """Test wrong hostname with noverify"""
+            with TempConfig(MOCKDUO_NOVERIFY) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+    class WithValidCert(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.WithValidCert, self).run(result)
+
+        def test_http_server_abort_errors(self):
+            for code in ["400", "402", "403", "404"]:
+                for config in [MOCKDUO_CONF, MOCKDUO_FAILSECURE, MOCKDUO_AUTOPUSH]:
+                    with TempConfig(config) as temp:
+                        result = self.call_binary(
+                            ["-d", "-c", temp.name, "-f", code, "true"]
+                        )
+                        self.assertRegex(
+                            result["stderr"][0],
+                            r"Aborted Duo login for '{code}'.*: HTTP {code}".format(
+                                code=code
+                            ),
+                        )
+
+        def test_http_server_failmode_errors(self):
+            for code in ["500", "501", "502", "503", "504"]:
+                for config in [MOCKDUO_CONF, MOCKDUO_AUTOPUSH, MOCKDUO_FAILSECURE]:
+                    with TempConfig(config) as temp:
+                        result = self.call_binary(
+                            ["-d", "-c", temp.name, "-f", code, "true"]
+                        )
+                        self.assertRegex(
+                            result["stderr"][0],
+                            r"{failmode} Duo login for '{code}'.*: HTTP {code}".format(
+                                failmode=config.failmode_as_prefix(), code=code
+                            ),
+                        )
+
+        def test_http_server_invalid_credentials_error(self):
+            code = "401"
+            for config in [MOCKDUO_CONF, MOCKDUO_AUTOPUSH, MOCKDUO_FAILSECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", code, "true"]
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"{failmode} Duo login for '{code}'.*: Invalid ikey or skey".format(
+                            failmode=config.failmode_as_prefix(), code=code
+                        ),
+                    )
+
+        def test_with_bad_keys(self):
+            for config in [MOCKDUO_BADKEYS, MOCKDUO_BADKEYS_FAILSECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", "whatever", "true"]
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"{failmode} Duo login for .*: Invalid ikey or skey".format(
+                            failmode=config.failmode_as_prefix()
+                        ),
+                    )
+                    if config.get("failmode", None) == "secure":
+                        self.assertEqual(result["returncode"], 1)
+
+    class PreauthStates(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.PreauthStates, self).run(result)
+
+        def check_preauth_state(self, user, message, prefix=None):
+            for config in [MOCKDUO_CONF, MOCKDUO_FAILSECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", user, "true"]
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"{prefix} Duo login for '{user}'.*{message}".format(
+                            prefix=prefix if prefix else config.failmode_as_prefix(),
+                            user=user,
+                            message=message,
+                        ),
+                    )
+
+        def test_preauth_ok_missing_response(self):
+            self.check_preauth_state(
+                "preauth-ok-missing_response", "JSON missing valid 'response'"
+            )
+
+        def test_preauth_fail_missing_response(self):
+            self.check_preauth_state(
+                "preauth-fail-missing_response", "JSON missing valid 'code'"
+            )
+
+        def test_preauth_bad_stat(self):
+            self.check_preauth_state("preauth-bad-stat", "")
+
+        def test_preauth_fail(self):
+            self.check_preauth_state(
+                "preauth-fail", "1000: Pre-authentication failed", prefix="Failed"
+            )
+
+        def test_preauth_deny(self):
+            self.check_preauth_state("preauth-deny", "preauth-denied", prefix="Aborted")
+
+        def test_preauth_allow(self):
+            self.check_preauth_state(
+                "preauth-allow", "preauth-allowed", prefix="Skipped"
+            )
+
+        def test_preauth_allow_bad_response(self):
+            self.check_preauth_state(
+                "preauth-allow-bad_response", "JSON missing valid 'status'"
+            )
+
+    class Hosts(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.Hosts, self).run(result)
+
+        def check_host_reporting(self, host):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "-h", host, "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow' from {host}: preauth-allowed".format(
+                        host=host
+                    ),
+                )
+
+        def test_host_names(self):
+            for host in [
+                "1.2.3.4",
+                "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:AAA.BBB.CCC.DDD",
+                "nowhere",
+                '"%s"',
+                '"!@#$%^&*()_+<>{}|;\'"',
+            ]:
+                self.check_host_reporting(host)
+
+    class HTTPProxy(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.HTTPProxy, self).run(result)
+
+        def test_with_no_http_proxy(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={},
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+        def test_with_broadcast_proxy(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={"http_proxy": "0.0.0.0"},
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+            with TempConfig(MOCKDUO_PROXY) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={"http_proxy": "0.0.0.0"},
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Failsafe Duo login for .*: Couldn't connect to localhost:4443: Failed to connect",
+                )
+
+    class GetHostname(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.GetHostname, self).run(result)
+
+        def test_getting_hostname(self):
+            config = MOCKDUO_CONF
+            with TempConfig(config) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "hostname", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Aborted Duo login for 'hostname': correct hostname",
+                )
+                if config.get("failmode", None) == "secure":
+                    self.assertEqual(result["returncode"], 1)
+
+    class FIPS(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.FIPS, self).run(result)
+
+        @unittest.skipIf(
+            fips_available() is False, reason="Fips is not supported on this platform"
+        )
+        def test_fips_login(self):
+            with TempConfig(MOCKDUO_FIPS) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    timeout=10,
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+        @unittest.skipIf(
+            fips_available() is True, reason="Fips is supported on this platform"
+        )
+        def test_fips_unavailable(self):
+            with TempConfig(MOCKDUO_FIPS) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    "FIPS mode flag specified, but OpenSSL not built with FIPS support. Failing the auth.",
+                )
+
+    class PreauthFailures(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.PreauthFailures, self).run(result)
+
+        def test_failmode_preauth_fail(self):
+            for config in [MOCKDUO_AUTOPUSH, MOCKDUO_AUTOPUSH_SECURE]:
+                with TempConfig(config) as temp:
+                    result = self.call_binary(
+                        ["-d", "-c", temp.name, "-f", "auth_timeout", "true"],
+                    )
+                    self.assertRegex(
+                        result["stderr"][0],
+                        r"Error in Duo login for 'auth_timeout': HTTP 500",
+                    )
+
+        def test_failopen_report(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "failopen", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Aborted Duo login for 'failopen': correct failmode",
+                )
+
+        def test_failclosed_report(self):
+            with TempConfig(MOCKDUO_FAILSECURE) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "failclosed", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Aborted Duo login for 'failclosed': correct failmode",
+                )
+
+        def test_enroll(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "enroll", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"User enrollment required",
+                )
+
+    class Env(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.Env, self).run(result)
+
+        def test_fallback_and_uid(self):
+            with TempConfig(MOCKDUO_FALLBACK) as temp:
+                result = self.call_binary(
+                    [
+                        "-d",
+                        "-c",
+                        temp.name,
+                        "-f",
+                        "preauth-allow",
+                        "-h",
+                        "BADHOST",
+                        "true",
+                    ],
+                    env={
+                        "FALLBACK": "1",
+                        "UID": "1001",
+                    },
+                    timeout=15,
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+        def test_ssh_connection_host(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={
+                        "SSH_CONNECTION": "1.2.3.4",
+                    },
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r" Skipped Duo login for 'preauth-allow'",
+                )
+
+        def test_configuration_with_extra_space(self):
+            with TempConfig(MOCKDUO_EXTRA_SPACE) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"]
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow'.*: preauth-allowed",
+                )
+
+    class Interactive(CommonTestCase):
+        PROMPT_REGEX = ".* or option \(1-4\): $"
+        PROMPT_TEXT = [
+            "Duo login for foobar",
+            "Choose or lose:",
+            "  1. Push 1",
+            "  2. Phone 1",
+            "  3. SMS 1 (deny)",
+            "  4. Phone 2 (deny)",
+            "Passcode or option (1-4): ",
+        ]
+
+        def assertOutputEqual(self, output, expected):
+            processed_output = [line for line in output.split("\r\n") if line != ""]
+            self.assertListEqual(processed_output, expected)
+
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.Interactive, self).run(result)
+
+        def three_failed_inputs(self, config):
+            with TempConfig(config) as temp:
+                process = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "foobar", "echo", "SUCCESS"],
+                )
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=10), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0), CommonSuites.Interactive.PROMPT_TEXT
+                )
+                process.sendline(b"123456")
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=1), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0),
+                    [
+                        "123456",
+                        "Invalid passcode, please try again.",
+                        "[4] Failed Duo login for 'foobar'",
+                    ]
+                    + CommonSuites.Interactive.PROMPT_TEXT,
+                )
+                process.sendline(b"wefawefgoiagj3rj")
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=1), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0),
+                    [
+                        "wefawefgoiagj3rj",
+                        "Invalid passcode, please try again.",
+                        "[4] Failed Duo login for 'foobar'",
+                    ]
+                    + CommonSuites.Interactive.PROMPT_TEXT,
+                )
+                process.sendline(b"A" * 500)
+                self.assertEqual(process.expect(pexpect.EOF), 0)
+                self.maxDiff = None
+                self.assertOutputEqual(
+                    process.before,
+                    [
+                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+                        "[3] Error in Duo login for 'foobar'",
+                    ],
+                )
+
+        def menu_options(self, config):
+            with TempConfig(config) as temp:
+                process = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "foobar", "true"],
+                )
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=10), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0), CommonSuites.Interactive.PROMPT_TEXT
+                )
+                process.sendline(b"3")
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=5), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0),
+                    [
+                        "3",
+                        "New SMS passcodes sent",
+                        "[4] Failed Duo login for 'foobar'",
+                    ]
+                    + CommonSuites.Interactive.PROMPT_TEXT,
+                )
+                process.sendline(b"4")
+                self.assertEqual(
+                    process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=5), 0
+                )
+                self.assertOutputEqual(
+                    process.match.group(0),
+                    [
+                        "4",
+                        "Dialing XXX-XXX-5678...",
+                        "Answered. Press '#' on your phone to log in.",
+                        "Authentication timed out.",
+                        "[4] Failed Duo login for 'foobar'",
+                    ]
+                    + CommonSuites.Interactive.PROMPT_TEXT,
+                )
+                process.sendline(b"1")
+                self.assertEqual(process.expect(pexpect.EOF), 0)
+                self.assertOutputEqual(
+                    process.before,
+                    [
+                        "1",
+                        "Pushed a login request to your phone.",
+                        "Success. Logging you in...",
+                        "[6] Successful Duo login for 'foobar'",
+                    ],
+                )
+
+        def menu_success(self, config):
+            with TempConfig(config) as temp:
+                process = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "foobar", "true"],
+                )
+                # This is here to prevent race conditions with character entry
+                process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=10)
+                process.sendline(b"2")
+                self.assertEqual(process.expect(pexpect.EOF), 0)
+                self.assertOutputEqual(
+                    process.before,
+                    [
+                        "2",
+                        "Dialing XXX-XXX-1234...",
+                        "Answered. Press '#' on your phone to log in.",
+                        "Success. Logging you in...",
+                        "[6] Successful Duo login for 'foobar'",
+                    ],
+                )
+
+        def test_three_failed_inputs(self):
+            self.three_failed_inputs(MOCKDUO_CONF)
+
+        @unittest.skipIf(
+            fips_available() is False, reason="Fips is not supported on this platform"
+        )
+        def test_fips_three_failed_inputs(self):
+            self.three_failed_inputs(MOCKDUO_FIPS)
+
+        def test_menu_options(self):
+            self.menu_options(MOCKDUO_CONF)
+            self.menu_success(MOCKDUO_CONF)
+
+        @unittest.skipIf(
+            fips_available() is False, reason="Fips is not supported on this platform"
+        )
+        def test_fips_menu_options(self):
+            self.menu_options(MOCKDUO_FIPS)
+            self.menu_success(MOCKDUO_FIPS)
+
+        def test_autopush_nomenu(self):
+            with TempConfig(MOCKDUO_AUTOPUSH) as temp:
+                process = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "foobar", "true"],
+                )
+                self.assertEqual(
+                    process.expect("Autopushing login request to phone...", timeout=10),
+                    0,
+                )
+
+    class InvalidBSON(CommonTestCase):
+        def run(self, result=None):
+            with MockDuo(NORMAL_CERT):
+                return super(CommonSuites.InvalidBSON, self).run(result)
+
+        def test_basic_invalid_json(self):
+            with TempConfig(MOCKDUO_CONF) as temp:
+                result = self.call_binary(
+                    ["-d", "-c", temp.name, "-f", "bad-json", "true"],
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"invalid JSON response",
+                )
diff --git a/tests/config.py b/tests/config.py
new file mode 100644
index 0000000..d530932
--- /dev/null
+++ b/tests/config.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+from tempfile import NamedTemporaryFile
+from textwrap import dedent
+
+
+class DuoUnixConfig(dict):
+    def __str__(self):
+        config = dedent(
+            """
+        [duo]\n
+        """
+        )
+        for key in self:
+            config += "{key} = {value}\n".format(key=key, value=self[key])
+        return config
+
+    def failmode_as_prefix(self):
+        failmode = self.get("failmode", "safe")
+        if failmode == "safe" or failmode is None:
+            return "Failsafe"
+        if failmode == "secure":
+            return "Failsecure"
+        else:
+            return "Unknown"
+
+
+# Referred to as "duo.conf" in cram testing
+TESTCONF = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+)
+
+# Referred to as "bad-corrupt.conf" in cram testing
+BAD_CORRUPT_CONF = """
+[duo]
+ikey =
+skey =
+host =
+q3598pjg9jajaf
+"""
+BAD_CORRUPT_SECURE_CONF = """
+[duo]
+failmode=secure
+ikey =
+skey =
+host =
+q3598pjg9jajaf
+"""
+
+
+# Referred to as "bad-header_only.conf" in cram testing
+BAD_HEADER_CONF = """
+[duo]
+"""
+
+# Referred to as "bad-empty.conf" in cram testing
+BAD_EMPTY_CONF = """
+"""
+
+# Referred to as "bad-missing_values.conf" in cram testing
+BAD_MISSING_VALUES_CONF = """
+[duo]
+ikey =
+skey =
+host =
+"""
+
+# Referred to as "mockduo_failsecure.conf"
+MOCKDUO_FAILSECURE = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    failmode="secure",
+)
+
+MOCKDUO_FAILSECURE_BAD_CERT = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="nonexistent/ca.pem",
+    failmode="secure",
+)
+
+# Referred to as "mockduo.conf"
+MOCKDUO_CONF = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+)
+
+# Referred to as "mockduo_noverify.conf"
+MOCKDUO_NOVERIFY = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    noverify="1",
+)
+
+# Referred to as "mockduo_autopush.conf"
+MOCKDUO_AUTOPUSH = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    autopush="yes",
+    prompts="1",
+)
+
+# Referred to as "mockduo_badkeys.conf"
+MOCKDUO_BADKEYS = DuoUnixConfig(
+    ikey="foo",
+    skey="bar",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+)
+
+MOCKDUO_BADKEYS_FAILSECURE = DuoUnixConfig(
+    ikey="foo",
+    skey="bar",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    failmode="secure",
+)
+
+# Referred to as "mockduo_fallback.conf" in cram tests
+MOCKDUO_FALLBACK = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    fallback_local_ip="yes",
+)
+
+# Referred to as "mockduo_proxy.conf" in cram tests
+MOCKDUO_PROXY = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    http_proxy="http://localhost:8888/",
+)
+
+MOCKDUO_FIPS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    dev_fips_mode="true",
+    cafile="certs/mockduo-ca.pem",
+    noverify="1",
+)
+
+# Referred to as "duo.conf" in the cram tests
+DUO_CONF = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+)
+
+# Referred to as "mockduo_prompts_1.conf" in cram tests
+MOCKDUO_PROMPTS_1 = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    autopush="yes",
+    prompts="1",
+)
+
+
+# Refered to as "mockduo_prompts_default.conf" in cram tests
+MOCKDUO_PROMPTS_DEFAULT = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    autopush="true",
+)
+
+# Referred to as "mockduo_autopush_secure.conf" in cram tests
+MOCKDUO_AUTOPUSH_SECURE = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    autopush="yes",
+    prompts="1",
+    failmode="secure",
+)
+
+MOCKDUO_GECOS_SEND_UNPARSED = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    send_gecos="true",
+)
+
+MOCKDUO_GECOS_DEPRECATED_PARSE_FLAG = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_parsed="true",
+)
+
+MOCKDUO_GECOS_DEFAULT_DELIM_6_POS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_username_pos="6",
+)
+
+MOCKDUO_GECOS_SLASH_DELIM_3_POS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_delim="/",
+    gecos_username_pos="3",
+)
+
+MOCKDUO_GECOS_LONG_DELIM = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_delim=",,",
+)
+
+MOCKDUO_GECOS_INVALID_DELIM_COLON = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_delim=":",
+)
+
+MOCKDUO_GECOS_INVALID_DELIM_PUNC = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_delim="a",
+)
+
+
+MOCKDUO_GECOS_INVALID_DELIM_WHITESPACE = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_delim="  ",
+)
+
+MOCKDUO_GECOS_INVALID_POS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    gecos_username_pos="-1",
+)
+
+# Referred to as "mockduo_users.conf"
+MOCKDUO_USERS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    groups="users",
+)
+
+MOCKDUO_USERS_ADMINS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    group="users,admin",
+)
+
+
+MOCKDUO_ADMINS_NO_USERS = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    group="admin,!users",
+)
+
+MOTD_CONF = DuoUnixConfig(
+    ikey="DIXYZV6YM8IFYVWBINCA",
+    skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+    host="localhost:4443",
+    cafile="certs/mockduo-ca.pem",
+    motd="yes",
+)
+
+MOCKDUO_EXTRA_SPACE = """
+[duo]
+ikey = DIXYZV6YM8IFYVWBINCA
+skey =
+ yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo
+host = localhost:4443
+cafile = certs/mockduo-ca.pem
+ ; This comment shouldn't break Duo
+"""
+
+
+class TempConfig(object):
+    def __init__(self, config_data):
+        self.config_data = str(config_data)
+        self.temp_file = None
+
+    def __enter__(self):
+        self.temp_file = NamedTemporaryFile()
+        self.temp_file.write(self.config_data.encode("utf8"))
+        self.temp_file.flush()
+        return self.temp_file
+
+    def __exit__(self, type, value, traceback):
+        self.temp_file.close()
diff --git a/tests/confs/mockduo_extra_space.conf b/tests/confs/mockduo_extra_space.conf
new file mode 100644
index 0000000..ea16b93
--- /dev/null
+++ b/tests/confs/mockduo_extra_space.conf
@@ -0,0 +1,7 @@
+[duo]
+ikey = DIXYZV6YM8IFYVWBINCA
+skey =
+ yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo
+host = localhost:4443
+cafile = certs/mockduo-ca.pem
+ ; This comment shouldn't break Duo
diff --git a/tests/fips_scanner.sh b/tests/fips_scanner.sh
index 3dca5f5..dade83a 100755
--- a/tests/fips_scanner.sh
+++ b/tests/fips_scanner.sh
@@ -125,14 +125,17 @@
 echo -e "Checking for low-level cipher calls"
 echo -e "===================================\n"
 
+EXITCODE=0
+
 #Exclude files that are being used to search for anything not fips compliant 
 #Unless excluded, these files will also be scanned and trigger false positives
+errorFile="fips_scanner.sh.err"
 fipsScanner="fips_scanner.sh"
 testCrypto="test_crypto-0*"
 for cipher in ${CIPHER_LIST[@]} ; do
-    echo "Scanning for cipher function: ${cipher}"
-    if grep -R ${cipher} ${DIR} --exclude={$fipsScanner,$testCrypto} ; then
-      echo -e "\e[92mFound potential calls for ${cipher}\e[0m"
+    if grep -R ${cipher} ${DIR} --exclude={$fipsScanner,$testCrypto,$errorFile} ; then
+      echo "Found potential calls for ${cipher}"
+      EXITCODE=1
     fi
 done
 
@@ -156,8 +159,10 @@
 echo -e "\nChecking for low-level digest calls"
 echo -e "===================================\n"
 for digest in ${DIGEST_LIST[@]} ; do
-    echo "Scanning for cipher function: ${digest}"
-    if grep -R ${digest} ${DIR} --exclude={$fipsScanner,$testCrypto} ; then
-      echo -e "\e[92mFound potential calls for ${digest}\e[0m"
+    if grep -R ${digest} ${DIR} --exclude={$fipsScanner,$testCrypto,$errorFile} ; then
+      echo "Found potential calls for ${digest}"
+      EXITCODE=1
     fi    
 done
+
+exit $EXITCODE
diff --git a/tests/groups_preload.c b/tests/groups_preload.c
index bc3c281..04f3df8 100644
--- a/tests/groups_preload.c
+++ b/tests/groups_preload.c
@@ -10,6 +10,9 @@
 #include <string.h>
 #include <stdlib.h>
 #include <unistd.h>
+#include <dlfcn.h>
+
+FILE *(*_fopen)(const char* filename, const char* mode);
 
 static struct passwd _passwd[6] = {
         { "user1", "*", 1000, 1000, .pw_gecos = "gecos", .pw_dir = "/",
@@ -104,6 +107,20 @@
         return (&_groups[_group_ptr++]);
 }
 
+FILE *
+fopen(const char *filename, const char *mode)
+{
+    if (strcmp(filename, "/etc/motd") == 0) {
+        char *m = getenv("MOTD_FILE");
+        if(m) {
+            _fopen = dlsym(RTLD_NEXT, "fopen");
+            return (*_fopen)(m, mode);
+        }
+    }
+    _fopen = dlsym(RTLD_NEXT, "fopen");
+    return (*_fopen)(filename, mode);
+}
+
 int
 #ifdef __APPLE__
 getgrouplist(const char *user, int group, int *groups, int *ngroups)
diff --git a/tests/is_fips_supported.sh b/tests/is_fips_supported.sh
index 9fe3cdd..2338dc7 100755
--- a/tests/is_fips_supported.sh
+++ b/tests/is_fips_supported.sh
@@ -5,7 +5,7 @@
 # We also echo the return code before exiting since there's no good way to capture it
 # without some actual output in cram :(
 
-FIPS_VALIDATED_DISTROS=("centos8" "rhel8" "centos7" "rhel7" "centos6" "rhel6")
+FIPS_VALIDATED_DISTROS=("centos8" "rhel8" "centos7" "rhel7" "centos6" "rhel6", "fedora34")
 
 # We can't use uname since that won't work with Docker images.
 # See https://stackoverflow.com/questions/31012297/uname-a-returning-the-same-in-docker-host-or-any-docker-container for more details.
diff --git a/tests/login_duo-4.t b/tests/login_duo-4.t
index 85557bd..1c1b980 100644
--- a/tests/login_duo-4.t
+++ b/tests/login_duo-4.t
@@ -103,3 +103,6 @@
   [4] Aborted Duo login for 'hostname': correct hostname
   [1]
  
+Test extra whitespace before comment in conf file
+  $ ${BUILDDIR}/login_duo/login_duo -d -c confs/mockduo_extra_space.conf -f preauth-allow true
+  [4] Skipped Duo login for 'preauth-allow': preauth-allowed
diff --git a/tests/login_duo.py b/tests/login_duo.py
index 0513684..aa2aab1 100755
--- a/tests/login_duo.py
+++ b/tests/login_duo.py
@@ -1,31 +1,34 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 import os
+import platform
 import subprocess
 import sys
-import platform
 
 import paths
 
+
 def main():
     env = os.environ.copy()
 
-    if sys.platform == 'darwin':
-        env['DYLD_LIBRARY_PATH'] = paths.topbuilddir + '/lib/.libs'
-        env['DYLD_INSERT_LIBRARIES'] = paths.build + \
-                                       '/.libs/liblogin_duo_preload.dylib'
-        env['DYLD_FORCE_FLAT_NAMESPACE'] = '1'
-    elif sys.platform == 'sunos5':
-        architecture = {'32bit': '32', '64bit': '64'}[platform.architecture()[0]]
-        env['LD_PRELOAD_' + architecture] = paths.build + '/.libs/liblogin_duo_preload.so'
+    if sys.platform == "darwin":
+        env["DYLD_LIBRARY_PATH"] = paths.topbuilddir + "/lib/.libs"
+        env["DYLD_INSERT_LIBRARIES"] = paths.build + "/.libs/liblogin_duo_preload.dylib"
+        env["DYLD_FORCE_FLAT_NAMESPACE"] = "1"
+    elif sys.platform == "sunos5":
+        architecture = {"32bit": "32", "64bit": "64"}[platform.architecture()[0]]
+        env["LD_PRELOAD_" + architecture] = (
+            paths.build + "/.libs/liblogin_duo_preload.so"
+        )
     else:
-        env['LD_PRELOAD'] = paths.build + '/.libs/liblogin_duo_preload.so'
+        env["LD_PRELOAD"] = paths.build + "/.libs/liblogin_duo_preload.so"
 
-    args = [ paths.login_duo ] + sys.argv[1:]
+    args = [paths.login_duo] + sys.argv[1:]
     p = subprocess.Popen(args, env=env)
     p.wait()
-    
+
     sys.exit(p.returncode)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
diff --git a/tests/login_duo_preload.c b/tests/login_duo_preload.c
index 58cf543..bd9731c 100644
--- a/tests/login_duo_preload.c
+++ b/tests/login_duo_preload.c
@@ -21,10 +21,24 @@
 int (*_sys_connect)(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 int (*_sys_getaddrinfo)(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
 char *(*_sys_inet_ntoa)(struct in_addr in);
+struct passwd *(*_getpwnam)(const char* name);
+FILE *(*_fopen)(const char* filename, const char* mode);
 
-static struct passwd _passwd[1] = {
+static struct passwd _passwd[11] = {
+        { "sshd", "*", 1000, 100, .pw_gecos = "gecos", .pw_dir = "/",
+          .pw_shell = "/bin/sh" },
         { "user1", "*", 1001, 100, .pw_gecos = "gecos", .pw_dir = "/",
           .pw_shell = "/bin/sh" },
+        { "gecos/6", "*", 1010, 100, .pw_gecos = "1/2/3/4/5/gecos_user_gecos_field6", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "gecos/3", "*", 1011, 100, .pw_gecos = "1/2/gecos_user_gecos_field3/4/5/6", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "gecos,6", "*", 1012, 100, .pw_gecos = "1,2,3,4,5,gecos_user_gecos_field6", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "gecos,3", "*", 1013, 100, .pw_gecos = "1,2,gecos_user_gecos_field3,4,5,6", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "fullgecos", "*", 1014, 100, .pw_gecos = "full_gecos_field", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "noshell", "*", 1015, 100, .pw_gecos = "full_gecos_field", .pw_dir = "/", .pw_shell = NULL},
+        { "emptygecos", "*", 1016, 100, .pw_gecos = "", .pw_dir = "/", .pw_shell = "/bin/sh" },
+        { "slashshell", "*", 1017, 100, .pw_gecos = "full_gecos_field", .pw_dir = "/", .pw_shell = "/bin/echo"},
+        { "preauth-allow", "*", 1018, 100, .pw_gecos = "gecos", .pw_dir = "/",
+          .pw_shell = "/bin/sh" },
 };
 
 int
@@ -106,7 +120,9 @@
 uid_t
 geteuid(void)
 {
-        return (getuid());
+        char *p = getenv("EUID");
+
+        return (p ? atoi(p) : getuid());
 }
 
 struct passwd *
@@ -115,9 +131,45 @@
         int i;
 
         for (i = 0; i < sizeof(_passwd) / sizeof(_passwd[0]); i++) {
-                if (_passwd[i].pw_uid == uid)
-                        return (&_passwd[i]);
+                if (_passwd[i].pw_uid == uid) {
+                    // we have to copy the pw_gecos field because it might be modified
+                    // by `duo_split_at` which casues a segfault if we leave it as a
+                    // constant literal
+                    _passwd[i].pw_gecos = strdup(_passwd[i].pw_gecos);
+                    return (&_passwd[i]);
+                }
         }
         errno = ENOENT;
         return (NULL);
 }
+
+struct passwd *
+getpwnam(const char *name)
+{
+    char *u = getenv("NO_PRIVSEP_USER");
+    int i;
+    if(u) {
+        return NULL;
+    }
+    for (i = 0; i < sizeof(_passwd) / sizeof(_passwd[0]); i++) {
+            if (strcmp(_passwd[i].pw_name, name) == 0) {
+                return (&_passwd[i]);
+            }
+    }
+    _getpwnam = dlsym(RTLD_NEXT, "getpwnam");
+    return (*_getpwnam)(name);
+}
+
+FILE *
+fopen(const char *filename, const char *mode)
+{
+    if (strcmp(filename, "/etc/motd") == 0) {
+        char *m = getenv("MOTD_FILE");
+        if(m) {
+            _fopen = dlsym(RTLD_NEXT, "fopen");
+            return (*_fopen)(m, mode);
+        }
+    }
+    _fopen = dlsym(RTLD_NEXT, "fopen");
+    return (*_fopen)(filename, mode);
+}
diff --git a/tests/mockduo.py b/tests/mockduo.py
index a901bf8..55acb39 100755
--- a/tests/mockduo.py
+++ b/tests/mockduo.py
@@ -1,116 +1,133 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-import BaseHTTPServer
 import cgi
-import bson
+import json
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
 try:
     from hashlib import sha1
 except ImportError:
     import sha as sha1
+
+import base64
 import hmac
 import os
+import socket
 import ssl
 import sys
 import time
 import urllib
-import socket
+import urllib.parse
 
-IKEY = 'DIXYZV6YM8IFYVWBINCA'
-SKEY = 'yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo'
+IKEY = "DIXYZV6YM8IFYVWBINCA"
+SKEY = b"yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo"
 # Used to check if the FQDN is set to either the ipv4 or ipv6 address
-IPV6_LOOPBACK_ADDR = '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa'
-IPV4_LOOPBACK_ADDR = '1.0.0.127.in-addr.arpa'
+IPV6_LOOPBACK_ADDR = (
+    "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa"
+)
+IPV4_LOOPBACK_ADDR = "1.0.0.127.in-addr.arpa"
 
 tx_msgs = {
-    'txPUSH1': [ '0:Pushed a login request to your phone.',
-                 '1:Success. Logging you in...' ],
-    'txVOICE1': [ '0:Dialing XXX-XXX-1234...',
-                  "1:Answered. Press '#' on your phone to log in.",
-                  '1:Success. Logging you in...' ],
-    'txSMSREFRESH1': [ '0:New SMS passcodes sent' ],
-    'txVOICE2': [ '0:Dialing XXX-XXX-5678...',
-                  "1:Answered. Press '#' on your phone to log in.",
-                  '2:Authentication timed out.' ],
-    }
+    "txPUSH1": [
+        "0:Pushed a login request to your phone.",
+        "1:Success. Logging you in...",
+    ],
+    "txVOICE1": [
+        "0:Dialing XXX-XXX-1234...",
+        "1:Answered. Press '#' on your phone to log in.",
+        "1:Success. Logging you in...",
+    ],
+    "txSMSREFRESH1": ["0:New SMS passcodes sent"],
+    "txVOICE2": [
+        "0:Dialing XXX-XXX-5678...",
+        "1:Answered. Press '#' on your phone to log in.",
+        "2:Authentication timed out.",
+    ],
+}
 
-class MockDuoHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-    server_version = 'MockDuo/1.0'
-    protocol_version = 'HTTP/1.1'
+
+class MockDuoHandler(BaseHTTPRequestHandler):
+    server_version = "MockDuo/1.0"
+    protocol_version = "HTTP/1.1"
 
     def _verify_sig(self):
-        authz = self.headers['Authorization'].split()[1].decode('base64')
-        ikey, sig = authz.split(':')
+        authz = base64.b64decode(self.headers["Authorization"].split()[1]).decode(
+            "utf-8"
+        )
+        ikey, sig = authz.split(":")
         if ikey != IKEY:
             return False
-        
-        canon = [ self.method,
-                  self.headers['Host'].split(':')[0].lower(),
-                  self.path ]
+
+        canon = [self.method, self.headers["Host"].split(":")[0].lower(), self.path]
         l = []
         for k in sorted(self.args.keys()):
-            l.append('%s=%s' % (urllib.quote(k, '~'),
-                                urllib.quote(self.args[k], '~')))
-        canon.append('&'.join(l))
-        h = hmac.new(SKEY, '\n'.join(canon), sha1)
-        
+            l.append(
+                "{0}={1}".format(
+                    urllib.parse.quote(k, "~"), urllib.parse.quote(self.args[k], "~")
+                )
+            )
+        canon.append("&".join(l))
+        h = hmac.new(SKEY, ("\n".join(canon)).encode("utf8"), digestmod="sha1")
+
         return sig == h.hexdigest()
 
-    def _get_args(self): 
-        if self.method == 'POST':
-            env = { 'REQUEST_METHOD': 'POST',
-                    'CONTENT_TYPE': self.headers['Content-Type'] }
-            fs = cgi.FieldStorage(fp=self.rfile, headers=self.headers,
-                                  environ=env)
+    def _get_args(self):
+        if self.method == "POST":
+            env = {
+                "REQUEST_METHOD": "POST",
+                "CONTENT_TYPE": self.headers["Content-Type"],
+            }
+            fs = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
             args = {}
             for k in fs.keys():
                 args[k] = fs[k].value
         else:
-            args = dict(cgi.parse_qsl(self.qs))
-        print 'got %s %s args: %s' % (self.method, self.path, args)
+            args = dict(urllib.parse.parse_qsl(self.qs))
+        print("got {0} {1} args: {2}".format(self.method, self.path, args))
         return args
 
-    def _get_tx_response(self, txid, async):
+    def _get_tx_response(self, txid, is_async):
         last = True
         if txid not in tx_msgs:
-            secs, msg = 0, 'Invalid passcode, please try again.'
-        elif async:
-            secs, msg = tx_msgs[txid].pop(0).split(':', 1)
+            secs, msg = 0, "Invalid passcode, please try again."
+        elif is_async:
+            secs, msg = tx_msgs[txid].pop(0).split(":", 1)
             last = not tx_msgs[txid]
         else:
-            secs, msg = tx_msgs[txid][-1].split(':', 1)
-        
-        if msg.startswith('Success'):
-            rsp = { 'result': 'allow', 'status': msg }
-        elif async and not last:
-            rsp = { 'status': msg }
+            secs, msg = tx_msgs[txid][-1].split(":", 1)
+
+        if msg.startswith("Success"):
+            rsp = {"result": "allow", "status": msg}
+        elif is_async and not last:
+            rsp = {"status": msg}
         else:
-            rsp = { 'result': 'deny', 'status': msg }
+            rsp = {"result": "deny", "status": msg}
         time.sleep(int(secs))
         return rsp
 
-    def _send(self, code, buf=''):
+    def _send(self, code, buf=b""):
         self.send_response(code)
         self.send_header("Content-length", str(len(buf)))
         if buf:
-            self.send_header("Content-type", "application/bson")
+            self.send_header("Content-type", "application/json")
             self.end_headers()
-            self.wfile.write(buf)
+            self.wfile.write(buf.encode("utf8"))
         else:
             self.end_headers()
-        
+
     def do_GET(self):
-        self.method = 'GET'
-        self.path, self.qs = self.path.split('?', 1)
+        self.method = "GET"
+        self.path, self.qs = self.path.split("?", 1)
         self.args = self._get_args()
-        
+
         if not self._verify_sig():
             return self._send(401)
-        
-        ret = { 'stat': 'OK' }
-        
-        if self.path == '/rest/v1/status.bson':
-            ret['response'] = self._get_tx_response(self.args['txid'], 1)
-            buf = bson.dumps(ret)
+
+        ret = {"stat": "OK"}
+
+        if self.path == "/rest/v1/status.json":
+            ret["response"] = self._get_tx_response(self.args["txid"], 1)
+            buf = json.dumps(ret)
             return self._send(200, buf)
 
         self._send(404)
@@ -118,122 +135,147 @@
     def hostname_check(self, hostname):
         domain_fqdn = socket.getfqdn().lower()
         if hostname == domain_fqdn.lower() or hostname == socket.gethostname().lower():
-            return True 
-        #Check if socket.getfqdn() is the loopback address for ipv4 or ipv6 then check the hostname of the machine 
+            return True
+        # Check if socket.getfqdn() is the loopback address for ipv4 or ipv6 then check the hostname of the machine
         if domain_fqdn == IPV6_LOOPBACK_ADDR or domain_fqdn == IPV4_LOOPBACK_ADDR:
             if hostname == socket.gethostbyaddr(socket.gethostname())[0].lower():
                 return True
-        return False 
+        return False
 
     def do_POST(self):
-        self.method = 'POST'
+        self.method = "POST"
         self.args = self._get_args()
-        
+        buf = None
+
         if not self._verify_sig():
             return self._send(401)
-        
+
         try:
-            return self._send(int(self.args['user']))
+            return self._send(int(self.args["user"]))
         except:
-            ret = { 'stat': 'OK' }
-        
-        if self.path == '/rest/v1/preauth.bson':
-            if self.args['user'] == 'preauth-ok-missing_response':
+            ret = {"stat": "OK"}
+
+        if self.path == "/rest/v1/preauth.json":
+            if self.args["user"] == "preauth-ok-missing_response":
                 pass
-            elif self.args['user'] == 'preauth-fail-missing_response':
-                ret['stat'] = 'FAIL'
-            elif self.args['user'] == 'preauth-bad-stat':
-                ret['stat'] = 'BAD_STATUS'
-            elif self.args['user'] == 'preauth-fail':
-                ret = { 'stat': 'FAIL', 'code': 1000, 'message': 'Pre-authentication failed' }
-            elif self.args['user'] == 'preauth-deny':
-                ret['response'] = { 'result': 'deny', 'status': 'preauth-denied' }
-            elif self.args['user'] == 'preauth-allow':
-                ret['response'] = { 'result': 'allow', 'status': 'preauth-allowed' }
-            elif self.args['user'] == 'preauth-allow-bad_response':
-                ret['response'] = { 'result': 'allow', 'xxx': 'preauth-allowed-bad-response' }
-            elif (self.args['user'] == 'hostname'):
-                if (self.hostname_check(self.args['hostname'].lower())):
-                    ret['response'] = { 'result': 'deny', 'status': 'correct hostname' }
+            elif self.args["user"] == "preauth-fail-missing_response":
+                ret["stat"] = "FAIL"
+            elif self.args["user"] == "preauth-bad-stat":
+                ret["stat"] = "BAD_STATUS"
+            elif self.args["user"] == "preauth-fail":
+                ret = {
+                    "stat": "FAIL",
+                    "code": 1000,
+                    "message": "Pre-authentication failed",
+                }
+            elif self.args["user"] == "preauth-deny":
+                ret["response"] = {"result": "deny", "status": "preauth-denied"}
+            elif self.args["user"] == "preauth-allow":
+                ret["response"] = {"result": "allow", "status": "preauth-allowed"}
+            elif self.args["user"] == "preauth-allow-bad_response":
+                ret["response"] = {
+                    "result": "allow",
+                    "xxx": "preauth-allowed-bad-response",
+                }
+            elif self.args["user"] == "hostname":
+                if self.hostname_check(self.args["hostname"].lower()):
+                    ret["response"] = {"result": "deny", "status": "correct hostname"}
                 else:
-                    response = "hostname recieved: " + self.args['hostname'] + " found: " + socket.getfqdn()
-                    ret['response'] = { 'result': 'deny', 'status': response }
-            elif self.args['user'] == 'failopen':
-                if self.args['failmode'] == 'open':
-                    ret['response'] = { 'result': 'deny', 'status': 'correct failmode' }
+                    response = (
+                        "hostname recieved: "
+                        + self.args["hostname"]
+                        + " found: "
+                        + socket.getfqdn()
+                    )
+                    ret["response"] = {"result": "deny", "status": response}
+            elif self.args["user"] == "failopen":
+                if self.args["failmode"] == "open":
+                    ret["response"] = {"result": "deny", "status": "correct failmode"}
                 else:
-                    ret['response'] = { 'result': 'deny', 'status': 'incorrect failmode' }
-            elif self.args['user'] == 'failclosed':
-                if self.args['failmode'] == 'closed':
-                    ret['response'] = { 'result': 'deny', 'status': 'correct failmode' }
+                    ret["response"] = {"result": "deny", "status": "incorrect failmode"}
+            elif self.args["user"] == "failclosed":
+                if self.args["failmode"] == "closed":
+                    ret["response"] = {"result": "deny", "status": "correct failmode"}
                 else:
-                    ret['response'] = { 'result': 'deny', 'status': 'incorrect failmode' }
-            elif self.args['user'] == 'gecos_user_gecos_field6':
-                ret['response'] = { 'result': 'allow', 'status': 'gecos-user-gecos-field6-allowed' }
-            elif self.args['user'] == 'gecos_user_gecos_field3':
-                ret['response'] = { 'result': 'allow', 'status': 'gecos-user-gecos-field3-allowed' }
-            elif self.args['user'] == 'full_gecos_field':
-                ret['response'] = { 'result': 'allow', 'status': 'full-gecos-field' }
-            elif self.args['user'] == 'gecos/6':
-                ret['response'] = { 'result': 'allow', 'status': 'gecos/6' }
+                    ret["response"] = {"result": "deny", "status": "incorrect failmode"}
+            elif self.args["user"] == "gecos_user_gecos_field6":
+                ret["response"] = {
+                    "result": "allow",
+                    "status": "gecos-user-gecos-field6-allowed",
+                }
+            elif self.args["user"] == "gecos_user_gecos_field3":
+                ret["response"] = {
+                    "result": "allow",
+                    "status": "gecos-user-gecos-field3-allowed",
+                }
+            elif self.args["user"] == "full_gecos_field":
+                ret["response"] = {"result": "allow", "status": "full-gecos-field"}
+            elif self.args["user"] == "gecos/6":
+                ret["response"] = {"result": "allow", "status": "gecos/6"}
+            elif self.args["user"] == "enroll":
+                ret["response"] = {"result": "enroll", "status": "please enroll"}
+            elif self.args["user"] == "bad-json":
+                buf = b""
             else:
-                ret['response'] = {
-                    'result': 'auth',
-                    'prompt': 'Duo login for %s\n\n' % self.args['user'] + \
-                              'Choose or lose:\n\n' + \
-                              '  1. Push 1\n  2. Phone 1\n' + \
-                              '  3. SMS 1 (deny)\n  4. Phone 2 (deny)\n\n' + \
-                              'Passcode or option (1-4): ',
-                    'factors': {
-                        'default': 'push1',
-                        '1': 'push1',
-                        '2': 'voice1',
-                        '3': 'smsrefresh1',
-                        '4': 'voice2',
-                        }
-                    }
-        elif self.path == '/rest/v1/auth.bson':
-            if self.args['factor'] == 'auto':
-                txid = 'tx' + self.args['auto'].upper()
-                if self.args['user'] == 'pam_prompt':
-                    ret['response'] = { 'txid': 'wrongFactor1' }
-                elif self.args['async'] == '1':
-                    ret['response'] = { 'txid': txid }
+                ret["response"] = {
+                    "result": "auth",
+                    "prompt": "Duo login for {0}\n\n".format(self.args["user"])
+                    + "Choose or lose:\n\n"
+                    + "  1. Push 1\n  2. Phone 1\n"
+                    + "  3. SMS 1 (deny)\n  4. Phone 2 (deny)\n\n"
+                    + "Passcode or option (1-4): ",
+                    "factors": {
+                        "default": "push1",
+                        "1": "push1",
+                        "2": "voice1",
+                        "3": "smsrefresh1",
+                        "4": "voice2",
+                    },
+                }
+        elif self.path == "/rest/v1/auth.json":
+            if self.args["factor"] == "auto":
+                txid = "tx" + self.args["auto"].upper()
+                if self.args["user"] == "pam_prompt":
+                    ret["response"] = {"txid": "wrongFactor1"}
+                elif self.args["async"] == "1":
+                    ret["response"] = {"txid": txid}
                 else:
-                    ret['response'] = self._get_tx_response(txid, 0)
+                    ret["response"] = self._get_tx_response(txid, 0)
             else:
-                ret['response'] = { 'result': 'deny',
-                                    'status': 'no %s' % self.args['factor'] }
-            if (self.args['user'] == 'auth_timeout'):
+                ret["response"] = {
+                    "result": "deny",
+                    "status": "no {0}".format(self.args["factor"]),
+                }
+            if self.args["user"] == "auth_timeout":
                 return self._send(500)
         else:
             return self._send(404)
 
-        buf = bson.dumps(ret)
-        
+        if buf is None:
+            buf = json.dumps(ret)
+
         return self._send(200, buf)
 
+
 def main():
     port = 4443
-    host = 'localhost'
+    host = "localhost"
     if len(sys.argv) == 1:
-        cafile = os.path.realpath('%s/certs/mockduo.pem' %
-                                  os.path.dirname(__file__))
+        cafile = os.path.realpath(
+            "{0}/certs/mockduo.pem".format(os.path.dirname(__file__))
+        )
     elif len(sys.argv) == 2:
         cafile = sys.argv[1]
     else:
-        print >>sys.stderr, 'Usage: %s [certfile]\n' % sys.argv[0]
+        print("Usage: {0} [certfile]\n".format(sys.argv[0]), file=sys.stderr)
         sys.exit(1)
-    
-    httpd = BaseHTTPServer.HTTPServer((host, port), MockDuoHandler)
 
-    httpd.socket = ssl.wrap_socket(
-        httpd.socket,
-        certfile=cafile,
-        server_side=True
-        )
+    httpd = HTTPServer((host, port), MockDuoHandler)
+
+    httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cafile, server_side=True)
 
     httpd.serve_forever()
-    
-if __name__ == '__main__':
+
+
+if __name__ == "__main__":
     main()
diff --git a/tests/mockduo_context.py b/tests/mockduo_context.py
new file mode 100644
index 0000000..b04db76
--- /dev/null
+++ b/tests/mockduo_context.py
@@ -0,0 +1,121 @@
+import os
+import socket
+import subprocess
+import time
+
+from paths import topbuilddir
+
+TESTDIR = os.path.realpath(os.path.dirname(__file__))
+
+WRONGHOST_CERT = os.path.join(TESTDIR, "certs", "mockduo-wronghost.pem")
+NORMAL_CERT = os.path.join(TESTDIR, "certs", "mockduo.pem")
+SELFSIGNED_CERT = os.path.join(TESTDIR, "certs", "selfsigned.pem")
+
+
+def port_open(ip, port):
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    try:
+        s.connect((ip, int(port)))
+        s.shutdown(2)
+        s.close()
+        return True
+    except:
+        s.close()
+        return False
+    finally:
+        s.close()
+
+
+class MockDuoException(Exception):
+    def __init__(self, returncode, cmd, stderr, stdout):
+        self.returncode = returncode
+        self.cmd = cmd
+        self.stderr = stderr
+        self.stdout = stdout
+
+    def __str__(self):
+        if self.stderr:
+            stderr_output = "STDERR:\n{stderr}".format(stderr=self.stderr)
+        else:
+            stderr_output = ""
+
+        if self.stdout:
+            stdout_output = "STDOUT:\n{stdout}".format(stdout=self.stdout)
+        else:
+            stdout_output = ""
+
+        return "Command: '{cmd}' returned non-zero exit code: {returncode}\n{stdout}{stderr}".format(
+            cmd=self.cmd,
+            returncode=self.returncode,
+            stderr=stderr_output,
+            stdout=stdout_output,
+        )
+
+
+class MockDuoTimeoutException(MockDuoException):
+    def __str__(self):
+        return (
+            "Timeout starting MockDuo\n"
+            + super(MockDuoTimeoutException, self).__str__()
+        )
+
+
+class MockDuo:
+    def __init__(self, cert=NORMAL_CERT):
+        self.cert = cert
+        self.cmd = ["python3", os.path.join(TESTDIR, "mockduo.py"), self.cert]
+        self.process = None
+
+    def __enter__(self):
+        self.process = subprocess.Popen(
+            self.cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+
+        # wait a couple of seconds max for the local server to start
+        for i in range(0, 80):
+            if port_open("127.0.0.1", 4443):
+                break
+            time.sleep(0.05)
+        else:
+            stderr = self.process.stderr.read().decode("utf-8")
+            stdout = self.process.stdout.read().decode("utf-8")
+            self.process.stderr.close()
+            self.process.stdout.close()
+
+            if self.process.stdin:
+                self.process.stdin.close()
+
+            self.process.wait()
+            raise MockDuoTimeoutException(
+                returncode=None,
+                cmd=self.cmd,
+                stderr=stderr,
+                stdout=stdout,
+            )
+
+        time.sleep(0.3)
+        return self.process
+
+    def __exit__(self, type, value, traceback):
+        try:
+            returncode = self.process.poll()
+            if returncode is None:
+                self.process.terminate()
+                return
+
+            stderr = self.process.stderr.read().decode("utf-8")
+            stdout = self.process.stdout.read().decode("utf-8")
+            if returncode != 0:
+                raise MockDuoException(
+                    returncode=returncode,
+                    cmd=self.cmd,
+                    stderr=stderr,
+                    stdout=stdout,
+                )
+        finally:
+            self.process.stderr.close()
+            self.process.stdout.close()
+            self.process.terminate()
+            self.process.wait()
diff --git a/tests/mocklogin_duo.py b/tests/mocklogin_duo.py
index 66b3e1d..17353d1 100755
--- a/tests/mocklogin_duo.py
+++ b/tests/mocklogin_duo.py
@@ -1,58 +1,59 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 import os
-import pexpect
 import sys
 
 import paths
+import pexpect
 
-PROMPT = '.* or option \(1-4\): $'
+PROMPT = ".* or option \(1-4\): $"
+
 
 def _login_duo(confs):
-    p = pexpect.spawn(paths.login_duo + ' -d -c' + confs + \
-                      ' -f foobar echo SUCCESS')
+    p = pexpect.spawn(paths.login_duo + " -d -c" + confs + " -f foobar echo SUCCESS")
     p.expect(PROMPT, timeout=10)
-    print '===> %r' % p.match.group(0)
+    print "===> %r" % p.match.group(0)
     return p
 
+
 def main():
     confs = sys.argv[1]
     p = _login_duo(confs)
 
     # 3 failures in a row
-    p.sendline('123456')
+    p.sendline("123456")
     p.expect(PROMPT)
-    print '===> %r' % p.match.group(0)
-    
-    p.sendline('wefawefgoiagj3rj')
+    print "===> %r" % p.match.group(0)
+
+    p.sendline("wefawefgoiagj3rj")
     p.expect(PROMPT)
-    print '===> %r' % p.match.group(0)
-    
-    p.sendline('A' * 500)
+    print "===> %r" % p.match.group(0)
+
+    p.sendline("A" * 500)
     p.expect(pexpect.EOF)
-    print '===> %r' % p.before
+    print "===> %r" % p.before
 
     # menu options
     p = _login_duo(confs)
 
-    p.sendline('3')
+    p.sendline("3")
     p.expect(PROMPT)
-    print '===> %r' % p.match.group(0)
-    
-    p.sendline('4')
-    p.expect(PROMPT)
-    print '===> %r' % p.match.group(0)
+    print "===> %r" % p.match.group(0)
 
-    p.sendline('1')
+    p.sendline("4")
+    p.expect(PROMPT)
+    print "===> %r" % p.match.group(0)
+
+    p.sendline("1")
     p.expect(pexpect.EOF)
-    print '===> %r' % p.before
-    
+    print "===> %r" % p.before
+
     p = _login_duo(confs)
-    
-    p.sendline('2')
+
+    p.sendline("2")
     p.expect(pexpect.EOF)
-    print '===> %r' % p.before
+    print "===> %r" % p.before
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
-
diff --git a/tests/pam_duo-4.t b/tests/pam_duo-4.t
index 45f93a2..b041796 100644
--- a/tests/pam_duo-4.t
+++ b/tests/pam_duo-4.t
@@ -113,3 +113,7 @@
   Invalid passcode, please try again.
   Autopushing login request to phone...
   Invalid passcode, please try again.
+
+Test extra whitespace before comment in conf fil
+  $ ./testpam.py -d -c confs/mockduo_extra_space.conf -f preauth-allow true
+  [4] Skipped Duo login for 'preauth-allow': preauth-allowed
diff --git a/tests/pexpect.py b/tests/pexpect.py
index 67c6389..7e38b2f 100644
--- a/tests/pexpect.py
+++ b/tests/pexpect.py
@@ -64,36 +64,49 @@
 """
 
 try:
-    import os, sys, time
-    import select
-    import string
-    import re
-    import struct
-    import resource
-    import types
-    import pty
-    import tty
-    import termios
-    import fcntl
     import errno
-    import traceback
+    import fcntl
+    import os
+    import pty
+    import re
+    import resource
+    import select
     import signal
-except ImportError, e:
-    raise ImportError (str(e) + """
+    import string
+    import struct
+    import sys
+    import termios
+    import time
+    import traceback
+    import tty
+    import types
+except ImportError as e:
+    raise ImportError(
+        str(e)
+        + """
 
 A critical module was not found. Probably this operating system does not
-support it. Pexpect is intended for UNIX-like operating systems.""")
+support it. Pexpect is intended for UNIX-like operating systems."""
+    )
 
-__version__ = '2.3'
-__revision__ = '$Revision: 399 $'
-__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'run', 'which',
-    'split_command_line', '__version__', '__revision__']
+__version__ = "2.3"
+__revision__ = "$Revision: 399 $"
+__all__ = [
+    "ExceptionPexpect",
+    "EOF",
+    "TIMEOUT",
+    "spawn",
+    "run",
+    "which",
+    "split_command_line",
+    "__version__",
+    "__revision__",
+]
 
 # Exception classes used by this module.
 class ExceptionPexpect(Exception):
 
-    """Base class for all exceptions raised by this module.
-    """
+    """Base class for all exceptions raised by this module."""
 
     def __init__(self, value):
 
@@ -107,30 +120,33 @@
 
         """This returns an abbreviated stack trace with lines that only concern
         the caller. In other words, the stack trace inside the Pexpect module
-        is not included. """
+        is not included."""
 
         tblist = traceback.extract_tb(sys.exc_info()[2])
-        #tblist = filter(self.__filter_not_pexpect, tblist)
+        # tblist = filter(self.__filter_not_pexpect, tblist)
         tblist = [item for item in tblist if self.__filter_not_pexpect(item)]
         tblist = traceback.format_list(tblist)
-        return ''.join(tblist)
+        return "".join(tblist)
 
     def __filter_not_pexpect(self, trace_list_item):
 
-        """This returns True if list item 0 the string 'pexpect.py' in it. """
+        """This returns True if list item 0 the string 'pexpect.py' in it."""
 
-        if trace_list_item[0].find('pexpect.py') == -1:
+        if trace_list_item[0].find("pexpect.py") == -1:
             return True
         else:
             return False
 
+
 class EOF(ExceptionPexpect):
 
     """Raised when EOF is read from a child. This usually means the child has exited."""
 
+
 class TIMEOUT(ExceptionPexpect):
 
-    """Raised when a read time exceeds the timeout. """
+    """Raised when a read time exceeds the timeout."""
+
 
 ##class TIMEOUT_PATTERN(TIMEOUT):
 ##    """Raised when the pattern match time exceeds the timeout.
@@ -141,7 +157,17 @@
 ##class MAXBUFFER(ExceptionPexpect):
 ##    """Raised when a scan buffer fills before matching an expected pattern."""
 
-def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None, cwd=None, env=None):
+
+def run(
+    command,
+    timeout=-1,
+    withexitstatus=False,
+    events=None,
+    extra_args=None,
+    logfile=None,
+    cwd=None,
+    env=None,
+):
 
     """
     This function runs the given command; waits for it to finish; then
@@ -213,58 +239,71 @@
     the next event. A callback may also return a string which will be sent to
     the child. 'extra_args' is not used by directly run(). It provides a way to
     pass data to a callback function through run() through the locals
-    dictionary passed to a callback. """
+    dictionary passed to a callback."""
 
     if timeout == -1:
         child = spawn(command, maxread=2000, logfile=logfile, cwd=cwd, env=env)
     else:
-        child = spawn(command, timeout=timeout, maxread=2000, logfile=logfile, cwd=cwd, env=env)
+        child = spawn(
+            command, timeout=timeout, maxread=2000, logfile=logfile, cwd=cwd, env=env
+        )
     if events is not None:
         patterns = events.keys()
         responses = events.values()
     else:
-        patterns=None # We assume that EOF or TIMEOUT will save us.
-        responses=None
+        patterns = None  # We assume that EOF or TIMEOUT will save us.
+        responses = None
     child_result_list = []
     event_count = 0
     while 1:
         try:
-            index = child.expect (patterns)
-            if type(child.after) in types.StringTypes:
+            index = child.expect(patterns)
+            if type(child.after) is str:
                 child_result_list.append(child.before + child.after)
-            else: # child.after may have been a TIMEOUT or EOF, so don't cat those.
+            else:  # child.after may have been a TIMEOUT or EOF, so don't cat those.
                 child_result_list.append(child.before)
-            if type(responses[index]) in types.StringTypes:
+            if type(responses[index]) is str:
                 child.send(responses[index])
-            elif type(responses[index]) is types.FunctionType:
+            elif isinstance(responses[index], types.FunctionType):
                 callback_result = responses[index](locals())
                 sys.stdout.flush()
-                if type(callback_result) in types.StringTypes:
+                if type(callback_result) is str:
                     child.send(callback_result)
                 elif callback_result:
                     break
             else:
-                raise TypeError ('The callback must be a string or function type.')
+                raise TypeError("The callback must be a string or function type.")
             event_count = event_count + 1
-        except TIMEOUT, e:
+        except TIMEOUT as e:
             child_result_list.append(child.before)
             break
-        except EOF, e:
+        except EOF as e:
             child_result_list.append(child.before)
             break
-    child_result = ''.join(child_result_list)
+    child_result = "".join(child_result_list)
     if withexitstatus:
         child.close()
         return (child_result, child.exitstatus)
     else:
         return child_result
 
-class spawn (object):
+
+class spawn(object):
 
     """This is the main class interface for Pexpect. Use this class to start
-    and control child applications. """
+    and control child applications."""
 
-    def __init__(self, command, args=[], timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None):
+    def __init__(
+        self,
+        command,
+        args=[],
+        timeout=30,
+        maxread=2000,
+        searchwindowsize=None,
+        logfile=None,
+        cwd=None,
+        env=None,
+    ):
 
         """This is the constructor. The command parameter may be a string that
         includes a command and any arguments to the command. For example::
@@ -336,12 +375,12 @@
         the input from the child and output sent to the child. Sometimes you
         don't want to see everything you write to the child. You only want to
         log what the child sends back. For example::
-        
+
             child = pexpect.spawn('some_command')
             child.logfile_read = sys.stdout
 
         To separately log output sent to the child use logfile_send::
-        
+
             self.logfile_send = fout
 
         The delaybeforesend helps overcome a weird behavior that many users
@@ -372,7 +411,7 @@
         signalstatus will store the signal value and exitstatus will be None.
         If you need more detail you can also read the self.status member which
         stores the status returned by os.waitpid. You can interpret this using
-        os.WIFEXITED/os.WEXITSTATUS or os.WIFSIGNALED/os.TERMSIG. """
+        os.WIFEXITED/os.WEXITSTATUS or os.WIFSIGNALED/os.TERMSIG."""
 
         self.STDIN_FILENO = pty.STDIN_FILENO
         self.STDOUT_FILENO = pty.STDOUT_FILENO
@@ -390,43 +429,46 @@
         self.terminated = True
         self.exitstatus = None
         self.signalstatus = None
-        self.status = None # status returned by os.waitpid
+        self.status = None  # status returned by os.waitpid
         self.flag_eof = False
         self.pid = None
-        self.child_fd = -1 # initially closed
+        self.child_fd = -1  # initially closed
         self.timeout = timeout
         self.delimiter = EOF
         self.logfile = logfile
-        self.logfile_read = None # input from child (read_nonblocking)
-        self.logfile_send = None # output to send (send, sendline)
-        self.maxread = maxread # max bytes to read at one time into buffer
-        self.buffer = '' # This is the read buffer. See maxread.
-        self.searchwindowsize = searchwindowsize # Anything before searchwindowsize point is preserved, but not searched.
+        self.logfile_read = None  # input from child (read_nonblocking)
+        self.logfile_send = None  # output to send (send, sendline)
+        self.maxread = maxread  # max bytes to read at one time into buffer
+        self.buffer = ""  # This is the read buffer. See maxread.
+        self.searchwindowsize = searchwindowsize  # Anything before searchwindowsize point is preserved, but not searched.
         # Most Linux machines don't like delaybeforesend to be below 0.03 (30 ms).
-        self.delaybeforesend = 0.05 # Sets sleep time used just before sending data to child. Time in seconds.
-        self.delayafterclose = 0.1 # Sets delay in close() method to allow kernel time to update process status. Time in seconds.
-        self.delayafterterminate = 0.1 # Sets delay in terminate() method to allow kernel time to update process status. Time in seconds.
-        self.softspace = False # File-like object.
-        self.name = '<' + repr(self) + '>' # File-like object.
-        self.encoding = None # File-like object.
-        self.closed = True # File-like object.
+        self.delaybeforesend = 0.05  # Sets sleep time used just before sending data to child. Time in seconds.
+        self.delayafterclose = 0.1  # Sets delay in close() method to allow kernel time to update process status. Time in seconds.
+        self.delayafterterminate = 0.1  # Sets delay in terminate() method to allow kernel time to update process status. Time in seconds.
+        self.softspace = False  # File-like object.
+        self.name = "<" + repr(self) + ">"  # File-like object.
+        self.encoding = None  # File-like object.
+        self.closed = True  # File-like object.
         self.cwd = cwd
         self.env = env
-        self.__irix_hack = (sys.platform.lower().find('irix')>=0) # This flags if we are running on irix
+        self.__irix_hack = (
+            sys.platform.lower().find("irix") >= 0
+        )  # This flags if we are running on irix
         # Solaris uses internal __fork_pty(). All others use pty.fork().
-        if (sys.platform.lower().find('solaris')>=0) or (sys.platform.lower().find('sunos5')>=0):
+        if (sys.platform.lower().find("solaris") >= 0) or (
+            sys.platform.lower().find("sunos5") >= 0
+        ):
             self.use_native_pty_fork = False
         else:
             self.use_native_pty_fork = True
 
-
         # allow dummy instances for subclasses that may not use command or args.
         if command is None:
             self.command = None
             self.args = None
-            self.name = '<pexpect factory incomplete>'
+            self.name = "<pexpect factory incomplete>"
         else:
-            self._spawn (command, args)
+            self._spawn(command, args)
 
     def __del__(self):
 
@@ -434,7 +476,7 @@
         garbage collects Python objects. OS file descriptors are not Python
         objects, so they must be handled explicitly. If the child file
         descriptor was opened outside of this class (passed to the constructor)
-        then this does not close it. """
+        then this does not close it."""
 
         if not self.closed:
             # It is possible for __del__ methods to execute during the
@@ -449,43 +491,43 @@
     def __str__(self):
 
         """This returns a human-readable string that represents the state of
-        the object. """
+        the object."""
 
         s = []
         s.append(repr(self))
-        s.append('version: ' + __version__ + ' (' + __revision__ + ')')
-        s.append('command: ' + str(self.command))
-        s.append('args: ' + str(self.args))
-        s.append('searcher: ' + str(self.searcher))
-        s.append('buffer (last 100 chars): ' + str(self.buffer)[-100:])
-        s.append('before (last 100 chars): ' + str(self.before)[-100:])
-        s.append('after: ' + str(self.after))
-        s.append('match: ' + str(self.match))
-        s.append('match_index: ' + str(self.match_index))
-        s.append('exitstatus: ' + str(self.exitstatus))
-        s.append('flag_eof: ' + str(self.flag_eof))
-        s.append('pid: ' + str(self.pid))
-        s.append('child_fd: ' + str(self.child_fd))
-        s.append('closed: ' + str(self.closed))
-        s.append('timeout: ' + str(self.timeout))
-        s.append('delimiter: ' + str(self.delimiter))
-        s.append('logfile: ' + str(self.logfile))
-        s.append('logfile_read: ' + str(self.logfile_read))
-        s.append('logfile_send: ' + str(self.logfile_send))
-        s.append('maxread: ' + str(self.maxread))
-        s.append('ignorecase: ' + str(self.ignorecase))
-        s.append('searchwindowsize: ' + str(self.searchwindowsize))
-        s.append('delaybeforesend: ' + str(self.delaybeforesend))
-        s.append('delayafterclose: ' + str(self.delayafterclose))
-        s.append('delayafterterminate: ' + str(self.delayafterterminate))
-        return '\n'.join(s)
+        s.append("version: " + __version__ + " (" + __revision__ + ")")
+        s.append("command: " + str(self.command))
+        s.append("args: " + str(self.args))
+        s.append("searcher: " + str(self.searcher))
+        s.append("buffer (last 100 chars): " + str(self.buffer)[-100:])
+        s.append("before (last 100 chars): " + str(self.before)[-100:])
+        s.append("after: " + str(self.after))
+        s.append("match: " + str(self.match))
+        s.append("match_index: " + str(self.match_index))
+        s.append("exitstatus: " + str(self.exitstatus))
+        s.append("flag_eof: " + str(self.flag_eof))
+        s.append("pid: " + str(self.pid))
+        s.append("child_fd: " + str(self.child_fd))
+        s.append("closed: " + str(self.closed))
+        s.append("timeout: " + str(self.timeout))
+        s.append("delimiter: " + str(self.delimiter))
+        s.append("logfile: " + str(self.logfile))
+        s.append("logfile_read: " + str(self.logfile_read))
+        s.append("logfile_send: " + str(self.logfile_send))
+        s.append("maxread: " + str(self.maxread))
+        s.append("ignorecase: " + str(self.ignorecase))
+        s.append("searchwindowsize: " + str(self.searchwindowsize))
+        s.append("delaybeforesend: " + str(self.delaybeforesend))
+        s.append("delayafterclose: " + str(self.delayafterclose))
+        s.append("delayafterterminate: " + str(self.delayafterterminate))
+        return "\n".join(s)
 
-    def _spawn(self,command,args=[]):
+    def _spawn(self, command, args=[]):
 
         """This starts the given command in a child process. This does all the
         fork/exec type of stuff for a pty. This is called by __init__. If args
         is empty then command will be parsed (split on spaces) and args will be
-        set to parsed arguments. """
+        set to parsed arguments."""
 
         # The pid and child_fd of this object get set by this method.
         # Note that it is difficult for this method to fail.
@@ -498,41 +540,45 @@
 
         # If command is an int type then it may represent a file descriptor.
         if type(command) == type(0):
-            raise ExceptionPexpect ('Command is an int type. If this is a file descriptor then maybe you want to use fdpexpect.fdspawn which takes an existing file descriptor instead of a command string.')
+            raise ExceptionPexpect(
+                "Command is an int type. If this is a file descriptor then maybe you want to use fdpexpect.fdspawn which takes an existing file descriptor instead of a command string."
+            )
 
-        if type (args) != type([]):
-            raise TypeError ('The argument, args, must be a list.')
+        if type(args) != type([]):
+            raise TypeError("The argument, args, must be a list.")
 
         if args == []:
             self.args = split_command_line(command)
             self.command = self.args[0]
         else:
-            self.args = args[:] # work with a copy
-            self.args.insert (0, command)
+            self.args = args[:]  # work with a copy
+            self.args.insert(0, command)
             self.command = command
 
         command_with_path = which(self.command)
         if command_with_path is None:
-            raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command)
+            raise ExceptionPexpect(
+                "The command was not found or was not executable: %s." % self.command
+            )
         self.command = command_with_path
         self.args[0] = self.command
 
-        self.name = '<' + ' '.join (self.args) + '>'
+        self.name = "<" + " ".join(self.args) + ">"
 
-        assert self.pid is None, 'The pid member should be None.'
-        assert self.command is not None, 'The command member should not be None.'
+        assert self.pid is None, "The pid member should be None."
+        assert self.command is not None, "The command member should not be None."
 
         if self.use_native_pty_fork:
             try:
                 self.pid, self.child_fd = pty.fork()
-            except OSError, e:
-                raise ExceptionPexpect('Error! pty.fork() failed: ' + str(e))
-        else: # Use internal __fork_pty
+            except OSError as e:
+                raise ExceptionPexpect("Error! pty.fork() failed: " + str(e))
+        else:  # Use internal __fork_pty
             self.pid, self.child_fd = self.__fork_pty()
 
-        if self.pid == 0: # Child
+        if self.pid == 0:  # Child
             try:
-                self.child_fd = sys.stdout.fileno() # used by setwinsize()
+                self.child_fd = sys.stdout.fileno()  # used by setwinsize()
                 self.setwinsize(24, 80)
             except:
                 # Some platforms do not like setwinsize (Cygwin).
@@ -542,9 +588,9 @@
                 pass
             # Do not allow child to inherit open file descriptors from parent.
             max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
-            for i in range (3, max_fd):
+            for i in range(3, max_fd):
                 try:
-                    os.close (i)
+                    os.close(i)
                 except OSError:
                     pass
 
@@ -581,11 +627,11 @@
 
         parent_fd, child_fd = os.openpty()
         if parent_fd < 0 or child_fd < 0:
-            raise ExceptionPexpect, "Error! Could not open pty with os.openpty()."
+            raise ExceptionPexpect("Error! Could not open pty with os.openpty().")
 
         pid = os.fork()
         if pid < 0:
-            raise ExceptionPexpect, "Error! Failed os.fork()."
+            raise ExceptionPexpect("Error! Failed os.fork().")
         elif pid == 0:
             # Child.
             os.close(parent_fd)
@@ -607,12 +653,12 @@
 
         """This makes the pseudo-terminal the controlling tty. This should be
         more portable than the pty.fork() function. Specifically, this should
-        work on Solaris. """
+        work on Solaris."""
 
         child_name = os.ttyname(tty_fd)
 
         # Disconnect from controlling tty if still connected.
-        fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY);
+        fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
         if fd >= 0:
             os.close(fd)
 
@@ -620,69 +666,74 @@
 
         # Verify we are disconnected from controlling tty
         try:
-            fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY);
+            fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
             if fd >= 0:
                 os.close(fd)
-                raise ExceptionPexpect, "Error! We are not disconnected from a controlling tty."
+                raise ExceptionPexpect(
+                    "Error! We are not disconnected from a controlling tty."
+                )
         except:
             # Good! We are disconnected from a controlling tty.
             pass
 
         # Verify we can open child pty.
-        fd = os.open(child_name, os.O_RDWR);
+        fd = os.open(child_name, os.O_RDWR)
         if fd < 0:
-            raise ExceptionPexpect, "Error! Could not open child pty, " + child_name
+            raise ExceptionPexpect("Error! Could not open child pty, " + child_name)
         else:
             os.close(fd)
 
         # Verify we now have a controlling tty.
         fd = os.open("/dev/tty", os.O_WRONLY)
         if fd < 0:
-            raise ExceptionPexpect, "Error! Could not open controlling tty, /dev/tty"
+            raise ExceptionPexpect("Error! Could not open controlling tty, /dev/tty")
         else:
             os.close(fd)
 
-    def fileno (self):   # File-like object.
+    def fileno(self):  # File-like object.
 
-        """This returns the file descriptor of the pty for the child.
-        """
+        """This returns the file descriptor of the pty for the child."""
 
         return self.child_fd
 
-    def close (self, force=True):   # File-like object.
+    def close(self, force=True):  # File-like object.
 
         """This closes the connection with the child application. Note that
         calling close() more than once is valid. This emulates standard Python
         behavior with files. Set force to True if you want to make sure that
         the child is terminated (SIGKILL is sent if the child ignores SIGHUP
-        and SIGINT). """
+        and SIGINT)."""
 
         if not self.closed:
             self.flush()
-            os.close (self.child_fd)
-            time.sleep(self.delayafterclose) # Give kernel time to update process status.
+            os.close(self.child_fd)
+            time.sleep(
+                self.delayafterclose
+            )  # Give kernel time to update process status.
             if self.isalive():
                 if not self.terminate(force):
-                    raise ExceptionPexpect ('close() could not terminate the child using terminate()')
+                    raise ExceptionPexpect(
+                        "close() could not terminate the child using terminate()"
+                    )
             self.child_fd = -1
             self.closed = True
-            #self.pid = None
+            # self.pid = None
 
-    def flush (self):   # File-like object.
+    def flush(self):  # File-like object.
 
         """This does nothing. It is here to support the interface for a
-        File-like object. """
+        File-like object."""
 
         pass
 
-    def isatty (self):   # File-like object.
+    def isatty(self):  # File-like object.
 
         """This returns True if the file descriptor is open and connected to a
-        tty(-like) device, else False. """
+        tty(-like) device, else False."""
 
         return os.isatty(self.child_fd)
 
-    def waitnoecho (self, timeout=-1):
+    def waitnoecho(self, timeout=-1):
 
         """This waits until the terminal ECHO flag is set False. This returns
         True if the echo mode is off. This returns False if the ECHO flag was
@@ -704,7 +755,7 @@
         if timeout == -1:
             timeout = self.timeout
         if timeout is not None:
-            end_time = time.time() + timeout 
+            end_time = time.time() + timeout
         while True:
             if not self.getecho():
                 return True
@@ -714,18 +765,18 @@
                 timeout = end_time - time.time()
             time.sleep(0.1)
 
-    def getecho (self):
+    def getecho(self):
 
         """This returns the terminal echo mode. This returns True if echo is
         on or False if echo is off. Child applications that are expecting you
-        to enter a password often set ECHO False. See waitnoecho(). """
+        to enter a password often set ECHO False. See waitnoecho()."""
 
         attr = termios.tcgetattr(self.child_fd)
         if attr[3] & termios.ECHO:
             return True
         return False
 
-    def setecho (self, state):
+    def setecho(self, state):
 
         """This sets the terminal echo mode on or off. Note that anything the
         child sent before the echo will be lost, so you should be sure that
@@ -766,7 +817,7 @@
         # and blocked on some platforms. TCSADRAIN is probably ideal if it worked.
         termios.tcsetattr(self.child_fd, termios.TCSANOW, attr)
 
-    def read_nonblocking (self, size = 1, timeout = -1):
+    def read_nonblocking(self, size=1, timeout=-1):
 
         """This reads at most size characters from the child application. It
         includes a timeout. If the read does not complete within the timeout
@@ -786,10 +837,10 @@
         It will not wait for 30 seconds for another 99 characters to come in.
 
         This is a wrapper around os.read(). It uses select.select() to
-        implement the timeout. """
+        implement the timeout."""
 
         if self.closed:
-            raise ValueError ('I/O operation on closed file in read_nonblocking().')
+            raise ValueError("I/O operation on closed file in read_nonblocking().")
 
         if timeout == -1:
             timeout = self.timeout
@@ -800,62 +851,72 @@
         # For this case, I test isalive() before doing any reading.
         # If isalive() is false, then I pretend that this is the same as EOF.
         if not self.isalive():
-            r,w,e = self.__select([self.child_fd], [], [], 0) # timeout of 0 means "poll"
+            r, w, e = self.__select(
+                [self.child_fd], [], [], 0
+            )  # timeout of 0 means "poll"
             if not r:
                 self.flag_eof = True
-                raise EOF ('End Of File (EOF) in read_nonblocking(). Braindead platform.')
+                raise EOF(
+                    "End Of File (EOF) in read_nonblocking(). Braindead platform."
+                )
         elif self.__irix_hack:
             # This is a hack for Irix. It seems that Irix requires a long delay before checking isalive.
             # This adds a 2 second delay, but only when the child is terminated.
             r, w, e = self.__select([self.child_fd], [], [], 2)
             if not r and not self.isalive():
                 self.flag_eof = True
-                raise EOF ('End Of File (EOF) in read_nonblocking(). Pokey platform.')
+                raise EOF("End Of File (EOF) in read_nonblocking(). Pokey platform.")
 
-        r,w,e = self.__select([self.child_fd], [], [], timeout)
+        r, w, e = self.__select([self.child_fd], [], [], timeout)
 
         if not r:
             if not self.isalive():
                 # Some platforms, such as Irix, will claim that their processes are alive;
                 # then timeout on the select; and then finally admit that they are not alive.
                 self.flag_eof = True
-                raise EOF ('End of File (EOF) in read_nonblocking(). Very pokey platform.')
+                raise EOF(
+                    "End of File (EOF) in read_nonblocking(). Very pokey platform."
+                )
             else:
-                raise TIMEOUT ('Timeout exceeded in read_nonblocking().')
+                raise TIMEOUT("Timeout exceeded in read_nonblocking().")
 
         if self.child_fd in r:
             try:
                 s = os.read(self.child_fd, size)
-            except OSError, e: # Linux does this
+            except OSError as e:  # Linux does this
                 self.flag_eof = True
-                raise EOF ('End Of File (EOF) in read_nonblocking(). Exception style platform.')
-            if s == '': # BSD style
+                raise EOF(
+                    "End Of File (EOF) in read_nonblocking(). Exception style platform."
+                )
+            if s == "":  # BSD style
                 self.flag_eof = True
-                raise EOF ('End Of File (EOF) in read_nonblocking(). Empty string style platform.')
+                raise EOF(
+                    "End Of File (EOF) in read_nonblocking(). Empty string style platform."
+                )
 
             if self.logfile is not None:
-                self.logfile.write (s)
+                self.logfile.write(s)
                 self.logfile.flush()
             if self.logfile_read is not None:
-                self.logfile_read.write (s)
+                self.logfile_read.write(s)
                 self.logfile_read.flush()
 
             return s
 
-        raise ExceptionPexpect ('Reached an unexpected state in read_nonblocking().')
+        raise ExceptionPexpect("Reached an unexpected state in read_nonblocking().")
 
-    def read (self, size = -1):   # File-like object.
+    def read(self, size=-1):  # File-like object.
 
         """This reads at most "size" bytes from the file (less if the read hits
         EOF before obtaining size bytes). If the size argument is negative or
         omitted, read all data until EOF is reached. The bytes are returned as
         a string object. An empty string is returned when EOF is encountered
-        immediately. """
+        immediately."""
 
         if size == 0:
-            return ''
+            return ""
         if size < 0:
-            self.expect (self.delimiter) # delimiter default is EOF
+            self.expect(self.delimiter)  # delimiter default is EOF
             return self.before
 
         # I could have done this more directly by not using expect(), but
@@ -865,13 +926,13 @@
         # worry about if I have to later modify read() or expect().
         # Note, it's OK if size==-1 in the regex. That just means it
         # will never match anything in which case we stop only on EOF.
-        cre = re.compile('.{%d}' % size, re.DOTALL)
-        index = self.expect ([cre, self.delimiter]) # delimiter default is EOF
+        cre = re.compile(".{%d}" % size, re.DOTALL)
+        index = self.expect([cre, self.delimiter])  # delimiter default is EOF
         if index == 0:
-            return self.after ### self.before should be ''. Should I assert this?
+            return self.after  ### self.before should be ''. Should I assert this?
         return self.before
 
-    def readline (self, size = -1):    # File-like object.
+    def readline(self, size=-1):  # File-like object.
 
         """This reads and returns one entire line. A trailing newline is kept
         in the string, but may be absent when a file ends with an incomplete
@@ -880,37 +941,35 @@
         you may expect you will receive the newline as \\r\\n. An empty string
         is returned when EOF is hit immediately. Currently, the size argument is
         mostly ignored, so this behavior is not standard for a file-like
-        object. If size is 0 then an empty string is returned. """
+        object. If size is 0 then an empty string is returned."""
 
         if size == 0:
-            return ''
-        index = self.expect (['\r\n', self.delimiter]) # delimiter default is EOF
+            return ""
+        index = self.expect(["\r\n", self.delimiter])  # delimiter default is EOF
         if index == 0:
-            return self.before + '\r\n'
+            return self.before + "\r\n"
         else:
             return self.before
 
-    def __iter__ (self):    # File-like object.
+    def __iter__(self):  # File-like object.
 
-        """This is to support iterators over a file-like object.
-        """
+        """This is to support iterators over a file-like object."""
 
         return self
 
-    def next (self):    # File-like object.
+    def next(self):  # File-like object.
 
-        """This is to support iterators over a file-like object.
-        """
+        """This is to support iterators over a file-like object."""
 
         result = self.readline()
         if result == "":
             raise StopIteration
         return result
 
-    def readlines (self, sizehint = -1):    # File-like object.
+    def readlines(self, sizehint=-1):  # File-like object.
 
         """This reads until EOF using readline() and returns a list containing
-        the lines thus read. The optional "sizehint" argument is ignored. """
+        the lines thus read. The optional "sizehint" argument is ignored."""
 
         lines = []
         while True:
@@ -920,14 +979,13 @@
             lines.append(line)
         return lines
 
-    def write(self, s):   # File-like object.
+    def write(self, s):  # File-like object.
 
-        """This is similar to send() except that there is no return value.
-        """
+        """This is similar to send() except that there is no return value."""
 
-        self.send (s)
+        self.send(s)
 
-    def writelines (self, sequence):   # File-like object.
+    def writelines(self, sequence):  # File-like object.
 
         """This calls write() for each element in the sequence. The sequence
         can be any iterable object producing strings, typically a list of
@@ -935,31 +993,31 @@
         """
 
         for s in sequence:
-            self.write (s)
+            self.write(s)
 
     def send(self, s):
 
         """This sends a string to the child process. This returns the number of
         bytes written. If a log file was set then the data is also written to
-        the log. """
+        the log."""
 
         time.sleep(self.delaybeforesend)
         if self.logfile is not None:
-            self.logfile.write (s)
+            self.logfile.write(s)
             self.logfile.flush()
         if self.logfile_send is not None:
-            self.logfile_send.write (s)
+            self.logfile_send.write(s)
             self.logfile_send.flush()
         c = os.write(self.child_fd, s)
         return c
 
-    def sendline(self, s=''):
+    def sendline(self, s=""):
 
         """This is like send(), but it adds a line feed (os.linesep). This
-        returns the number of bytes written. """
+        returns the number of bytes written."""
 
         n = self.send(s)
-        n = n + self.send (os.linesep)
+        n = n + self.send(os.linesep.encode("utf-8"))
         return n
 
     def sendcontrol(self, char):
@@ -974,19 +1032,26 @@
 
         char = char.lower()
         a = ord(char)
-        if a>=97 and a<=122:
-            a = a - ord('a') + 1
-            return self.send (chr(a))
-        d = {'@':0, '`':0,
-            '[':27, '{':27,
-            '\\':28, '|':28,
-            ']':29, '}': 29,
-            '^':30, '~':30,
-            '_':31,
-            '?':127}
+        if a >= 97 and a <= 122:
+            a = a - ord("a") + 1
+            return self.send(chr(a))
+        d = {
+            "@": 0,
+            "`": 0,
+            "[": 27,
+            "{": 27,
+            "\\": 28,
+            "|": 28,
+            "]": 29,
+            "}": 29,
+            "^": 30,
+            "~": 30,
+            "_": 31,
+            "?": 127,
+        }
         if char not in d:
             return 0
-        return self.send (chr(d[char]))
+        return self.send(chr(d[char]))
 
     def sendeof(self):
 
@@ -997,25 +1062,25 @@
         end-of-file. This means to work as expected a sendeof() has to be
         called at the beginning of a line. This method does not send a newline.
         It is the responsibility of the caller to ensure the eof is sent at the
-        beginning of a line. """
+        beginning of a line."""
 
         ### Hmmm... how do I send an EOF?
         ###C  if ((m = write(pty, *buf, p - *buf)) < 0)
         ###C      return (errno == EWOULDBLOCK) ? n : -1;
-        #fd = sys.stdin.fileno()
-        #old = termios.tcgetattr(fd) # remember current state
-        #attr = termios.tcgetattr(fd)
-        #attr[3] = attr[3] | termios.ICANON # ICANON must be set to recognize EOF
-        #try: # use try/finally to ensure state gets restored
+        # fd = sys.stdin.fileno()
+        # old = termios.tcgetattr(fd) # remember current state
+        # attr = termios.tcgetattr(fd)
+        # attr[3] = attr[3] | termios.ICANON # ICANON must be set to recognize EOF
+        # try: # use try/finally to ensure state gets restored
         #    termios.tcsetattr(fd, termios.TCSADRAIN, attr)
         #    if hasattr(termios, 'CEOF'):
         #        os.write (self.child_fd, '%c' % termios.CEOF)
         #    else:
         #        # Silly platform does not define CEOF so assume CTRL-D
         #        os.write (self.child_fd, '%c' % 4)
-        #finally: # restore state
+        # finally: # restore state
         #    termios.tcsetattr(fd, termios.TCSADRAIN, old)
-        if hasattr(termios, 'VEOF'):
+        if hasattr(termios, "VEOF"):
             char = termios.tcgetattr(self.child_fd)[6][termios.VEOF]
         else:
             # platform does not define VEOF so assume CTRL-D
@@ -1025,19 +1090,18 @@
     def sendintr(self):
 
         """This sends a SIGINT to the child. It does not require
-        the SIGINT to be the first character on a line. """
+        the SIGINT to be the first character on a line."""
 
-        if hasattr(termios, 'VINTR'):
+        if hasattr(termios, "VINTR"):
             char = termios.tcgetattr(self.child_fd)[6][termios.VINTR]
         else:
             # platform does not define VINTR so assume CTRL-C
             char = chr(3)
-        self.send (char)
+        self.send(char)
 
-    def eof (self):
+    def eof(self):
 
-        """This returns True if the EOF exception was ever raised.
-        """
+        """This returns True if the EOF exception was ever raised."""
 
         return self.flag_eof
 
@@ -1046,7 +1110,7 @@
         """This forces a child process to terminate. It starts nicely with
         SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This
         returns True if the child was terminated. This returns False if the
-        child could not be terminated. """
+        child could not be terminated."""
 
         if not self.isalive():
             return True
@@ -1071,7 +1135,7 @@
                 else:
                     return False
             return False
-        except OSError, e:
+        except OSError as e:
             # I think there are kernel timing issues that sometimes cause
             # this to happen. I think isalive() reports True, but the
             # process is dead to the kernel.
@@ -1088,25 +1152,27 @@
         not read any data from the child, so this will block forever if the
         child has unread output and has terminated. In other words, the child
         may have printed output then called exit(); but, technically, the child
-        is still alive until its output is read. """
+        is still alive until its output is read."""
 
         if self.isalive():
             pid, status = os.waitpid(self.pid, 0)
         else:
-            raise ExceptionPexpect ('Cannot wait for dead child process.')
+            raise ExceptionPexpect("Cannot wait for dead child process.")
         self.exitstatus = os.WEXITSTATUS(status)
-        if os.WIFEXITED (status):
+        if os.WIFEXITED(status):
             self.status = status
             self.exitstatus = os.WEXITSTATUS(status)
             self.signalstatus = None
             self.terminated = True
-        elif os.WIFSIGNALED (status):
+        elif os.WIFSIGNALED(status):
             self.status = status
             self.exitstatus = None
             self.signalstatus = os.WTERMSIG(status)
             self.terminated = True
-        elif os.WIFSTOPPED (status):
-            raise ExceptionPexpect ('Wait was called for a child process that is stopped. This is not supported. Is some other process attempting job control with our child pid?')
+        elif os.WIFSTOPPED(status):
+            raise ExceptionPexpect(
+                "Wait was called for a child process that is stopped. This is not supported. Is some other process attempting job control with our child pid?"
+            )
         return self.exitstatus
 
     def isalive(self):
@@ -1115,7 +1181,7 @@
         non-blocking. If the child was terminated then this will read the
         exitstatus or signalstatus of the child. This returns True if the child
         process appears to be running or False if not. It can take literally
-        SECONDS for Solaris to return the right status. """
+        SECONDS for Solaris to return the right status."""
 
         if self.terminated:
             return False
@@ -1130,9 +1196,11 @@
 
         try:
             pid, status = os.waitpid(self.pid, waitpid_options)
-        except OSError, e: # No child processes
+        except OSError as e:  # No child processes
             if e[0] == errno.ECHILD:
-                raise ExceptionPexpect ('isalive() encountered condition where "terminated" is 0, but there was no child process. Did someone else call waitpid() on our process?')
+                raise ExceptionPexpect(
+                    'isalive() encountered condition where "terminated" is 0, but there was no child process. Did someone else call waitpid() on our process?'
+                )
             else:
                 raise e
 
@@ -1141,10 +1209,14 @@
         # report, and the value of status is undefined.
         if pid == 0:
             try:
-                pid, status = os.waitpid(self.pid, waitpid_options) ### os.WNOHANG) # Solaris!
-            except OSError, e: # This should never happen...
+                pid, status = os.waitpid(
+                    self.pid, waitpid_options
+                )  ### os.WNOHANG) # Solaris!
+            except OSError as e:  # This should never happen...
                 if e[0] == errno.ECHILD:
-                    raise ExceptionPexpect ('isalive() encountered condition that should never happen. There was no child process. Did someone else call waitpid() on our process?')
+                    raise ExceptionPexpect(
+                        "isalive() encountered condition that should never happen. There was no child process. Did someone else call waitpid() on our process?"
+                    )
                 else:
                     raise e
 
@@ -1158,25 +1230,27 @@
         if pid == 0:
             return True
 
-        if os.WIFEXITED (status):
+        if os.WIFEXITED(status):
             self.status = status
             self.exitstatus = os.WEXITSTATUS(status)
             self.signalstatus = None
             self.terminated = True
-        elif os.WIFSIGNALED (status):
+        elif os.WIFSIGNALED(status):
             self.status = status
             self.exitstatus = None
             self.signalstatus = os.WTERMSIG(status)
             self.terminated = True
-        elif os.WIFSTOPPED (status):
-            raise ExceptionPexpect ('isalive() encountered condition where child process is stopped. This is not supported. Is some other process attempting job control with our child pid?')
+        elif os.WIFSTOPPED(status):
+            raise ExceptionPexpect(
+                "isalive() encountered condition where child process is stopped. This is not supported. Is some other process attempting job control with our child pid?"
+            )
         return False
 
     def kill(self, sig):
 
         """This sends the given signal to the child application. In keeping
         with UNIX tradition it has a misleading name. It does not necessarily
-        kill the child unless you send the right signal. """
+        kill the child unless you send the right signal."""
 
         # Same as os.kill, but the pid is given for you.
         if self.isalive():
@@ -1209,28 +1283,31 @@
 
         if patterns is None:
             return []
-        if type(patterns) is not types.ListType:
+        if type(patterns) is not list:
             patterns = [patterns]
 
-        compile_flags = re.DOTALL # Allow dot to match \n
+        compile_flags = re.DOTALL  # Allow dot to match \n
         if self.ignorecase:
             compile_flags = compile_flags | re.IGNORECASE
         compiled_pattern_list = []
         for p in patterns:
-            if type(p) in types.StringTypes:
+            if type(p) is str:
                 compiled_pattern_list.append(re.compile(p, compile_flags))
             elif p is EOF:
                 compiled_pattern_list.append(EOF)
             elif p is TIMEOUT:
                 compiled_pattern_list.append(TIMEOUT)
-            elif type(p) is type(re.compile('')):
+            elif type(p) is type(re.compile("")):
                 compiled_pattern_list.append(p)
             else:
-                raise TypeError ('Argument must be one of StringTypes, EOF, TIMEOUT, SRE_Pattern, or a list of those type. %s' % str(type(p)))
+                raise TypeError(
+                    "Argument must be one of StringTypes, EOF, TIMEOUT, SRE_Pattern, or a list of those type. %s"
+                    % str(type(p))
+                )
 
         return compiled_pattern_list
 
-    def expect(self, pattern, timeout = -1, searchwindowsize=None):
+    def expect(self, pattern, timeout=-1, searchwindowsize=None):
 
         """This seeks through the stream until a pattern is matched. The
         pattern is overloaded and may take several types. The pattern can be a
@@ -1310,7 +1387,7 @@
         compiled_pattern_list = self.compile_pattern_list(pattern)
         return self.expect_list(compiled_pattern_list, timeout, searchwindowsize)
 
-    def expect_list(self, pattern_list, timeout = -1, searchwindowsize = -1):
+    def expect_list(self, pattern_list, timeout=-1, searchwindowsize=-1):
 
         """This takes a list of compiled regular expressions and returns the
         index into the pattern_list that matched the child output. The list may
@@ -1320,11 +1397,11 @@
         may help if you are trying to optimize for speed, otherwise just use
         the expect() method.  This is called by expect(). If timeout==-1 then
         the self.timeout value is used. If searchwindowsize==-1 then the
-        self.searchwindowsize value is used. """
+        self.searchwindowsize value is used."""
 
         return self.expect_loop(searcher_re(pattern_list), timeout, searchwindowsize)
 
-    def expect_exact(self, pattern_list, timeout = -1, searchwindowsize = -1):
+    def expect_exact(self, pattern_list, timeout=-1, searchwindowsize=-1):
 
         """This is similar to expect(), but uses plain string matching instead
         of compiled regular expressions in 'pattern_list'. The 'pattern_list'
@@ -1338,51 +1415,53 @@
         This method is also useful when you don't want to have to worry about
         escaping regular expression characters that you want to match."""
 
-        if type(pattern_list) in types.StringTypes or pattern_list in (TIMEOUT, EOF):
+        if type(pattern_list) is str or pattern_list in (TIMEOUT, EOF):
             pattern_list = [pattern_list]
-        return self.expect_loop(searcher_string(pattern_list), timeout, searchwindowsize)
+        return self.expect_loop(
+            searcher_string(pattern_list), timeout, searchwindowsize
+        )
 
-    def expect_loop(self, searcher, timeout = -1, searchwindowsize = -1):
+    def expect_loop(self, searcher, timeout=-1, searchwindowsize=-1):
 
         """This is the common loop used inside expect. The 'searcher' should be
         an instance of searcher_re or searcher_string, which describes how and what
         to search for in the input.
 
-        See expect() for other arguments, return value and exceptions. """
+        See expect() for other arguments, return value and exceptions."""
 
         self.searcher = searcher
 
         if timeout == -1:
             timeout = self.timeout
         if timeout is not None:
-            end_time = time.time() + timeout 
+            end_time = time.time() + timeout
         if searchwindowsize == -1:
             searchwindowsize = self.searchwindowsize
 
         try:
             incoming = self.buffer
             freshlen = len(incoming)
-            while True: # Keep reading until exception or return.
+            while True:  # Keep reading until exception or return.
                 index = searcher.search(incoming, freshlen, searchwindowsize)
                 if index >= 0:
-                    self.buffer = incoming[searcher.end : ]
-                    self.before = incoming[ : searcher.start]
+                    self.buffer = incoming[searcher.end :]
+                    self.before = incoming[: searcher.start]
                     self.after = incoming[searcher.start : searcher.end]
                     self.match = searcher.match
                     self.match_index = index
                     return self.match_index
                 # No match at this point
                 if timeout < 0 and timeout is not None:
-                    raise TIMEOUT ('Timeout exceeded in expect_any().')
+                    raise TIMEOUT("Timeout exceeded in expect_any().")
                 # Still have time left, so read more data
-                c = self.read_nonblocking (self.maxread, timeout)
+                c = self.read_nonblocking(self.maxread, timeout).decode("utf-8")
                 freshlen = len(c)
-                time.sleep (0.0001)
+                time.sleep(0.0001)
                 incoming = incoming + c
                 if timeout is not None:
                     timeout = end_time - time.time()
-        except EOF, e:
-            self.buffer = ''
+        except EOF as e:
+            self.buffer = ""
             self.before = incoming
             self.after = EOF
             index = searcher.eof_index
@@ -1393,8 +1472,8 @@
             else:
                 self.match = None
                 self.match_index = None
-                raise EOF (str(e) + '\n' + str(self))
-        except TIMEOUT, e:
+                raise EOF(str(e) + "\n" + str(self))
+        except TIMEOUT as e:
             self.buffer = incoming
             self.before = incoming
             self.after = TIMEOUT
@@ -1406,7 +1485,7 @@
             else:
                 self.match = None
                 self.match_index = None
-                raise TIMEOUT (str(e) + '\n' + str(self))
+                raise TIMEOUT(str(e) + "\n" + str(self))
         except:
             self.before = incoming
             self.after = None
@@ -1417,12 +1496,12 @@
     def getwinsize(self):
 
         """This returns the terminal window size of the child tty. The return
-        value is a tuple of (rows, cols). """
+        value is a tuple of (rows, cols)."""
 
-        TIOCGWINSZ = getattr(termios, 'TIOCGWINSZ', 1074295912L)
-        s = struct.pack('HHHH', 0, 0, 0, 0)
+        TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", 1074295912)
+        s = struct.pack("HHHH", 0, 0, 0, 0)
         x = fcntl.ioctl(self.fileno(), TIOCGWINSZ, s)
-        return struct.unpack('HHHH', x)[0:2]
+        return struct.unpack("HHHH", x)[0:2]
 
     def setwinsize(self, r, c):
 
@@ -1430,7 +1509,7 @@
         a SIGWINCH signal to be sent to the child. This does not change the
         physical window size. It changes the size reported to TTY-aware
         applications like vi or curses -- applications that respond to the
-        SIGWINCH signal. """
+        SIGWINCH signal."""
 
         # Check for buggy platforms. Some Python versions on some platforms
         # (notably OSF1 Alpha and RedHat 7.1) truncate the value for
@@ -1440,14 +1519,14 @@
         # TIOCSWINSZ and they don't have a truncate problem.
         # Newer versions of Linux have totally different values for TIOCSWINSZ.
         # Note that this fix is a hack.
-        TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561)
-        if TIOCSWINSZ == 2148037735L: # L is not required in Python >= 2.2.
-            TIOCSWINSZ = -2146929561 # Same bits, but with sign.
+        TIOCSWINSZ = getattr(termios, "TIOCSWINSZ", -2146929561)
+        if TIOCSWINSZ == 2148037735:  # L is not required in Python >= 2.2.
+            TIOCSWINSZ = -2146929561  # Same bits, but with sign.
         # Note, assume ws_xpixel and ws_ypixel are zero.
-        s = struct.pack('HHHH', r, c, 0, 0)
+        s = struct.pack("HHHH", r, c, 0, 0)
         fcntl.ioctl(self.fileno(), TIOCSWINSZ, s)
 
-    def interact(self, escape_character = chr(29), input_filter = None, output_filter = None):
+    def interact(self, escape_character=chr(29), input_filter=None, output_filter=None):
 
         """This gives control of the child process to the interactive user (the
         human at the keyboard). Keystrokes are sent to the child process, and
@@ -1483,9 +1562,9 @@
         """
 
         # Flush the buffer.
-        self.stdout.write (self.buffer)
+        self.stdout.write(self.buffer)
         self.stdout.flush()
-        self.buffer = ''
+        self.buffer = ""
         mode = tty.tcgetattr(self.STDIN_FILENO)
         tty.setraw(self.STDIN_FILENO)
         try:
@@ -1495,37 +1574,38 @@
 
     def __interact_writen(self, fd, data):
 
-        """This is used by the interact() method.
-        """
+        """This is used by the interact() method."""
 
-        while data != '' and self.isalive():
+        while data != "" and self.isalive():
             n = os.write(fd, data)
             data = data[n:]
 
     def __interact_read(self, fd):
 
-        """This is used by the interact() method.
-        """
+        """This is used by the interact() method."""
 
         return os.read(fd, 1000)
 
-    def __interact_copy(self, escape_character = None, input_filter = None, output_filter = None):
+    def __interact_copy(
+        self, escape_character=None, input_filter=None, output_filter=None
+    ):
 
-        """This is used by the interact() method.
-        """
+        """This is used by the interact() method."""
 
         while self.isalive():
-            r,w,e = self.__select([self.child_fd, self.STDIN_FILENO], [], [])
+            r, w, e = self.__select([self.child_fd, self.STDIN_FILENO], [], [])
             if self.child_fd in r:
                 data = self.__interact_read(self.child_fd)
-                if output_filter: data = output_filter(data)
+                if output_filter:
+                    data = output_filter(data)
                 if self.logfile is not None:
-                    self.logfile.write (data)
+                    self.logfile.write(data)
                     self.logfile.flush()
                 os.write(self.STDOUT_FILENO, data)
             if self.STDIN_FILENO in r:
                 data = self.__interact_read(self.STDIN_FILENO)
-                if input_filter: data = input_filter(data)
+                if input_filter:
+                    data = input_filter(data)
                 i = data.rfind(escape_character)
                 if i != -1:
                     data = data[:i]
@@ -1533,12 +1613,12 @@
                     break
                 self.__interact_writen(self.child_fd, data)
 
-    def __select (self, iwtd, owtd, ewtd, timeout=None):
+    def __select(self, iwtd, owtd, ewtd, timeout=None):
 
         """This is a wrapper around select.select() that ignores signals. If
         select.select raises a select.error exception and errno is an EINTR
         error then it is ignored. Mainly this is used to ignore sigwinch
-        (terminal resize). """
+        (terminal resize)."""
 
         # if select() is interrupted by a signal (errno==EINTR) then
         # we loop back and enter the select() again.
@@ -1546,39 +1626,44 @@
             end_time = time.time() + timeout
         while True:
             try:
-                return select.select (iwtd, owtd, ewtd, timeout)
-            except select.error, e:
+                return select.select(iwtd, owtd, ewtd, timeout)
+            except select.error as e:
                 if e[0] == errno.EINTR:
                     # if we loop back we have to subtract the amount of time we already waited.
                     if timeout is not None:
                         timeout = end_time - time.time()
                         if timeout < 0:
-                            return ([],[],[])
-                else: # something else caused the select.error, so this really is an exception
+                            return ([], [], [])
+                else:  # something else caused the select.error, so this really is an exception
                     raise
 
-##############################################################################
-# The following methods are no longer supported or allowed.
+    ##############################################################################
+    # The following methods are no longer supported or allowed.
 
-    def setmaxread (self, maxread):
+    def setmaxread(self, maxread):
 
         """This method is no longer supported or allowed. I don't like getters
-        and setters without a good reason. """
+        and setters without a good reason."""
 
-        raise ExceptionPexpect ('This method is no longer supported or allowed. Just assign a value to the maxread member variable.')
+        raise ExceptionPexpect(
+            "This method is no longer supported or allowed. Just assign a value to the maxread member variable."
+        )
 
-    def setlog (self, fileobject):
+    def setlog(self, fileobject):
 
-        """This method is no longer supported or allowed.
-        """
+        """This method is no longer supported or allowed."""
 
-        raise ExceptionPexpect ('This method is no longer supported or allowed. Just assign a value to the logfile member variable.')
+        raise ExceptionPexpect(
+            "This method is no longer supported or allowed. Just assign a value to the logfile member variable."
+        )
+
 
 ##############################################################################
 # End of spawn class
 ##############################################################################
 
-class searcher_string (object):
+
+class searcher_string(object):
 
     """This is a plain string search helper for the spawn.expect_any() method.
 
@@ -1598,7 +1683,7 @@
     def __init__(self, strings):
 
         """This creates an instance of searcher_string. This argument 'strings'
-        may be a list; a sequence of strings; or the EOF or TIMEOUT types. """
+        may be a list; a sequence of strings; or the EOF or TIMEOUT types."""
 
         self.eof_index = -1
         self.timeout_index = -1
@@ -1617,15 +1702,15 @@
         """This returns a human-readable string that represents the state of
         the object."""
 
-        ss =  [ (ns[0],'    %d: "%s"' % ns) for ns in self._strings ]
-        ss.append((-1,'searcher_string:'))
+        ss = [(ns[0], '    %d: "%s"' % ns) for ns in self._strings]
+        ss.append((-1, "searcher_string:"))
         if self.eof_index >= 0:
-            ss.append ((self.eof_index,'    %d: EOF' % self.eof_index))
+            ss.append((self.eof_index, "    %d: EOF" % self.eof_index))
         if self.timeout_index >= 0:
-            ss.append ((self.timeout_index,'    %d: TIMEOUT' % self.timeout_index))
+            ss.append((self.timeout_index, "    %d: TIMEOUT" % self.timeout_index))
         ss.sort()
         ss = zip(*ss)[1]
-        return '\n'.join(ss)
+        return "\n".join(ss)
 
     def search(self, buffer, freshlen, searchwindowsize=None):
 
@@ -1637,7 +1722,7 @@
         See class spawn for the 'searchwindowsize' argument.
 
         If there is a match this returns the index of that string, and sets
-        'start', 'end' and 'match'. Otherwise, this returns -1. """
+        'start', 'end' and 'match'. Otherwise, this returns -1."""
 
         absurd_match = len(buffer)
         first_match = absurd_match
@@ -1653,12 +1738,12 @@
         # rescanning until we've read three more bytes.
         #
         # Sadly, I don't know enough about this interesting topic. /grahn
-        
+
         for index, s in self._strings:
             if searchwindowsize is None:
                 # the match, if any, can only be in the fresh data,
                 # or at the very end of the old data
-                offset = -(freshlen+len(s))
+                offset = -(freshlen + len(s))
             else:
                 # better obey searchwindowsize
                 offset = -searchwindowsize
@@ -1673,7 +1758,8 @@
         self.end = self.start + len(self.match)
         return best_index
 
-class searcher_re (object):
+
+class searcher_re(object):
 
     """This is regular expression string search helper for the
     spawn.expect_any() method.
@@ -1715,15 +1801,18 @@
         """This returns a human-readable string that represents the state of
         the object."""
 
-        ss =  [ (n,'    %d: re.compile("%s")' % (n,str(s.pattern))) for n,s in self._searches]
-        ss.append((-1,'searcher_re:'))
+        ss = [
+            (n, '    %d: re.compile("%s")' % (n, str(s.pattern)))
+            for n, s in self._searches
+        ]
+        ss.append((-1, "searcher_re:"))
         if self.eof_index >= 0:
-            ss.append ((self.eof_index,'    %d: EOF' % self.eof_index))
+            ss.append((self.eof_index, "    %d: EOF" % self.eof_index))
         if self.timeout_index >= 0:
-            ss.append ((self.timeout_index,'    %d: TIMEOUT' % self.timeout_index))
+            ss.append((self.timeout_index, "    %d: TIMEOUT" % self.timeout_index))
         ss.sort()
-        ss = zip(*ss)[1]
-        return '\n'.join(ss)
+        ss = list(zip(*ss))[1]
+        return "\n".join(ss)
 
     def search(self, buffer, freshlen, searchwindowsize=None):
 
@@ -1732,7 +1821,7 @@
         'buffer' which have not been searched before.
 
         See class spawn for the 'searchwindowsize' argument.
-        
+
         If there is a match this returns the index of that string, and sets
         'start', 'end' and 'match'. Otherwise, returns -1."""
 
@@ -1743,7 +1832,7 @@
         if searchwindowsize is None:
             searchstart = 0
         else:
-            searchstart = max(0, len(buffer)-searchwindowsize)
+            searchstart = max(0, len(buffer) - searchwindowsize)
         for index, s in self._searches:
             match = s.search(buffer, searchstart)
             if match is None:
@@ -1760,26 +1849,27 @@
         self.end = self.match.end()
         return best_index
 
-def which (filename):
+
+def which(filename):
 
     """This takes a given filename; tries to find it in the environment path;
     then checks if it is executable. This returns the full path to the filename
     if found and executable. Otherwise this returns None."""
 
     # Special case where filename already contains a path.
-    if os.path.dirname(filename) != '':
-        if os.access (filename, os.X_OK):
+    if os.path.dirname(filename) != "":
+        if os.access(filename, os.X_OK):
             return filename
 
-    if not os.environ.has_key('PATH') or os.environ['PATH'] == '':
+    if not os.environ.get("PATH", None) or os.environ["PATH"] == "":
         p = os.defpath
     else:
-        p = os.environ['PATH']
+        p = os.environ["PATH"]
 
     # Oddly enough this was the one line that made Pexpect
     # incompatible with Python 1.5.2.
-    #pathlist = p.split (os.pathsep)
-    pathlist = string.split (p, os.pathsep)
+    # pathlist = p.split (os.pathsep)
+    pathlist = p.split(os.pathsep)
 
     for path in pathlist:
         f = os.path.join(path, filename)
@@ -1787,39 +1877,40 @@
             return f
     return None
 
+
 def split_command_line(command_line):
 
     """This splits a command line into a list of arguments. It splits arguments
     on spaces, but handles embedded quotes, doublequotes, and escaped
     characters. It's impossible to do this with a regular expression, so I
-    wrote a little state machine to parse the command line. """
+    wrote a little state machine to parse the command line."""
 
     arg_list = []
-    arg = ''
+    arg = ""
 
     # Constants to name the states we can be in.
     state_basic = 0
     state_esc = 1
     state_singlequote = 2
     state_doublequote = 3
-    state_whitespace = 4 # The state of consuming whitespace between commands.
+    state_whitespace = 4  # The state of consuming whitespace between commands.
     state = state_basic
 
     for c in command_line:
         if state == state_basic or state == state_whitespace:
-            if c == '\\': # Escape the next character
+            if c == "\\":  # Escape the next character
                 state = state_esc
-            elif c == r"'": # Handle single quote
+            elif c == r"'":  # Handle single quote
                 state = state_singlequote
-            elif c == r'"': # Handle double quote
+            elif c == r'"':  # Handle double quote
                 state = state_doublequote
             elif c.isspace():
                 # Add arg to arg_list if we aren't in the middle of whitespace.
                 if state == state_whitespace:
-                    None # Do nothing.
+                    None  # Do nothing.
                 else:
                     arg_list.append(arg)
-                    arg = ''
+                    arg = ""
                     state = state_whitespace
             else:
                 arg = arg + c
@@ -1838,8 +1929,9 @@
             else:
                 arg = arg + c
 
-    if arg != '':
+    if arg != "":
         arg_list.append(arg)
     return arg_list
 
+
 # vi:ts=4:sw=4:expandtab:ft=python:
diff --git a/tests/test_crypto.py b/tests/test_crypto.py
new file mode 100755
index 0000000..cd2f31c
--- /dev/null
+++ b/tests/test_crypto.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+import unittest
+
+from paths import topbuilddir
+
+TESTDIR = os.path.realpath(os.path.dirname(__file__))
+
+
+class TestCrypto(unittest.TestCase):
+    def test_crypto(self):
+        process = subprocess.Popen(
+            [os.path.join(TESTDIR, "fips_scanner.sh")], stdout=subprocess.PIPE
+        )
+        (stdout, stderr) = process.communicate()
+        self.assertEqual(
+            process.returncode,
+            0,
+            "ERROR: Found potential non-FIPS compliant calls:\n{stdout}".format(
+                stdout=stdout
+            ),
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_duo_split_at.py b/tests/test_duo_split_at.py
new file mode 100755
index 0000000..ebd3ed2
--- /dev/null
+++ b/tests/test_duo_split_at.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+import unittest
+
+from paths import topbuilddir
+
+BUILDDIR = topbuilddir
+
+
+def testutil_duo_split_at(args):
+    return (
+        subprocess.check_output(
+            [os.path.join(BUILDDIR, "lib", "testutil_duo_split_at")] + args
+        )
+        .decode("utf-8")
+        .strip()
+    )
+
+
+class TestDuoSplitAt(unittest.TestCase):
+    def test_basic(self):
+        self.assertEqual(testutil_duo_split_at(["foo/bar/baz", "/", "1", "bar"]), "OK")
+
+    def test_first(self):
+        self.assertEqual(testutil_duo_split_at(["foo/bar/baz", "/", "0", "foo"]), "OK")
+
+    def test_last(self):
+        self.assertEqual(testutil_duo_split_at(["foo/bar/baz", "/", "2", "baz"]), "OK")
+
+    def test_too_many(self):
+        self.assertEqual(
+            testutil_duo_split_at(["foo/bar/baz", "/", "100", "NULL"]), "OK"
+        )
+
+    def test_no_delimiter(self):
+        self.assertEqual(testutil_duo_split_at(["foo", "/", "1", "NULL"]), "OK")
+
+    def test_starts_with_delimiter(self):
+        self.assertEqual(testutil_duo_split_at(["/foo/bar/baz", "/", "0", ""]), "OK")
+
+    def test_ends_with_delimiter(self):
+        self.assertEqual(testutil_duo_split_at(["foo/bar/baz/", "/", "3", ""]), "OK")
+
+    def test_empty(self):
+        self.assertEqual(testutil_duo_split_at(["", "/", "0", ""]), "OK")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_login_duo.py b/tests/test_login_duo.py
new file mode 100755
index 0000000..3ae52c8
--- /dev/null
+++ b/tests/test_login_duo.py
@@ -0,0 +1,707 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+import sys
+import time
+import unittest
+from tempfile import NamedTemporaryFile
+
+import pexpect
+from common_suites import NORMAL_CERT, CommonSuites, fips_available
+from config import (
+    MOCKDUO_ADMINS_NO_USERS,
+    MOCKDUO_AUTOPUSH,
+    MOCKDUO_CONF,
+    MOCKDUO_FIPS,
+    MOCKDUO_GECOS_DEFAULT_DELIM_6_POS,
+    MOCKDUO_GECOS_DEPRECATED_PARSE_FLAG,
+    MOCKDUO_GECOS_INVALID_DELIM_COLON,
+    MOCKDUO_GECOS_INVALID_DELIM_PUNC,
+    MOCKDUO_GECOS_INVALID_DELIM_WHITESPACE,
+    MOCKDUO_GECOS_INVALID_POS,
+    MOCKDUO_GECOS_LONG_DELIM,
+    MOCKDUO_GECOS_SEND_UNPARSED,
+    MOCKDUO_GECOS_SLASH_DELIM_3_POS,
+    MOCKDUO_USERS,
+    MOCKDUO_USERS_ADMINS,
+    MOTD_CONF,
+    DuoUnixConfig,
+    TempConfig,
+)
+from mockduo_context import MockDuo
+from paths import topbuilddir
+
+BUILDDIR = topbuilddir
+TESTDIR = os.path.realpath(os.path.dirname(__file__))
+
+
+class LoginDuoTimeoutException(Exception):
+    def __init__(self, message="", stdout=None, stderr=None):
+        self.message = message
+        self.stdout = stdout
+        self.stderr = stderr
+
+    def __str__(self):
+        if self.stderr:
+            stderr_output = "STDERR:\n{stderr}".format(stderr=self.stderr)
+        else:
+            stderr_output = ""
+
+        if self.stdout:
+            stdout_output = "STDOUT:\n{stdout}".format(stdout=self.stdout)
+        else:
+            stdout_output = ""
+
+        return "Timeout waiting for 'login_duo' to execute\n{message}\n{stdout}\n{stderr}".format(
+            mesage=self.message,
+            stderr=stderr_output,
+            stdout=stdout_output,
+        )
+
+
+def login_duo_interactive(args, env=None, preload_script=""):
+    if env is None:
+        env = {}
+
+    excluded_keys = ["SSH_CONNECTION", "FALLBACK", "UID", "http_proxy", "TIMEOUT"]
+    env_passthrough = {
+        key: os.environ[key] for key in os.environ if key not in excluded_keys
+    }
+    env_passthrough.update(env)
+
+    if preload_script != "":
+        login_duo_path = "python3"
+        args = [preload_script] + args
+    else:
+        login_duo_path = os.path.join(BUILDDIR, "login_duo", "login_duo")
+
+    process = pexpect.spawn(login_duo_path, args, cwd=TESTDIR, env=env_passthrough)
+    return process
+
+
+def login_duo(args, env=None, timeout=10, preload_script=""):
+    """Runs the login_duo binary in various ways
+    args: the list of arguments to pass through to either login_duo or login_duo.py
+
+    env: list of environment variables to pass to login_duo
+
+    timeout: how long to allow login_duo or login_duo.py to run before raising an exception
+
+    preload_script: whether or not to use a wrapping script to allow the caller to load
+    a custom preload library for mocking out certain parts of login_duo
+    """
+    if env is None:
+        env = {}
+
+    if preload_script != "":
+        login_duo_path = ["python3", preload_script]
+    else:
+        login_duo_path = [os.path.join(BUILDDIR, "login_duo", "login_duo")]
+
+    excluded_keys = ["SSH_CONNECTION", "FALLBACK", "UID", "http_proxy", "TIMEOUT"]
+    env_passthrough = {
+        key: os.environ[key] for key in os.environ if key not in excluded_keys
+    }
+    env_passthrough.update(env)
+
+    process = subprocess.Popen(
+        login_duo_path + args,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        stdin=subprocess.PIPE,
+        cwd=TESTDIR,
+        close_fds=True,
+        env=env_passthrough,
+    )
+
+    # Try to gracefully handle the case where we get a prompt
+    for i in range(0, int(timeout // 0.05)):
+        process.poll()
+        if process.returncode is not None:
+            break
+        time.sleep(0.05)
+    else:
+        (stdout, stderr) = process.communicate(input=b"1\r\n")
+        raise LoginDuoTimeoutException(
+            "login_duo unexpectedly blocked for user input", stdout, stderr
+        )
+
+    stdout = process.stdout.read().decode("utf-8").split("\n")
+    stderr = process.stderr.read().decode("utf-8").split("\n")
+    process.stdout.close()
+    process.stderr.close()
+    process.stdin.close()
+    return {
+        "returncode": process.returncode,
+        "stdout": stdout,
+        "stderr": stderr,
+    }
+
+
+class TestLoginDuoConfigs(CommonSuites.Configuration):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoDown(CommonSuites.DuoDown):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoSelfSignedCert(CommonSuites.DuoSelfSignedCert):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoBadCN(CommonSuites.DuoBadCN):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestMockDuoWithValidCert(CommonSuites.WithValidCert):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoPreauthStates(CommonSuites.PreauthStates):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoHosts(CommonSuites.Hosts):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoHTTPProxy(CommonSuites.HTTPProxy):
+    def call_binary(self, *args, **kwargs):
+        return login_duo(*args)
+
+
+class TestLoginDuoGetHostname(CommonSuites.GetHostname):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginDuoFIPS(CommonSuites.FIPS):
+    def call_binary(self, *args, **kwargs):
+        return login_duo(*args, **kwargs)
+
+
+class TestLoginDuoPreauthFailures(CommonSuites.PreauthFailures):
+    def call_binary(self, *args):
+        return login_duo(*args)
+
+
+class TestLoginBSON(CommonSuites.InvalidBSON):
+    def call_binary(self, *args, **kwargs):
+        return login_duo(*args, **kwargs)
+
+
+class TestLoginDuoConfig(unittest.TestCase):
+    def test_empty_args(self):
+        """Test to see how login_duo handles an empty string argument (we do need a valid argument also)"""
+        result = login_duo(["", "-h"])
+        self.assertRegex(
+            result["stderr"][0], ".*login_duo: option requires an argument.*"
+        )
+        self.assertEqual(
+            result["stderr"][1],
+            "Usage: login_duo [-v] [-c config] [-d] [-f duouser] [-h host] [prog [args...]]",
+        )
+        self.assertEqual(result["returncode"], 1)
+
+    def test_help_output(self):
+        """Basic help output"""
+        result = login_duo(["-h"])
+        self.assertRegex(
+            result["stderr"][0], ".*login_duo: option requires an argument.*"
+        )
+        self.assertEqual(
+            result["stderr"][1],
+            "Usage: login_duo [-v] [-c config] [-d] [-f duouser] [-h host] [prog [args...]]",
+        )
+        self.assertEqual(result["returncode"], 1)
+
+    def test_version_output(self):
+        """Check version output"""
+        result = login_duo(["-v"])
+        self.assertRegex(result["stderr"][0], "login_duo \d+\.\d+.\d+")
+
+
+class TestLoginDuoEnv(CommonSuites.Env):
+    def call_binary(self, *args, **kwargs):
+        return login_duo(*args)
+
+
+class TestLoginDuoSpecificEnv(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoSpecificEnv, self).run(result)
+
+    def test_missing_uid(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "timeout", "true"],
+                env={
+                    "TIMEOUT": "1",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Who are you?",
+            )
+
+    def test_command_from_env(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "preauth-allow"],
+                env={
+                    "UID": "1001",
+                    "SSH_ORIGINAL_COMMAND": "echo 'hello'",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stdout"][0],
+                r"hello",
+            )
+
+    def test_env_factor(self):
+        config = DuoUnixConfig(
+            ikey="DIXYZV6YM8IFYVWBINCA",
+            skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+            host="localhost:4443",
+            cafile="certs/mockduo-ca.pem",
+            accept_env_factor="yes",
+        )
+
+        with TempConfig(config) as temp:
+            process = login_duo_interactive(
+                ["-d", "-c", temp.name, "-f", "whatever", "echo", "SUCCESS"],
+                env={
+                    "UID": "1001",
+                    "DUO_PASSCODE": "push1",
+                },
+            )
+            self.assertEqual(process.expect("SUCCESS", timeout=10), 0)
+
+
+class TestLoginDuoUIDMismatch(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoUIDMismatch, self).run(result)
+
+    def test_nonroot(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "preauth-allow"],
+                env={
+                    "EUID": "1002",
+                    "UID": "1001",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Only root may specify -c or -f",
+            )
+
+    def test_sync(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "whatever", "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Successful Duo login for 'whatever'",
+            )
+
+    def test_unprivileged(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d"],
+                env={
+                    "EUID": "1000",
+                    "UID": "1001",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+                timeout=10,
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"couldn't drop privileges:",
+            )
+
+    def test_privsep_user_not_found(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d"],
+                env={
+                    "EUID": "0",
+                    "UID": "1001",
+                    "NO_PRIVSEP_USER": "1",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+                timeout=10,
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"User .* not found",
+            )
+
+
+class TestLoginDuoTimeout(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoTimeout, self).run(result)
+
+    def test_connection_timeout(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "timeout", "true"],
+                env={
+                    "UID": "1001",
+                    "TIMEOUT": "1",
+                },
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+                timeout=10,
+            )
+            for line in result["stderr"][:3]:
+                self.assertEqual(line, "Attempting connection")
+            self.assertRegex(
+                result["stderr"][3],
+                r"Failsafe Duo login for 'timeout': Couldn't connect to localhost:4443: Failed to connect",
+            )
+
+
+class TestLoginDuoShell(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoShell, self).run(result)
+
+    def test_default_shell(self):
+        """Test that we fallback to /bin/sh if there is no shell specified for the user"""
+        with TempConfig(MOCKDUO_AUTOPUSH) as temp:
+            process = login_duo_interactive(
+                ["-d", "-c", temp.name],
+                env={"PS1": "$ ", "UID": "1015"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            # this double escaping is needed to check for a literal "$"
+            self.assertEqual(process.expect("\\$", timeout=10), 0)
+
+    def test_shell_as_command(self):
+        with TempConfig(MOCKDUO_AUTOPUSH) as temp:
+            process = login_duo_interactive(
+                ["-d", "-c", temp.name, "echo", "SUCCESS"],
+                env={"PS1": "> ", "UID": "1017"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertEqual(process.expect("-c echo SUCCESS", timeout=10), 0)
+
+
+class TestLoginDuoGroups(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoGroups, self).run(result)
+
+    def test_users_only_match_users(self):
+        for uid in range(1000, 1003):
+            with TempConfig(MOCKDUO_USERS) as temp:
+                result = login_duo(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={
+                        "UID": str(uid),
+                    },
+                    preload_script=os.path.join(TESTDIR, "groups.py"),
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow': preauth-allowed",
+                )
+
+    def test_users_or_admins_match_users(self):
+        for uid in range(1000, 1004):
+            with TempConfig(MOCKDUO_USERS_ADMINS) as temp:
+                result = login_duo(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                    env={
+                        "UID": str(uid),
+                    },
+                    preload_script=os.path.join(TESTDIR, "groups.py"),
+                )
+                self.assertRegex(
+                    result["stderr"][0],
+                    r"Skipped Duo login for 'preauth-allow': preauth-allowed",
+                )
+
+    def test_admins_and_not_users_match_admins(self):
+        with TempConfig(MOCKDUO_ADMINS_NO_USERS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                env={
+                    "UID": "1003",
+                },
+                preload_script=os.path.join(TESTDIR, "groups.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Skipped Duo login for 'preauth-allow': preauth-allowed",
+            )
+
+    def test_users_bypass(self):
+        with TempConfig(MOCKDUO_USERS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "-f", "preauth-allow", "true"],
+                env={"UID": "1004"},
+                preload_script=os.path.join(TESTDIR, "groups.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"User preauth-allow bypassed Duo 2FA due to user's UNIX group",
+            )
+
+
+class TestLoginDuoInteractive(CommonSuites.Interactive):
+    def call_binary(self, *args, **kwargs):
+        return login_duo_interactive(*args, **kwargs)
+
+
+class TestLoginDuoGECOS(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestLoginDuoGECOS, self).run(result)
+
+    def test_gecos_field_unparsed(self):
+        with TempConfig(MOCKDUO_GECOS_SEND_UNPARSED) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1010"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Successful Duo login for '1/2/3/4/5/gecos_user_gecos_field6'",
+            )
+
+    def test_deprecated_gecos_parsed_flag(self):
+        with TempConfig(MOCKDUO_GECOS_DEPRECATED_PARSE_FLAG) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1010"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"The gecos_parsed configuration item for Duo Unix is deprecated and no longer has any effect. Use gecos_delim and gecos_username_pos instead",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                "Skipped Duo login for 'gecos/6': gecos/6",
+            )
+
+    def test_gecos_delimiter_default_position_6(self):
+        with TempConfig(MOCKDUO_GECOS_DEFAULT_DELIM_6_POS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1012"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                "Skipped Duo login for 'gecos_user_gecos_field6': gecos-user-gecos-field6-allowed",
+            )
+
+    def test_gecos_delimiter_slash_position_3(self):
+        with TempConfig(MOCKDUO_GECOS_SLASH_DELIM_3_POS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1011"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Skipped Duo login for 'gecos_user_gecos_field3': gecos-user-gecos-field3-allowed",
+            )
+
+    def test_gecos_parsing_error(self):
+        with TempConfig(MOCKDUO_GECOS_SLASH_DELIM_3_POS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1012"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Could not parse GECOS field",
+            )
+
+    def test_gecos_empty(self):
+        with TempConfig(MOCKDUO_GECOS_SEND_UNPARSED) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+                env={"UID": "1016"},
+                preload_script=os.path.join(TESTDIR, "login_duo.py"),
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Empty GECOS field",
+            )
+
+    def test_gecos_invalid_delimiter_length(self):
+        with TempConfig(MOCKDUO_GECOS_LONG_DELIM) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Invalid character option length. Character fields must be 1 character long: ',,'",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid login_duo option: 'gecos_delim'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+    def test_invalid_delimiter_value(self):
+        for config in [
+            MOCKDUO_GECOS_INVALID_DELIM_COLON,
+            MOCKDUO_GECOS_INVALID_DELIM_PUNC,
+        ]:
+            with TempConfig(config) as temp:
+                result = login_duo(
+                    ["-d", "-c", temp.name, "true"],
+                )
+                self.assertEqual(
+                    result["stderr"][0],
+                    "Invalid gecos_delim '{delim}' (delimiter must be punctuation other than ':')".format(
+                        delim=config["gecos_delim"]
+                    ),
+                )
+                self.assertRegex(
+                    result["stderr"][1],
+                    r"Invalid login_duo option: 'gecos_delim'",
+                )
+                self.assertRegex(
+                    result["stderr"][2],
+                    r"Parse error in {config}, line \d+".format(config=temp.name),
+                )
+
+    def test_invalid_delimiter_value_whitespace(self):
+        with TempConfig(MOCKDUO_GECOS_INVALID_DELIM_WHITESPACE) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertEqual(
+                result["stderr"][0],
+                "Invalid character option length. Character fields must be 1 character long: ''",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid login_duo option: 'gecos_delim'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+    def test_invalid_pos_value(self):
+        with TempConfig(MOCKDUO_GECOS_INVALID_POS) as temp:
+            result = login_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertEqual(
+                result["stderr"][0],
+                "Gecos position starts at 1",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid login_duo option: 'gecos_username_pos'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+
+@unittest.skipIf(
+    sys.platform == "darwin" or sys.platform == "sunos5",
+    reason="MOTD testing not available on Mac and Solaris",
+)
+class TestMOTD(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestMOTD, self).run(result)
+
+    def test_motd(self):
+        test_motd = "test_motd"
+        with TempConfig(MOTD_CONF) as temp:
+            try:
+                # I don't know why this test doesn't play nice with normal temp files
+                # either a race condition or a permissions issue but we have to do this instead
+                with open("/tmp/duo_unix_test_motd", "w") as fh:
+                    fh.write(test_motd + "\n")
+                process = login_duo_interactive(
+                    ["-d", "-c", temp.name, "-f", "whatever", "echo", "SUCCESS"],
+                    env={
+                        "UID": "1001",
+                        "MOTD_FILE": "/tmp/duo_unix_test_motd",
+                    },
+                    preload_script=os.path.join(TESTDIR, "login_duo.py"),
+                )
+                process.sendline(b"1")
+                self.assertEqual(process.expect(test_motd, timeout=10), 0)
+            finally:
+                try:
+                    os.remove("/tmp/duo_unix_test_motd")
+                except Exception:
+                    pass
+
+    def test_motd_with_ssh_command(self):
+        test_motd = "test_motd"
+        with TempConfig(MOTD_CONF) as temp:
+            with TempConfig(test_motd + "\n") as motd_file:
+                process = login_duo_interactive(
+                    ["-d", "-c", temp.name, "-f", "whatever", "echo", "SUCCESS"],
+                    env={
+                        "UID": "1001",
+                        "SSH_ORIGINAL_COMMAND": "ls",
+                        "MOTD_FILE": motd_file.name,
+                    },
+                    preload_script=os.path.join(TESTDIR, "login_duo.py"),
+                )
+                process.sendline(b"1")
+            self.assertEqual(process.expect([test_motd, pexpect.EOF], timeout=5), 1)
+
+    def test_motd_users_bypass(self):
+        bypass_config = DuoUnixConfig(
+            ikey="DIXYZV6YM8IFYVWBINCA",
+            skey="yWHSMhWucAcp7qvuH3HWTaSaKABs8Gaddiv1NIRo",
+            host="localhost:4443",
+            cafile="certs/mockduo-ca.pem",
+            groups="users",
+            motd="yes",
+        )
+        test_motd = "test_motd"
+        with TempConfig(bypass_config) as temp:
+            with TempConfig(test_motd + "\n") as motd_file:
+                process = login_duo_interactive(
+                    ["-d", "-c", temp.name, "-f", "preauth-allow", "echo", "SUCCESS"],
+                    env={
+                        "UID": "1004",
+                        "MOTD_FILE": motd_file.name,
+                    },
+                    preload_script=os.path.join(TESTDIR, "groups.py"),
+                )
+                process.sendline(b"1")
+                self.assertEqual(process.expect(test_motd, timeout=10), 0)
+                self.assertEqual(process.expect("SUCCESS", timeout=10), 0)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_pam_duo.py b/tests/test_pam_duo.py
new file mode 100755
index 0000000..b3a82d8
--- /dev/null
+++ b/tests/test_pam_duo.py
@@ -0,0 +1,437 @@
+#!/usr/bin/env python3
+import getpass
+import os
+import subprocess
+import time
+import unittest
+
+import pexpect
+from common_suites import NORMAL_CERT, CommonSuites
+from config import (
+    MOCKDUO_CONF,
+    MOCKDUO_GECOS_DEFAULT_DELIM_6_POS,
+    MOCKDUO_GECOS_DEPRECATED_PARSE_FLAG,
+    MOCKDUO_GECOS_INVALID_DELIM_COLON,
+    MOCKDUO_GECOS_INVALID_DELIM_PUNC,
+    MOCKDUO_GECOS_INVALID_DELIM_WHITESPACE,
+    MOCKDUO_GECOS_INVALID_POS,
+    MOCKDUO_GECOS_LONG_DELIM,
+    MOCKDUO_GECOS_SEND_UNPARSED,
+    MOCKDUO_GECOS_SLASH_DELIM_3_POS,
+    MOCKDUO_PROMPTS_1,
+    MOCKDUO_PROMPTS_DEFAULT,
+    TempConfig,
+)
+from mockduo_context import MockDuo
+from paths import topbuilddir
+from testpam import TempPamConfig, testpam
+
+TESTDIR = os.path.realpath(os.path.dirname(__file__))
+
+
+class PamDuoTimeoutException(Exception):
+    def __init__(self, stdout=None, stderr=None):
+        self.stdout = stdout
+        self.stderr = stderr
+
+    def __str__(self):
+        if self.stderr:
+            stderr_output = "STDERR:\n{stderr}".format(stderr=self.stderr)
+        else:
+            stderr_output = ""
+
+        if self.stdout:
+            stdout_output = "STDOUT:\n{stdout}".format(stdout=self.stdout)
+        else:
+            stdout_output = ""
+
+        return "Timeout waiting for 'pam_duo' to execute\n{stdout}\n{stderr}".format(
+            stderr=stderr_output,
+            stdout=stdout_output,
+        )
+
+
+def pam_duo_interactive(args, env={}, timeout=2):
+    pam_duo_path = os.path.join(TESTDIR, "testpam.py")
+    # we don't want to accidentally grab these from the calling environment
+    excluded_keys = ["SSH_CONNECTION", "FALLBACK", "UID", "http_proxy", "TIMEOUT"]
+    env_passthrough = {
+        key: os.environ[key] for key in os.environ if key not in excluded_keys
+    }
+    env_passthrough.update(env)
+
+    process = pexpect.spawn(
+        pam_duo_path,
+        args,
+        cwd=TESTDIR,
+        env=env_passthrough,
+    )
+    return process
+
+
+def pam_duo(args, env={}, timeout=2):
+    pam_duo_path = [os.path.join(TESTDIR, "testpam.py")]
+    # we don't want to accidentally grab these from the calling environment
+    excluded_keys = ["SSH_CONNECTION", "FALLBACK", "UID", "http_proxy", "TIMEOUT"]
+    env_passthrough = {
+        key: os.environ[key] for key in os.environ if key not in excluded_keys
+    }
+    env_passthrough.update(env)
+
+    process = subprocess.Popen(
+        pam_duo_path + args,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        stdin=subprocess.PIPE,
+        cwd=TESTDIR,
+        close_fds=True,
+        env=env_passthrough,
+    )
+
+    # Try to gracefully handle the case where we get a prompt
+    for i in range(0, int(timeout // 0.05)):
+        process.poll()
+        if process.returncode is not None:
+            break
+        time.sleep(0.05)
+    else:
+        (stdout, stderr) = process.communicate(input=b"1\r\n")
+        raise PamDuoTimeoutException(stdout, stderr)
+
+    stdout_lines = process.stdout.read().decode("utf-8").split("\n")
+    stderr_lines = process.stderr.read().decode("utf-8").split("\n")
+
+    process.stdout.close()
+    process.stderr.close()
+    process.stdin.close()
+
+    try:
+        process.terminate()
+    except:
+        pass
+
+    return {
+        "returncode": process.returncode,
+        "stdout": stdout_lines,
+        "stderr": stderr_lines,
+    }
+
+
+class TestPamDuoHelp(unittest.TestCase):
+    def test_help(self):
+        result = pam_duo(["-h"])
+        self.assertRegex(
+            result["stderr"][0],
+            r"Usage: .*/tests/testpam.py \[-d\] \[-c config\] \[-f user\] \[-h host\]",
+        )
+
+
+class TestPamDuoConfigs(CommonSuites.Configuration):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamDuoDown(CommonSuites.DuoDown):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamSelfSignedCerts(CommonSuites.DuoSelfSignedCert):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamDuoBadCN(CommonSuites.DuoBadCN):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamValidCerts(CommonSuites.WithValidCert):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamPreauthStates(CommonSuites.PreauthStates):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamHosts(CommonSuites.Hosts):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(timeout=15, *args, **kwargs)
+
+
+class TestPamHTTPProxy(CommonSuites.HTTPProxy):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(*args, **kwargs)
+
+
+class TestPamFIPS(CommonSuites.FIPS):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(*args, **kwargs)
+
+
+class TestPamGetHostname(CommonSuites.GetHostname):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(*args, **kwargs)
+
+
+class TestPamBSON(CommonSuites.InvalidBSON):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(*args, **kwargs)
+
+
+class TestPamPrompts(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestPamPrompts, self).run(result)
+
+    def test_max_prompts_equals_one(self):
+        with TempConfig(MOCKDUO_PROMPTS_1) as temp:
+            result = pam_duo(["-d", "-f", "pam_prompt", "-c", temp.name, "true"])
+            self.assertRegex(result["stderr"][0], "Failed Duo login for 'pam_prompt'")
+            self.assertRegex(
+                result["stdout"][0], "Autopushing login request to phone..."
+            )
+            self.assertRegex(result["stdout"][1], "Invalid passcode, please try again.")
+
+    def test_max_prompts_equals_maximum(self):
+        with TempConfig(MOCKDUO_PROMPTS_DEFAULT) as temp:
+            result = pam_duo(["-d", "-f", "pam_prompt", "-c", temp.name, "true"])
+            for i in range(0, 3):
+                self.assertRegex(
+                    result["stderr"][i], "Failed Duo login for 'pam_prompt'"
+                )
+
+            for i in range(0, 6, 2):
+                self.assertRegex(
+                    result["stdout"][i], "Autopushing login request to phone..."
+                )
+                self.assertRegex(
+                    result["stdout"][i + 1], "Invalid passcode, please try again."
+                )
+
+
+class TestPamEnv(CommonSuites.Env):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo(*args, **kwargs)
+
+
+class TestPamSpecificEnv(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestPamSpecificEnv, self).run(result)
+
+    def test_no_user(self):
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = pam_duo(["-d", "-c", temp.name], env={"NO_USER": "1"})
+            self.assertEqual(result["returncode"], 1)
+
+    def test_su_service_bad_user(self):
+        """Test that we return user unknown if we can't find the calling user"""
+        with TempConfig(MOCKDUO_CONF) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name],
+                env={"SIMULATE_SERVICE": "su", "NO_USER": "1"},
+            )
+            self.assertEqual(result["returncode"], 1)
+
+
+class TestPamPreauthFailures(CommonSuites.PreauthFailures):
+    def call_binary(self, *args):
+        return pam_duo(*args)
+
+
+class TestPamDuoInteractive(CommonSuites.Interactive):
+    def call_binary(self, *args, **kwargs):
+        return pam_duo_interactive(*args, **kwargs)
+
+    def test_su_service(self):
+        """Test that the -f option is ignored if the service is Su"""
+        with TempConfig(MOCKDUO_CONF) as temp:
+            process = self.call_binary(
+                ["-d", "-c", temp.name, "-f", "foobar", "true"],
+                env={"SIMULATE_SERVICE": "su"},
+            )
+            # This is here to prevent race conditions with character entry
+            process.expect(CommonSuites.Interactive.PROMPT_REGEX, timeout=10)
+            process.sendline(b"2")
+            self.assertEqual(process.expect(pexpect.EOF), 0)
+            user = getpass.getuser()
+            self.assertOutputEqual(
+                process.before,
+                [
+                    "2",
+                    "Dialing XXX-XXX-1234...",
+                    "Answered. Press '#' on your phone to log in.",
+                    "Success. Logging you in...",
+                    "[6] Successful Duo login for '{user}'".format(user=user),
+                ],
+            )
+
+
+class TestPamdConf(unittest.TestCase):
+    def test_invalid_argument(self):
+        with TempConfig(MOCKDUO_CONF) as duo_config:
+            pamd_conf = "auth  required  {libpath}/pam_duo.so conf={duo_config_path} notanarg".format(
+                libpath=os.path.join(topbuilddir, "pam_duo", ".libs"),
+                duo_config_path=duo_config.name,
+            )
+            with TempPamConfig(pamd_conf) as pam_config:
+                process = testpam(
+                    ["-d", "-c", duo_config.name, "-f", "whatever"], pam_config.name
+                )
+                self.assertEqual(process.returncode, 1)
+
+
+class TestPamGECOS(unittest.TestCase):
+    def run(self, result=None):
+        with MockDuo(NORMAL_CERT):
+            return super(TestPamGECOS, self).run(result)
+
+    def test_gecos_field_unparsed(self):
+        with TempConfig(MOCKDUO_GECOS_SEND_UNPARSED) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "-f", "fullgecos", "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Skipped Duo login for 'full_gecos_field': full-gecos-field",
+            )
+
+    def test_deprecated_gecos_parsed_flag(self):
+        with TempConfig(MOCKDUO_GECOS_DEPRECATED_PARSE_FLAG) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "-f", "gecos/6", "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"The gecos_parsed configuration item for Duo Unix is deprecated and no longer has any effect. Use gecos_delim and gecos_username_pos instead",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                "Skipped Duo login for 'gecos/6': gecos/6",
+            )
+
+    def test_gecos_delimiter_default_position_6(self):
+        with TempConfig(MOCKDUO_GECOS_DEFAULT_DELIM_6_POS) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "-f", "gecos,6", "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                "Skipped Duo login for 'gecos_user_gecos_field6': gecos-user-gecos-field6-allowed",
+            )
+
+    def test_gecos_delimiter_slash_position_3(self):
+        with TempConfig(MOCKDUO_GECOS_SLASH_DELIM_3_POS) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "-f", "gecos/3", "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Skipped Duo login for 'gecos_user_gecos_field3': gecos-user-gecos-field3-allowed",
+            )
+
+    def test_gecos_invalid_delimiter_length(self):
+        with TempConfig(MOCKDUO_GECOS_LONG_DELIM) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertRegex(
+                result["stderr"][0],
+                r"Invalid character option length. Character fields must be 1 character long: ',,'",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid pam_duo option: 'gecos_delim'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+    def test_invalid_delimiter_value_colon(self):
+        for config in [
+            MOCKDUO_GECOS_INVALID_DELIM_COLON,
+            MOCKDUO_GECOS_INVALID_DELIM_PUNC,
+        ]:
+            with TempConfig(config) as temp:
+                result = pam_duo(
+                    ["-d", "-c", temp.name, "true"],
+                )
+                self.assertEqual(
+                    result["stderr"][0],
+                    "Invalid gecos_delim '{delim}' (delimiter must be punctuation other than ':')".format(
+                        delim=config["gecos_delim"]
+                    ),
+                )
+                self.assertRegex(
+                    result["stderr"][1],
+                    r"Invalid pam_duo option: 'gecos_delim'",
+                )
+                self.assertRegex(
+                    result["stderr"][2],
+                    r"Parse error in {config}, line \d+".format(config=temp.name),
+                )
+
+    def test_invalid_delimiter_value_whitespace(self):
+        with TempConfig(MOCKDUO_GECOS_INVALID_DELIM_WHITESPACE) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertEqual(
+                result["stderr"][0],
+                "Invalid character option length. Character fields must be 1 character long: ''",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid pam_duo option: 'gecos_delim'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+    def test_invalid_pos_value(self):
+        with TempConfig(MOCKDUO_GECOS_INVALID_POS) as temp:
+            result = pam_duo(
+                ["-d", "-c", temp.name, "true"],
+            )
+            self.assertEqual(
+                result["stderr"][0],
+                "Gecos position starts at 1",
+            )
+            self.assertRegex(
+                result["stderr"][1],
+                r"Invalid pam_duo option: 'gecos_username_pos'",
+            )
+            self.assertRegex(
+                result["stderr"][2],
+                r"Parse error in {config}, line \d+".format(config=temp.name),
+            )
+
+    def test_gecos_parsing_error(self):
+        with TempConfig(MOCKDUO_GECOS_SLASH_DELIM_3_POS) as temp:
+            process = pam_duo_interactive(
+                ["-d", "-c", temp.name, "-f", "gecos,3"],
+            )
+            self.assertEqual(process.expect("Could not parse GECOS field"), 0)
+
+    def test_gecos_only_delim(self):
+        with TempConfig(MOCKDUO_GECOS_DEFAULT_DELIM_6_POS) as temp:
+            process = pam_duo_interactive(
+                ["-d", "-c", temp.name, "-f", "onlydelim"],
+            )
+            self.assertEqual(process.expect("Could not parse GECOS field"), 0)
+
+    def test_gecos_empty(self):
+        with TempConfig(MOCKDUO_GECOS_SEND_UNPARSED) as temp:
+            process = pam_duo_interactive(
+                ["-d", "-c", temp.name, "-f", "emptygecos"],
+            )
+            self.assertEqual(process.expect("Empty GECOS field"), 0)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/testpam.c b/tests/testpam.c
index b80de2f..32ad5e7 100644
--- a/tests/testpam.c
+++ b/tests/testpam.c
@@ -103,7 +103,7 @@
     if (argc > 2)
         host = argv[2];
 
-    if ((ret = pam_start("testpam", user, &conv, &pamh)) != PAM_SUCCESS) {
+    if ((ret = pam_start("test_duo_unix_service", user, &conv, &pamh)) != PAM_SUCCESS) {
                 die(pamh, ret);
     }
     if (host != NULL) {
diff --git a/tests/testpam.py b/tests/testpam.py
index 19b4757..aea7e7b 100755
--- a/tests/testpam.py
+++ b/tests/testpam.py
@@ -1,71 +1,106 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
+import argparse
 import getopt
 import getpass
 import os
+import platform
 import subprocess
 import sys
 import tempfile
-import platform
 
 import paths
 
 # login_duo-compatible wrapper to pam_duo
 
+PAM_SERVICE = "test_duo_unix_service"
+PAM_SERVICE_PATH = os.path.join("/etc", "pam.d", "test_duo_unix_service")
+
+
 def usage():
-    print >>sys.stderr, 'Usage: %s [-d] [-c config] [-f user] [-h host]' % \
-          sys.argv[0]
+    print(
+        "Usage: {0} [-d] [-c config] [-f user] [-h host]".format(sys.argv[0]),
+        file=sys.stderr,
+    )
     sys.exit(1)
-    
+
+
+class TempPamConfig(object):
+    def __init__(self, config):
+        self.config = config
+        try:
+            self.file = open(PAM_SERVICE_PATH, "wb")
+        except PermissionError as e:
+            raise Exception(
+                "Permission denied opening pam.d make sure you run tests with elevated permissions"
+            ) from e
+
+    def __enter__(self):
+        if sys.platform == "sunos5":
+            self.file.write(PAM_SERVICE.decode("utf8") + b" ")
+        self.file.write(self.config.encode("utf-8"))
+        self.file.flush()
+        return self.file
+
+    def __exit__(self, type, value, traceback):
+        self.file.close()
+        os.remove(PAM_SERVICE_PATH)
+
+
+def testpam(args, config_file_name, env_overrides=None):
+    env = os.environ.copy()
+    env["PAM_CONF"] = config_file_name
+    env["PAM_SERVICE"] = PAM_SERVICE
+
+    if env_overrides:
+        env.update(env_overrides)
+
+    if sys.platform == "darwin":
+        env["DYLD_LIBRARY_PATH"] = paths.topbuilddir + "/lib/.libs"
+        env["DYLD_INSERT_LIBRARIES"] = paths.build + "/.libs/libtestpam_preload.dylib"
+        env["DYLD_FORCE_FLAT_NAMESPACE"] = "1"
+    elif sys.platform == "sunos5":
+        architecture = {"32bit": "32", "64bit": "64"}[platform.architecture()[0]]
+        env["LD_PRELOAD_" + architecture] = paths.build + "/.libs/libtestpam_preload.so"
+    else:
+        env["LD_PRELOAD"] = paths.build + "/.libs/libtestpam_preload.so"
+
+    testpam_path = [os.path.join(paths.build, "testpam")]
+    p = subprocess.Popen(testpam_path + args, env=env)
+    p.wait()
+    return p
+
+
 def main():
     try:
-        opts, args = getopt.getopt(sys.argv[1:], 'dc:f:h:')
+        opts, args = getopt.getopt(sys.argv[1:], "dc:f:h:")
     except getopt.GetoptError:
         usage()
 
-    opt_conf = '/etc/duo/pam_duo.conf'
+    opt_conf = "/etc/duo/pam_duo.conf"
     opt_user = getpass.getuser()
     opt_host = None
-    
+
     for o, a in opts:
-        if o == '-c':
+        if o == "-c":
             opt_conf = a
-        elif o == '-f':
+        elif o == "-f":
             opt_user = a
-        elif o == '-h':
+        elif o == "-h":
             opt_host = a
 
-    args = [ paths.build + '/testpam', opt_user ]
+    args = [opt_user]
     if opt_host:
         args.append(opt_host)
-    
-    f = tempfile.NamedTemporaryFile()
-    #f = open('/tmp/pam.conf', 'w')
-    if sys.platform == 'sunos5':
-        f.write('testpam ')
-    f.write('auth  required  %s/pam_duo.so conf=%s debug' %
-            (paths.topbuilddir + '/pam_duo/.libs', opt_conf))
-    f.flush()
-    
-    env = os.environ.copy()
-    env['PAM_CONF'] = f.name
 
-    if sys.platform == 'darwin':
-        env['DYLD_LIBRARY_PATH'] = paths.topbuilddir + '/lib/.libs'
-        env['DYLD_INSERT_LIBRARIES'] = paths.build + \
-                                       '/.libs/libtestpam_preload.dylib'
-        env['DYLD_FORCE_FLAT_NAMESPACE'] = '1'
-    elif sys.platform == 'sunos5':
-        architecture = {'32bit': '32', '64bit': '64'}[platform.architecture()[0]]
-        env['LD_PRELOAD_' + architecture] = paths.build + '/.libs/libtestpam_preload.so'
-    else:
-        env['LD_PRELOAD'] = paths.build + '/.libs/libtestpam_preload.so'
-        
-    p = subprocess.Popen(args, env=env)
-    p.wait()
-    f.close()
-    
-    sys.exit(p.returncode)
+    config = "auth  required  {libpath}/pam_duo.so conf={duo_config_path} debug".format(
+        libpath=paths.topbuilddir + "/pam_duo/.libs", duo_config_path=opt_conf
+    )
+    with TempPamConfig(config) as config_file:
+        process = testpam(args, config_file.name)
 
-if __name__ == '__main__':
+    sys.exit(process.returncode)
+
+
+if __name__ == "__main__":
     main()
diff --git a/tests/testpam_preload.c b/tests/testpam_preload.c
index fb60efc..b075783 100644
--- a/tests/testpam_preload.c
+++ b/tests/testpam_preload.c
@@ -18,6 +18,16 @@
 #include <stdlib.h>
 #include <unistd.h>
 
+#ifdef HAVE_SECURITY_PAM_APPL_H
+#include <security/pam_appl.h>
+#endif
+#ifdef HAVE_SECURITY_PAM_MODULES_H
+#include <security/pam_modules.h>
+#endif
+#ifdef HAVE_SECURITY_PAM_EXT_H
+#include <security/pam_ext.h>  /* Linux-PAM */
+#endif
+
 #ifdef __APPLE__
 # define _PATH_LIBC       "libc.dylib"
 #elif defined(__linux__)
@@ -26,58 +36,16 @@
 # define _PATH_LIBC       "libc.so"
 #endif
 
-static void _preload_init(void) __attribute((constructor));
-
 int (*_sys_open)(const char *pathname, int flags, ...);
 int (*_sys_open64)(const char *pathname, int flags, ...);
 FILE *(*_sys_fopen)(const char *filename, const char *mode);
 FILE *(*_sys_fopen64)(const char *filename, const char *mode);
 char *(*_sys_inet_ntoa)(struct in_addr in);
+struct passwd *(* _getpwuid)(uid_t uid);
+int (*_pam_get_item)(const pam_handle_t *pamh, int item_type, const void **item);
 
 void modify_gecos(const char *username, struct passwd *pass);
 
-static void
-_fatal(const char *msg)
-{
-	perror(msg);
-	exit(1);
-}
-
-static void
-_preload_init(void)
-{
-	void *libc;
-
-#ifndef DL_LAZY
-# define DL_LAZY RTLD_LAZY
-#endif
-	if (!(libc = dlopen(_PATH_LIBC, DL_LAZY))) {
-		_fatal("couldn't dlopen " _PATH_LIBC);
-	} else if (!(_sys_open = dlsym(libc, "open"))) {
-		_fatal("couldn't dlsym 'open'");
-#ifdef HAVE_OPEN64
-	} else if (!(_sys_open = dlsym(libc, "open64"))) {
-		_fatal("couldn't dlsym 'open64'");
-#endif
-	} else if (!(_sys_fopen = dlsym(libc, "fopen"))) {
-		_fatal("couldn't dlsym 'fopen'");
-#ifdef HAVE_FOPEN64
-	} else if (!(_sys_fopen64 = dlsym(libc, "fopen64"))) {
-		_fatal("couldn't dlsym 'fopen64'");
-#endif
-	}
-}
-
-const char *
-_replace(const char *filename)
-{
-	if (strcmp(filename, "/etc/pam.d/testpam") == 0 ||
-            strcmp(filename, "/etc/pam.conf") == 0) {
-		return (getenv("PAM_CONF"));
-	}
-	return (filename);
-}
-
 int
 _isfallback(void)
 {
@@ -85,30 +53,6 @@
         return (t ? atoi(t) : 0);
 }
 
-int
-open(const char *filename, int flags, ...)
-{
-	return ((*_sys_open)(_replace(filename), flags));
-}
-
-int
-open64(const char *filename, int flags, ...)
-{
-	return ((*_sys_open64)(_replace(filename), flags));
-}
-
-FILE *
-fopen(const char *filename, const char *mode)
-{
-	return ((*_sys_fopen)(_replace(filename), mode));
-}
-
-FILE *
-fopen64(const char *filename, const char *mode)
-{
-	return ((*_sys_fopen64)(_replace(filename), mode));
-}
-
 char *
 inet_ntoa(struct in_addr in)
 {
@@ -134,12 +78,37 @@
        pass->pw_gecos = strdup("1,2,gecos_user_gecos_field3,4,5,6");
     } else if (strcmp(username, "fullgecos") == 0) {
        pass->pw_gecos = strdup("full_gecos_field");
+    } else if (strcmp(username, "fullgecos") == 0) {
+       pass->pw_gecos = strdup("full_gecos_field");
+    } else if (strcmp(username, "emptygecos") == 0) {
+       pass->pw_gecos = strdup("");
+    } else if (strcmp(username, "onlydelim") == 0) {
+       pass->pw_gecos = strdup(",,,,,,,");
     }
 }
 
 struct passwd *
+getpwuid(uid_t uid)
+{
+    char *t = getenv("NO_USER");
+    if(t) {
+        return NULL;
+    }
+    else {
+        _getpwuid = dlsym(RTLD_NEXT, "getpwuid");
+        return (*_getpwuid)(uid);
+    }
+}
+
+
+struct passwd *
 getpwnam(const char *name)
 {
+    char *t = getenv("NO_USER");
+    if(t) {
+        return NULL;
+    }
+
     // Tests rely on the username being correctly set.
     static char username[1024];
     strncpy(username, name, 1024);
@@ -152,3 +121,15 @@
 
     return &ret;
 }
+
+int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) {
+    if(item_type == PAM_SERVICE) {
+        char *s = getenv("SIMULATE_SERVICE");
+        if(s) {
+            *item = s;
+            return PAM_SUCCESS;
+        }
+    }
+    _pam_get_item  = dlsym(RTLD_NEXT, "pam_get_item");
+    return (*_pam_get_item)(pamh, item_type, item);
+}
diff --git a/tests/unity_tests/Makefile.am b/tests/unity_tests/Makefile.am
index 7428727..ca46a72 100644
--- a/tests/unity_tests/Makefile.am
+++ b/tests/unity_tests/Makefile.am
@@ -3,14 +3,14 @@
 
 SUBDIRS = $(UNITY_VERSION)
 
-UNITY_TESTS = unityrunner add_param_test common_ini_wrong_flag_test common_ini_failmode_test common_ini_prompts_test common_ini_https_timeout common_ini_string_options common_ini_bool_options_test add_groupname_test gecos_ini_test bson_iter_init_test bson_iter_next_test
+UNITY_TESTS = unityrunner add_param_test common_ini_wrong_flag_test common_ini_failmode_test common_ini_prompts_test common_ini_https_timeout common_ini_string_options common_ini_bool_options_test add_groupname_test gecos_ini_test
 
 # Create the unity runner executable
 check_PROGRAMS = $(UNITY_TESTS)
 include_HEADERS = common_ini_test.h
 
 LDADD = $(top_builddir)/lib/libduo.la $(top_builddir)/compat/libcompat.la $(top_builddir)/tests/unity_tests/$(UNITY_VERSION)/libunity.la
-CFLAGS = -Werror -Wunused-function
+CFLAGS = @CFLAGS@ -Werror -Wunused-function
 
 if PAM
 UNITY_TESTS += pam_argv_parse_test
diff --git a/tests/unity_tests/bson_iter_init_test.c b/tests/unity_tests/bson_iter_init_test.c
deleted file mode 100644
index 7a43788..0000000
--- a/tests/unity_tests/bson_iter_init_test.c
+++ /dev/null
@@ -1,63 +0,0 @@
-#include "common_ini_test.h"
-#include "bson.h"
-
-/* Set to true if the test has reached the error_test function */
-int reached_error_test;
-
-void error_test(int i, const char* msg) {
-    /* Mock out bson_fatal_msg */
-    reached_error_test = 1;
-    printf("Reached function pointer: %d %s\n", i, msg);
-}
-
-/* The format of this BSON message is:
- *  size_of_message...bson_type key.size_of_value...value.bson_type KEY.size_of_VALUE...VALUE..
- */
-static void test_bson_iter_init_success() {
-    /* Test if bson_iterator_init properly sets the curSize and maxBufferSize */
-    bson_iterator it;
-    /* 35...2key.6...value.2KEY.6...VALUE.. */
-    char msg[35] = "\x23\x00\x00\x00\x02\x6b\x65\x79\x00\x06\x00\x00\x00\x76\x61\x6c\x75\x65\x00\x02\x4b\x45\x59\x00\x06\x00\x00\x00\x56\x41\x4c\x55\x45\x00\x00";
-    int msg_size = 35;
-    int expected_cur_size = 4;
-    int expected_max_buff_size = msg_size;
-    void (*func_ptr)(int, const char*);
-    func_ptr = &error_test;
-
-    bson_iterator_init(&it, msg, msg_size, func_ptr);
-    TEST_ASSERT_EQUAL(expected_cur_size, it.curSize);
-    TEST_ASSERT_EQUAL(expected_max_buff_size, it.maxBufferSize);
-}
-
-static void test_bson_iter_init_four_size() {
-    /* Test that bson_iterator_init fails if the BSON message has exactly 4 bytes */
-    bson_iterator it;
-    /* 4..1 */
-    char msg[4] = "\x04\x00\x00\x01";
-    char *expected_error_msg = "Invalid BSON response";
-    int msg_size = 4;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-static void test_bson_iter_init_five_size() {
-    /* Test that bson_iterator_init succeeds if the BSON message has exactly 5 bytes */
-    bson_iterator it;
-    /* 5...1 */
-    char msg[5] = "\x05\x00\x00\x00\x01";
-    int msg_size = 5;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    TEST_ASSERT_FALSE(reached_error_test);
-}
-
-int main() {
-    UNITY_BEGIN();
-    RUN_TEST(test_bson_iter_init_success);
-    RUN_TEST(test_bson_iter_init_four_size);
-    RUN_TEST(test_bson_iter_init_five_size);
-    return UNITY_END();
-}
diff --git a/tests/unity_tests/bson_iter_next_test.c b/tests/unity_tests/bson_iter_next_test.c
deleted file mode 100644
index 0cf4969..0000000
--- a/tests/unity_tests/bson_iter_next_test.c
+++ /dev/null
@@ -1,205 +0,0 @@
-
-#include "common_ini_test.h"
-#include "bson.h"
-
-/* Set to true if the test has reached the error_test function */
-int reached_error_test;
-
-void error_test(int i, const char* msg) {
-    /* Mock out bson_fatal_msg */
-    reached_error_test = 1;
-    printf("Reached function pointer: %d %s\n", i, msg);
-}
-
-/* The format of the shorter BSON messages:
- *  size_of_message...bson_type key.size_of_value...value..
- */
-static void test_bson_iter_next_basic_success() {
-    /* Test if bson_iterator_next successfully skips over the value "Value" and returns the end of the BSON string */
-    bson_iterator it;
-    /* 20...2Key.6...Value.. */
-    char msg[20] = "\x14\x00\x00\x00\x02\x4b\x65\x79\x00\x06\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    int expected_size = msg_size - 1;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    /* If it.first is 1, which it is by default, bson_iterator_next will return it.cur */
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_EQUAL(expected_size, it.curSize);
-}
-
-/* The format of this BSON message is:
- *  size_of_message...bson_type key.size_of_value...value.bson_type KEY.size_of_VALUE...VALUE..
- */
-static void test_bson_iter_next_long_msg_success() {
-    /* Test if bson_iterator_next successfully skips over the value "world" and returns the bson_type of the next key value pair */
-    bson_iterator it;
-    /* 35...2key.6...value.2KEY.6...VALUE.. */
-    char msg[35] = "\x23\x00\x00\x00\x02\x6b\x65\x79\x00\x06\x00\x00\x00\x76\x61\x6c\x75\x65\x00\x02\x4b\x45\x59\x00\x06\x00\x00\x00\x56\x41\x4c\x55\x45\x00\x00";
-    int msg_size = 35;
-    int response;
-    int expected_response = 2;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    response = bson_iterator_next(&it, error_test);
-    TEST_ASSERT_EQUAL(expected_response, response);
-}
-
-static void test_bson_iter_next_large_size() {
-    /* Test if bson_iterator_next succeeds when the size of the BSON message defined by the BSON is larger than the actual length of the message */
-    bson_iterator it;
-    /* 21...2Key.6...Value.. */
-    char msg[20] = "\x15\x00\x00\x00\x02\x4b\x65\x79\x00\x06\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    /* The size of the BSON message defined by the BSON is not taken into account so bson_iterator_next should not reach error_test */
-    TEST_ASSERT_FALSE(reached_error_test);
-}
-
-static void test_bson_iter_next_smaller_value_size() {
-    /* Test if bson_iterator_next succeeds when the size of "Value" according to the BSON is smaller than the actual size of "Value" */
-    bson_iterator it;
-    /* 20...2Key.4...Value.. */
-    char msg[20] = "\x14\x00\x00\x00\x02\x4b\x65\x79\x00\x04\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    /* This will skip 4 characters so it.cur will point to the "e" character and not go over the end of the BSON message */
-    TEST_ASSERT_FALSE(reached_error_test);
-}
-
-static void test_bson_iter_next_smaller_msg_size() {
-    /* Test if bson_iterator_next succeeds if the size of the BSON message according to the BSON is smaller than the actual size of the message */
-    bson_iterator it;
-    /* 18...2Key.6...Value.. */
-    char msg[20] = "\x12\x00\x00\x00\x02\x4b\x65\x79\x00\x06\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    /* The size of the BSON message defined by the BSON is not taken into account so bson_iterator_next should not reach error_test */
-    TEST_ASSERT_FALSE(reached_error_test);
-}
-
-static void test_bson_iter_next_eoo() {
-    /* Test if bson_iterator_next succeeds when it.cur is at the end of input */
-    bson_iterator it;
-    /* 20...2Key.6...Value.. */
-    char msg[20] = "\x14\x00\x00\x00\x02\x4b\x65\x79\x00\x06\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_FALSE(reached_error_test);
-}
-
-static void test_bson_iter_next_large_value() {
-    /* Test if bson_iterator_next fails when the size of "Value" according to the BSON is larger than the actual size of "Value" */
-    bson_iterator it;
-    /* 20...2Key.7...Value..*/
-    char msg[20] = "\x14\x00\x00\x00\x02\x4b\x65\x79\x00\x07\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 20;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-static void test_bson_iter_next_no_null() {
-    /* Test that bson_iterator_next will fail when the BSON message does not end with a null byte */
-    bson_iterator it;
-    /* 18...2Key.6...Value */
-    char msg[18] = "\x12\x00\x00\x00\x02\x4b\x65\x79\x00\x06\x00\x00\x00\x56\x61\x6c\x75\x65";
-    int msg_size = 18;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-static void test_bson_iter_next_missing_null() {
-    /* Test if bson_iterator_next fails when there is no null between the value and the size of the key */
-    bson_iterator it;
-    /* 19...2Key6...Value.. */
-    char msg[19] = "\x13\x00\x00\x00\x02\x4b\x65\x79\x06\x00\x00\x00\x56\x61\x6c\x75\x65\x00\x00";
-    int msg_size = 19;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-static void test_bson_iter_next_no_value() {
-    /* Test if bson_iterator_next fails when the bson message does not have a value for the key "Key" */
-    bson_iterator it;
-    /* 9...2Key. */
-    char msg[9] = "\x09\x00\x00\x00\x02\x4b\x65\x79\x00";
-    int msg_size = 9;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-static void test_bson_iter_small_msg() {
-    /* Test if bson_iterator_next fails when i->cur + 1 is out of bounds in bson_iterator_next */
-    bson_iterator it;
-    /* 5...2 */
-    char msg[5] = "\x05\x00\x00\x00\x02";
-    int msg_size = 5;
-    reached_error_test = 0;
-
-    bson_iterator_init(&it, msg, msg_size, error_test);
-    it.first = 0;
-
-    bson_iterator_next(&it, error_test);
-    TEST_ASSERT_TRUE(reached_error_test);
-}
-
-int main() {
-    UNITY_BEGIN();
-    RUN_TEST(test_bson_iter_next_basic_success);
-    RUN_TEST(test_bson_iter_next_long_msg_success);
-    RUN_TEST(test_bson_iter_next_large_size);
-    RUN_TEST(test_bson_iter_next_smaller_value_size);
-    RUN_TEST(test_bson_iter_next_smaller_msg_size);
-    RUN_TEST(test_bson_iter_next_eoo);
-    RUN_TEST(test_bson_iter_next_large_value);
-    RUN_TEST(test_bson_iter_next_no_null);
-    RUN_TEST(test_bson_iter_next_missing_null);
-    RUN_TEST(test_bson_iter_next_no_value);
-    RUN_TEST(test_bson_iter_small_msg);
-    return UNITY_END();
-}
diff --git a/tests/unity_tests/common_ini_string_options.c b/tests/unity_tests/common_ini_string_options.c
index 7c819e8..84633bf 100644
--- a/tests/unity_tests/common_ini_string_options.c
+++ b/tests/unity_tests/common_ini_string_options.c
@@ -5,8 +5,8 @@
     struct duo_config cfg = {0};
     char *name = "ikey";
     char *value = "1234123412341234";
-    char *expected_value = "1234123412341234"; 
-    
+    char *expected_value = "1234123412341234";
+
     duo_common_ini_handler(&cfg, SECTION, name, value);
     TEST_ASSERT_EQUAL_STRING(expected_value, cfg.ikey);
 }
@@ -17,7 +17,7 @@
     const char *expected_value = EMPTY_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, EMPTY_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.ikey); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.ikey);
 }
 
 static void test_ikey_null() {
@@ -26,7 +26,7 @@
     const char *expected_value = NULL_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, NULL_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.ikey); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.ikey);
 }
 
 /* Test adding skey to duo_config */
@@ -37,16 +37,16 @@
     char *expected_value = "1234123412341234";
 
     duo_common_ini_handler(&cfg, SECTION, name, value);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey);
 }
 
 static void test_skey_empty() {
     struct duo_config cfg = {0};
     char *name = "skey";
     const char *expected_value = EMPTY_STR;
-    
+
     duo_common_ini_handler(&cfg, SECTION, name, EMPTY_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey);
 }
 
 static void test_skey_null() {
@@ -55,7 +55,7 @@
     const char *expected_value = NULL_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, NULL_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.skey);
 }
 
 /* Test adding apihost to duo_config */
@@ -66,16 +66,16 @@
     char *expected_value = "123412341234";
 
     duo_common_ini_handler(&cfg, SECTION, name, value);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost);
 }
 
 static void test_host_empty() {
     struct duo_config cfg = {0};
     char *name = "host";
-    const char *expected_value = EMPTY_STR; 
-    
+    const char *expected_value = EMPTY_STR;
+
     duo_common_ini_handler(&cfg, SECTION, name, EMPTY_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost);
 }
 
 static void test_host_null() {
@@ -84,7 +84,7 @@
     const char *expected_value = NULL_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, NULL_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.apihost);
 }
 
 /* Test adding cafile to duo_config */
@@ -95,16 +95,16 @@
     char *expected_value = "cafilevalue";
 
     duo_common_ini_handler(&cfg, SECTION, name, value);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.cafile); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.cafile);
 }
 
 static void test_cafile_empty() {
     struct duo_config cfg = {0};
     char *name = "cafile";
-    const char *expected_value = EMPTY_STR;    
+    const char *expected_value = EMPTY_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, EMPTY_STR);
-    TEST_ASSERT_EQUAL_STRING(EMPTY_STR, cfg.cafile); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.cafile);
 }
 
 static void test_cafile_null() {
@@ -124,7 +124,7 @@
     char *expected_value = "http://username:password@proxy.example.org:8080";
 
     duo_common_ini_handler(&cfg, SECTION, name, value);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy);
 }
 
 static void test_http_proxy_empty() {
@@ -133,7 +133,7 @@
     const char *expected_value = EMPTY_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, EMPTY_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy);
 }
 
 static void test_http_proxy_null() {
@@ -142,7 +142,7 @@
     const char *expected_value = NULL_STR;
 
     duo_common_ini_handler(&cfg, SECTION, name, NULL_STR);
-    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy); 
+    TEST_ASSERT_EQUAL_STRING(expected_value, cfg.http_proxy);
 }
 
 int main() {
@@ -162,6 +162,6 @@
     RUN_TEST(test_http_proxy);
     RUN_TEST(test_http_proxy_empty);
     RUN_TEST(test_http_proxy_null);
-    
+
     return UNITY_END();
 }