summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkali <kali@leap.se>2012-07-22 21:10:15 -0700
committerkali <kali@leap.se>2012-07-22 21:10:15 -0700
commitc46d8da153ac658c8bd145376e22b1218db1090a (patch)
tree0943a4a866d9f3b1bc590c1c23f810ca13635f9e
initial import
-rw-r--r--.gitignore15
-rw-r--r--CHANGES.txt1
-rw-r--r--MANIFEST.in3
-rw-r--r--Makefile41
-rw-r--r--README.txt37
-rw-r--r--data/TODO1
-rw-r--r--data/images/conn_connected.pngbin0 -> 3031 bytes
-rw-r--r--data/images/conn_connecting.pngbin0 -> 3213 bytes
-rw-r--r--data/images/conn_error.pngbin0 -> 3571 bytes
-rw-r--r--data/images/leapfrog.jpgbin0 -> 1767 bytes
-rw-r--r--data/resources/mainwindow.qrc8
-rw-r--r--debian/README.Debian6
-rw-r--r--debian/README.source9
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control19
-rw-r--r--debian/copyright38
-rw-r--r--debian/docs2
-rw-r--r--debian/files1
-rw-r--r--debian/init.d.ex154
-rw-r--r--debian/menu.ex2
-rw-r--r--debian/patches/fix-manpage35
-rw-r--r--debian/patches/series1
-rw-r--r--debian/postinst.ex39
-rw-r--r--debian/postrm.ex37
-rw-r--r--debian/preinst.ex35
-rw-r--r--debian/prerm.ex38
-rw-r--r--debian/python-leap-client.cron.d.ex4
-rw-r--r--debian/python-leap-client.debhelper.log48
-rw-r--r--debian/python-leap-client.default.ex10
-rw-r--r--debian/python-leap-client.doc-base.EX20
-rw-r--r--debian/python-leap-client.install2
-rw-r--r--debian/python-leap-client.postinst.debhelper7
-rw-r--r--debian/python-leap-client.prerm.debhelper12
-rw-r--r--debian/python-leap-client.substvars4
-rwxr-xr-xdebian/rules59
-rw-r--r--debian/source/format1
-rw-r--r--debian/watch.ex23
-rw-r--r--docs/LICENSE340
-rw-r--r--docs/Makefile153
-rw-r--r--docs/conf.py242
-rw-r--r--docs/index.txt37
-rw-r--r--docs/leap.134
-rw-r--r--docs/make.bat190
-rw-r--r--ez_setup.py284
-rw-r--r--setup.cfg3
-rwxr-xr-xsetup.py63
-rw-r--r--setup/linux/leap.desktop13
-rw-r--r--setup/linux/polkit/net.openvpn.gui.leap.policy23
-rw-r--r--setup/requirements.pip0
-rwxr-xr-xsetup/scripts/leap6
-rw-r--r--src/eip_client.egg-info/PKG-INFO11
-rw-r--r--src/eip_client.egg-info/SOURCES.txt28
-rw-r--r--src/eip_client.egg-info/dependency_links.txt1
-rw-r--r--src/eip_client.egg-info/entry_points.txt3
-rw-r--r--src/eip_client.egg-info/not-zip-safe1
-rw-r--r--src/eip_client.egg-info/top_level.txt1
-rw-r--r--src/leap/__init__.py0
-rw-r--r--src/leap/app.py41
-rw-r--r--src/leap/baseapp/__init__.py0
-rw-r--r--src/leap/baseapp/config.py40
-rw-r--r--src/leap/baseapp/mainwindow.py398
-rw-r--r--src/leap/eip/__init__.py0
-rw-r--r--src/leap/eip/conductor.py272
-rw-r--r--src/leap/eip/vpnmanager.py262
-rw-r--r--src/leap/eip/vpnwatcher.py169
-rw-r--r--src/leap/gui/__init__.py0
-rw-r--r--src/leap/gui/mainwindow_rc.py789
-rw-r--r--src/leap/tests/fakeclient.py63
-rw-r--r--src/leap/tests/mocks/__init__.py1
-rw-r--r--src/leap/tests/mocks/manager.py20
-rw-r--r--src/leap/utils/__init__.py0
-rw-r--r--src/leap/utils/coroutines.py107
-rw-r--r--src/leap/utils/leap_argparse.py20
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/support.py111
-rw-r--r--tests/support_tests.py1725
-rw-r--r--tests/test_argparse.py26
-rw-r--r--tests/test_conductor.py8
-rw-r--r--tests/test_mainwindow.py150
-rw-r--r--tests/test_mgminterface.py333
-rw-r--r--tests/test_vpn_management.py42
82 files changed, 6729 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3a0afe7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.swp
+*.swo
+*.pyc
+.*
+bin/
+build/
+core
+debian/python-leap-client/
+dist/
+docs/_build
+include/
+lib/
+local/
+share/
+src/leap.egg-info/
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..e92537c
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1 @@
+-- 0.1.0 initial release
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..3ce64e4
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+# ??? not needed from win
+include setup/linux/polkit/*
+include docs/*
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8f50f56
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+# ################################
+# Makefile for compiling resources
+# files.
+###### EDIT ######################
+#Directory with ui and resource files
+RESOURCE_DIR = data/resources
+
+#Directory for compiled resources
+COMPILED_DIR = src/leap/gui
+
+#UI files to compile
+# UI_FILES = foo.ui
+UI_FILES =
+#Qt resource files to compile
+#images.qrc
+RESOURCES = mainwindow.qrc
+
+#pyuic4 and pyrcc4 binaries
+PYUIC = pyuic4
+PYRCC = pyrcc4
+
+#################################
+# DO NOT EDIT FOLLOWING
+
+COMPILED_UI = $(UI_FILES:%.ui=$(COMPILED_DIR)/ui_%.py)
+COMPILED_RESOURCES = $(RESOURCES:%.qrc=$(COMPILED_DIR)/%_rc.py)
+
+all : resources ui
+
+resources : $(COMPILED_RESOURCES)
+
+ui : $(COMPILED_UI)
+
+$(COMPILED_DIR)/ui_%.py : $(RESOURCE_DIR)/%.ui
+ $(PYUIC) $< -o $@
+
+$(COMPILED_DIR)/%_rc.py : $(RESOURCE_DIR)/%.qrc
+ $(PYRCC) $< -o $@
+
+clean :
+ $(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc)
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..14ac253
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,37 @@
+========================================
+= LEAP =
+= The Internet Encryption Toolkit =
+========================================
+
+Install
+=======
+python setup.py install
+
+Running tests
+=============
+nosetests -v
+
+Deps
+====
+apt-get install python-qt4 python-qt4-doc pyqt4-dev-tools
+
+Hack
+====
+
+(recommended)
+virtualenv . # ensure your .gitignore knows about it
+bin/activate
+
+# you should probably simlink sip.so and PyQt4 to your system-wide
+# install, there are some issues with it.
+
+python setup.py develop # ... TBD: finish develop howto.
+
+Compiling resource/ui files
+===========================
+You should refresh resource/ui files every time you
+change an image or a resource/ui (.ui / .qc). From
+the root folder:
+
+make ui
+make resources
diff --git a/data/TODO b/data/TODO
new file mode 100644
index 0000000..580227a
--- /dev/null
+++ b/data/TODO
@@ -0,0 +1 @@
+icons file and stuff should be moved here at some point!
diff --git a/data/images/conn_connected.png b/data/images/conn_connected.png
new file mode 100644
index 0000000..6a5bcba
--- /dev/null
+++ b/data/images/conn_connected.png
Binary files differ
diff --git a/data/images/conn_connecting.png b/data/images/conn_connecting.png
new file mode 100644
index 0000000..35cb0f6
--- /dev/null
+++ b/data/images/conn_connecting.png
Binary files differ
diff --git a/data/images/conn_error.png b/data/images/conn_error.png
new file mode 100644
index 0000000..ac1391d
--- /dev/null
+++ b/data/images/conn_error.png
Binary files differ
diff --git a/data/images/leapfrog.jpg b/data/images/leapfrog.jpg
new file mode 100644
index 0000000..a1ddf4b
--- /dev/null
+++ b/data/images/leapfrog.jpg
Binary files differ
diff --git a/data/resources/mainwindow.qrc b/data/resources/mainwindow.qrc
new file mode 100644
index 0000000..9a2531c
--- /dev/null
+++ b/data/resources/mainwindow.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource prefix="/">
+ <file>../images/conn_error.png</file>
+ <file>../images/conn_connecting.png</file>
+ <file>../images/conn_connected.png</file>
+ <file>../images/leapfrog.jpg</file>
+</qresource>
+</RCC>
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644
index 0000000..045d970
--- /dev/null
+++ b/debian/README.Debian
@@ -0,0 +1,6 @@
+python-leap-client for Debian
+-----------------------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- unknown <kali@croatan> Sat, 21 Jul 2012 00:11:05 -0700
diff --git a/debian/README.source b/debian/README.source
new file mode 100644
index 0000000..eae9eab
--- /dev/null
+++ b/debian/README.source
@@ -0,0 +1,9 @@
+python-leap-client for Debian
+-----------------------------
+
+<this file describes information about the source package, see Debian policy
+manual section 4.14. You WILL either need to modify or delete this file>
+
+
+
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..41dd76f
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+python-leap-client (0.1.0-1) unstable; urgency=low
+
+ * Initial release (Closes: #nnnn) <nnnn is the bug number of your ITP>
+
+ -- unknown <cal@croatan> Sat, 21 Jul 2012 00:11:05 -0700
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+8
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..d4b38f2
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,19 @@
+Source: python-leap-client
+Section: unknown
+Priority: extra
+Maintainer: kali <kali@leap.se>
+Standards-Version: 3.9.3
+Homepage: http://leap.se
+#Vcs-Git: git://git.debian.org/collab-maint/python-leap-client.git
+#Vcs-Browser: http://git.debian.org/?p=collab-maint/python-leap-client.git;a=summary
+X-Python-Version: >= 2.7
+
+Package: python-leap-client
+Architecture: any
+#XXX ???
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Depends: openvpn, python-qt4
+#XXX should deprecate python-support methinks
+Build-Depends: debhelper (>= 8.0.0), python-support, pyqt4-dev-tools, python-sphinx
+Description: the encrypted interned toolkit
+ <insert long description, indented with spaces>
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..a5907f4
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,38 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: python-leap-client
+Source: <url://example.com>
+
+Files: *
+Copyright: <years> <put author's name and email here>
+ <years> <likewise for another author>
+License: <special license>
+ <Put the license of the package here indented by 1 space>
+ <This follows the format of Description: lines in control file>
+ .
+ <Including paragraphs>
+
+# If you want to use GPL v2 or later for the /debian/* files use
+# the following clauses, or change it to suit. Delete these two lines
+Files: debian/*
+Copyright: 2012 unknown <cal@croatan>
+License: GPL-2+
+ This package is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
+# Please avoid to pick license terms that are more restrictive than the
+# packaged work, as it may make Debian's contributions unacceptable upstream.
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..e953f2c
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1,2 @@
+CHANGES.txt
+README.txt
diff --git a/debian/files b/debian/files
new file mode 100644
index 0000000..1aed53a
--- /dev/null
+++ b/debian/files
@@ -0,0 +1 @@
+python-leap-client_0.1.0-1_i386.deb unknown extra
diff --git a/debian/init.d.ex b/debian/init.d.ex
new file mode 100644
index 0000000..3eec795
--- /dev/null
+++ b/debian/init.d.ex
@@ -0,0 +1,154 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides: python-leap-client
+# Required-Start: $network $local_fs
+# Required-Stop:
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: <Enter a short description of the software>
+# Description: <Enter a long description of the software>
+# <...>
+# <...>
+### END INIT INFO
+
+# Author: unknown <cal@croatan>
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC=python-leap-client # Introduce a short description here
+NAME=python-leap-client # Introduce the short server's name here
+DAEMON=/usr/sbin/python-leap-client # Introduce the server's location here
+DAEMON_ARGS="" # Arguments to run the daemon with
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -x $DAEMON ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+ $DAEMON_ARGS \
+ || return 2
+ # Add code here, if necessary, that waits for the process to be ready
+ # to handle requests from services started subsequently which depend
+ # on this one. As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+ [ "$?" = 2 ] && return 2
+ # Many daemons don't delete their pidfiles when they exit.
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+ #
+ # If the daemon can reload its configuration without
+ # restarting (for example, when it is sent a SIGHUP),
+ # then implement that here.
+ #
+ start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+ return 0
+}
+
+case "$1" in
+ start)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ #reload|force-reload)
+ #
+ # If do_reload() is not implemented then leave this commented out
+ # and leave 'force-reload' as an alias for 'restart'.
+ #
+ #log_daemon_msg "Reloading $DESC" "$NAME"
+ #do_reload
+ #log_end_msg $?
+ #;;
+ restart|force-reload)
+ #
+ # If the "reload" option is implemented then remove the
+ # 'force-reload' alias
+ #
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+ echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
diff --git a/debian/menu.ex b/debian/menu.ex
new file mode 100644
index 0000000..0ac5a68
--- /dev/null
+++ b/debian/menu.ex
@@ -0,0 +1,2 @@
+?package(python-leap-client):needs="X11|text|vc|wm" section="Applications/see-menu-manual"\
+ title="python-leap-client" command="/usr/bin/python-leap-client"
diff --git a/debian/patches/fix-manpage b/debian/patches/fix-manpage
new file mode 100644
index 0000000..e279eb1
--- /dev/null
+++ b/debian/patches/fix-manpage
@@ -0,0 +1,35 @@
+Description: <short summary of the patch>
+ TODO: Put a short summary on the line above and replace this paragraph
+ with a longer explanation of this change. Complete the meta-information
+ with other relevant fields (see below for details). To make it easier, the
+ information below has been extracted from the changelog. Adjust it or drop
+ it.
+ .
+ python-leap-client (0.1.0-1) unstable; urgency=low
+ .
+ * Initial release (Closes: #nnnn) <nnnn is the bug number of your ITP>
+Author: unknown <cal@croatan>
+
+---
+The information above should follow the Patch Tagging Guidelines, please
+checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here
+are templates for supplementary fields that you might want to add:
+
+Origin: <vendor|upstream|other>, <url of original patch>
+Bug: <url in upstream bugtracker>
+Bug-Debian: http://bugs.debian.org/<bugnumber>
+Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber>
+Forwarded: <no|not-needed|url proving that it has been forwarded>
+Reviewed-By: <name and email of someone who approved the patch>
+Last-Update: <YYYY-MM-DD>
+
+--- python-leap-client-0.1.0.orig/docs/leap.1
++++ python-leap-client-0.1.0/docs/leap.1
+@@ -29,6 +29,6 @@ http://leap.se
+ You can report bugs at the bugtracker site of leap:
+ http://leap.se/code
+ .SH AUTHOR
+-Kali <kaliyuga at riseup dot net>
++This manpage written by kali <kaliyuga at riseup dot net> for the debian package, but obviously can be used for any other distribution.
+ .SH SEE ALSO
+ .BR PolicyKit.conf (7)
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..15e5320
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1 @@
+fix-manpage
diff --git a/debian/postinst.ex b/debian/postinst.ex
new file mode 100644
index 0000000..888928c
--- /dev/null
+++ b/debian/postinst.ex
@@ -0,0 +1,39 @@
+#!/bin/sh
+# postinst script for python-leap-client
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * <postinst> `configure' <most-recently-configured-version>
+# * <old-postinst> `abort-upgrade' <new version>
+# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+# <new-version>
+# * <postinst> `abort-remove'
+# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+# <failed-install-package> <version> `removing'
+# <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ configure)
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/postrm.ex b/debian/postrm.ex
new file mode 100644
index 0000000..5048c8e
--- /dev/null
+++ b/debian/postrm.ex
@@ -0,0 +1,37 @@
+#!/bin/sh
+# postrm script for python-leap-client
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * <postrm> `remove'
+# * <postrm> `purge'
+# * <old-postrm> `upgrade' <new-version>
+# * <new-postrm> `failed-upgrade' <old-version>
+# * <new-postrm> `abort-install'
+# * <new-postrm> `abort-install' <old-version>
+# * <new-postrm> `abort-upgrade' <old-version>
+# * <disappearer's-postrm> `disappear' <overwriter>
+# <overwriter-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/preinst.ex b/debian/preinst.ex
new file mode 100644
index 0000000..8aeafcf
--- /dev/null
+++ b/debian/preinst.ex
@@ -0,0 +1,35 @@
+#!/bin/sh
+# preinst script for python-leap-client
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * <new-preinst> `install'
+# * <new-preinst> `install' <old-version>
+# * <new-preinst> `upgrade' <old-version>
+# * <old-preinst> `abort-upgrade' <new-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ install|upgrade)
+ ;;
+
+ abort-upgrade)
+ ;;
+
+ *)
+ echo "preinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/prerm.ex b/debian/prerm.ex
new file mode 100644
index 0000000..19cc6ca
--- /dev/null
+++ b/debian/prerm.ex
@@ -0,0 +1,38 @@
+#!/bin/sh
+# prerm script for python-leap-client
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * <prerm> `remove'
+# * <old-prerm> `upgrade' <new-version>
+# * <new-prerm> `failed-upgrade' <old-version>
+# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
+# * <deconfigured's-prerm> `deconfigure' `in-favour'
+# <package-being-installed> <version> `removing'
+# <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ remove|upgrade|deconfigure)
+ ;;
+
+ failed-upgrade)
+ ;;
+
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/python-leap-client.cron.d.ex b/debian/python-leap-client.cron.d.ex
new file mode 100644
index 0000000..693a391
--- /dev/null
+++ b/debian/python-leap-client.cron.d.ex
@@ -0,0 +1,4 @@
+#
+# Regular cron jobs for the python-leap-client package
+#
+0 4 * * * root [ -x /usr/bin/python-leap-client_maintenance ] && /usr/bin/python-leap-client_maintenance
diff --git a/debian/python-leap-client.debhelper.log b/debian/python-leap-client.debhelper.log
new file mode 100644
index 0000000..c9704cf
--- /dev/null
+++ b/debian/python-leap-client.debhelper.log
@@ -0,0 +1,48 @@
+dh_auto_configure
+dh_auto_build
+dh_auto_test
+dh_prep
+dh_installdirs
+dh_auto_install
+dh_install
+dh_installdocs
+override_dh_installchangelogs dh_installchangelogs
+dh_installchangelogs
+dh_installexamples
+dh_installman
+dh_installcatalogs
+dh_installcron
+dh_installdebconf
+dh_installemacsen
+dh_installifupdown
+dh_installinfo
+dh_installinit
+dh_installmenu
+dh_installmime
+dh_installmodules
+dh_installlogcheck
+dh_installlogrotate
+dh_installpam
+dh_installppp
+dh_installudev
+dh_installwm
+dh_installxfonts
+dh_installgsettings
+dh_bugfiles
+dh_ucf
+dh_lintian
+dh_gconf
+dh_icons
+dh_perl
+dh_usrlocal
+dh_link
+dh_compress
+dh_fixperms
+dh_strip
+dh_makeshlibs
+dh_shlibdeps
+dh_installdeb
+dh_gencontrol
+dh_md5sums
+dh_builddeb
+dh_builddeb
diff --git a/debian/python-leap-client.default.ex b/debian/python-leap-client.default.ex
new file mode 100644
index 0000000..131c9f8
--- /dev/null
+++ b/debian/python-leap-client.default.ex
@@ -0,0 +1,10 @@
+# Defaults for python-leap-client initscript
+# sourced by /etc/init.d/python-leap-client
+# installed at /etc/default/python-leap-client by the maintainer scripts
+
+#
+# This is a POSIX shell fragment
+#
+
+# Additional options that are passed to the Daemon.
+DAEMON_OPTS=""
diff --git a/debian/python-leap-client.doc-base.EX b/debian/python-leap-client.doc-base.EX
new file mode 100644
index 0000000..e70c291
--- /dev/null
+++ b/debian/python-leap-client.doc-base.EX
@@ -0,0 +1,20 @@
+Document: python-leap-client
+Title: Debian python-leap-client Manual
+Author: <insert document author here>
+Abstract: This manual describes what python-leap-client is
+ and how it can be used to
+ manage online manuals on Debian systems.
+Section: unknown
+
+Format: debiandoc-sgml
+Files: /usr/share/doc/python-leap-client/python-leap-client.sgml.gz
+
+Format: postscript
+Files: /usr/share/doc/python-leap-client/python-leap-client.ps.gz
+
+Format: text
+Files: /usr/share/doc/python-leap-client/python-leap-client.text.gz
+
+Format: HTML
+Index: /usr/share/doc/python-leap-client/html/index.html
+Files: /usr/share/doc/python-leap-client/html/*.html
diff --git a/debian/python-leap-client.install b/debian/python-leap-client.install
new file mode 100644
index 0000000..11edacf
--- /dev/null
+++ b/debian/python-leap-client.install
@@ -0,0 +1,2 @@
+#usr/share/polkit-1/actions/net.openvpn.gui.leap
+polkit/net.openvpn.gui.leap.policy usr/share/polkit-1/actions/
diff --git a/debian/python-leap-client.postinst.debhelper b/debian/python-leap-client.postinst.debhelper
new file mode 100644
index 0000000..8b32391
--- /dev/null
+++ b/debian/python-leap-client.postinst.debhelper
@@ -0,0 +1,7 @@
+
+# Automatically added by dh_python2:
+if which pycompile >/dev/null 2>&1; then
+ pycompile -p python-leap-client
+fi
+
+# End automatically added section
diff --git a/debian/python-leap-client.prerm.debhelper b/debian/python-leap-client.prerm.debhelper
new file mode 100644
index 0000000..5ebc7ff
--- /dev/null
+++ b/debian/python-leap-client.prerm.debhelper
@@ -0,0 +1,12 @@
+
+# Automatically added by dh_python2:
+if which pyclean >/dev/null 2>&1; then
+ pyclean -p python-leap-client
+else
+ dpkg -L python-leap-client | grep \.py$ | while read file
+ do
+ rm -f "${file}"[co] >/dev/null
+ done
+fi
+
+# End automatically added section
diff --git a/debian/python-leap-client.substvars b/debian/python-leap-client.substvars
new file mode 100644
index 0000000..80ba5c8
--- /dev/null
+++ b/debian/python-leap-client.substvars
@@ -0,0 +1,4 @@
+python:Versions=2.7
+python:Provides=python2.7-leap-client
+python:Depends=python (>= 2.7), python (<< 2.8), python (>= 2.6.6-7~), python
+misc:Depends=
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..ec47a76
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,59 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+#
+#
+
+# needed???
+# DEB_PYTHON_SYSTEM=pysupport
+
+# Uncomment this to turn on verbose mode.
+#DH_VERBOSE=1
+
+PYTHON2=$(shell pyversions -vr)
+
+%:
+ dh $@ --with python2
+ #,sphinxdoc
+
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+
+# run tests!
+#
+test-python%:
+ python$* setup.py test -vv
+
+override_dh_auto_test: $(PYTHON2:%=test-python%) $(PYTHON3:%=test-python%)
+endif
+
+#dh_auto_build should be enough to build the python2 version
+
+#build-python%:
+# python$* setup.py build
+
+#override_dh_auto_build: $(PYTHON3:%=build-python%)
+# dh_auto_build
+
+#install-python%:
+# python$* setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb
+
+#override_dh_auto_install: $(PYTHON3:%=install-python%)
+# dh_auto_install
+
+override_dh_installchangelogs:
+ dh_installchangelogs -k CHANGES.txt
+
+# build and install sphinx docs
+#
+#override_dh_installdocs:
+# python setup.py build_sphinx
+# dh_installdocs build/sphinx/html
+
+override_dh_auto_clean:
+ dh_auto_clean
+ rm -rf build
+ rm -rf *.egg-info
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/watch.ex b/debian/watch.ex
new file mode 100644
index 0000000..791fbd7
--- /dev/null
+++ b/debian/watch.ex
@@ -0,0 +1,23 @@
+# Example watch control file for uscan
+# Rename this file to "watch" and then you can run the "uscan" command
+# to check for upstream updates and more.
+# See uscan(1) for format
+
+# Compulsory line, this is a version 3 file
+version=3
+
+# Uncomment to examine a Webpage
+# <Webpage URL> <string match>
+#http://www.example.com/downloads.php python-leap-client-(.*)\.tar\.gz
+
+# Uncomment to examine a Webserver directory
+#http://www.example.com/pub/python-leap-client-(.*)\.tar\.gz
+
+# Uncommment to examine a FTP server
+#ftp://ftp.example.com/pub/python-leap-client-(.*)\.tar\.gz debian uupdate
+
+# Uncomment to find new files on sourceforge, for devscripts >= 2.9
+# http://sf.net/python-leap-client/python-leap-client-(.*)\.tar\.gz
+
+# Uncomment to find new files on GooglePages
+# http://example.googlepages.com/foo.html python-leap-client-(.*)\.tar\.gz
diff --git a/docs/LICENSE b/docs/LICENSE
new file mode 100644
index 0000000..b7b5f53
--- /dev/null
+++ b/docs/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..16aa258
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LEAP.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LEAP.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/LEAP"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LEAP"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..862a2f1
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+#
+# LEAP documentation build configuration file, created by
+# sphinx-quickstart on Sun Jul 22 18:32:05 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'LEAP'
+copyright = u'2012, The Leap Project'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1.0'
+# The full version, including alpha/beta/rc tags.
+release = '0.1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'LEAPdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'LEAP.tex', u'LEAP Documentation',
+ u'The Leap Project', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'leap', u'LEAP Documentation',
+ [u'The Leap Project'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'LEAP', u'LEAP Documentation',
+ u'The Leap Project', 'LEAP', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
diff --git a/docs/index.txt b/docs/index.txt
new file mode 100644
index 0000000..fa42e6f
--- /dev/null
+++ b/docs/index.txt
@@ -0,0 +1,37 @@
+.. LEAP documentation master file, created by
+ sphinx-quickstart on Sun Jul 22 18:32:05 2012.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to LEAP's documentation!
+================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+Leap
+====
+The Encrypted Internet Toolkit
+
+Overview
+========
+...
+
+User Guide
+==========
+...
+
+Config
+======
+...
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/leap.1 b/docs/leap.1
new file mode 100644
index 0000000..aaa614b
--- /dev/null
+++ b/docs/leap.1
@@ -0,0 +1,34 @@
+.\" groff -man -Tascii foo.1
+.TH LEAP 1 "July 2012" leap "User manual"
+.SH NAME
+leap \- the internet encryption toolkit
+.SH SYNOPSIS
+.B leap
+.RI [ OPTIONS ]
+.SH DESCRIPTION
+.B leap
+allows to ... blah blah ...
+.SH OPTIONS
+.IP "-d, --debug"
+Show additional information on the command line.
+.IP "-h, --help"
+Show information about the usage of the command.
+.SH FILES
+.TP
+.I /usr/share/polkit-1/actions/net.openvpn.gui.leap.policy
+The PolicyKit definitions of the privileges used by leap, e.g. to run openvpn as root. To change the privileges please have a look at
+.BR PolicyKit.conf (1).
+.SH DIAGNOSTICS
+By default leaps logs to ... /dev/null. Furthermore you
+can foobarize yourself.
+.SH EXTRA TIPS
+Trust your technolust!
+.SH HOMEPAGE
+http://leap.se
+.SH BUGS
+You can report bugs at the bugtracker site of leap:
+http://leap.se/code
+.SH AUTHOR
+This manpage written by kali <kaliyuga at riseup dot net> for the debian package, but obviously can be used for any other distribution.
+.SH SEE ALSO
+.BR PolicyKit.conf (7)
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..b241ea3
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,190 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\LEAP.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\LEAP.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 0000000..b74adc0
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,284 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c11"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090',
+ 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4',
+ 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7',
+ 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5',
+ 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de',
+ 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b',
+ 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2',
+ 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ except pkg_resources.DistributionNotFound:
+ pass
+
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..01bb954
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..2bda6b6
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+
+try:
+ from setuptools import setup, find_packages
+except ImportError:
+ #FIXME old?
+ #use distribute_setup instead??
+ #http://packages.python.org/distribute/setuptools.html#using-setuptools-without-bundling-it
+ import ez_setup
+ #XXX move ez_setup somewhere else?
+ ez_setup.use_setuptools()
+ from setuptools import setup, find_packages
+import os
+
+# get version from somewhere else
+version = '0.1'
+
+setup_root = os.path.dirname(__file__)
+sys.path.insert(0, os.path.join(setup_root, "src"))
+
+setup(
+ name='eip-client',
+ package_dir={"": "src"},
+ version=version,
+ description="the internet encryption toolkit",
+ long_description="""\
+""",
+ classifiers=[], # Get strings from
+ # http://pypi.python.org/pypi?%3Aaction=list_classifiers
+
+ # XXX FIXME DEPS
+ # deps: pyqt
+ # test_deps: nose
+ # build_deps: pyqt-utils
+
+ keywords='leap, client, qt, encryption',
+ author='leap project',
+ author_email='info@leap.se',
+ url='http://leap.se',
+ license='GPL',
+ packages=find_packages(
+ 'src',
+ exclude=['ez_setup', 'setup', 'examples', 'tests']),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ # -*- Extra requirements: -*-
+ ],
+ data_files=[
+ ("share/man/man1",
+ ["docs/leap.1"]),
+ ("share/polkit-1/actions",
+ ["setup/linux/polkit/net.openvpn.gui.leap.policy"])
+ ],
+ platforms="all",
+ scripts=["setup/scripts/leap"],
+ entry_points="""
+ # -*- Entry points: -*-
+ """,
+)
diff --git a/setup/linux/leap.desktop b/setup/linux/leap.desktop
new file mode 100644
index 0000000..7a6d39d
--- /dev/null
+++ b/setup/linux/leap.desktop
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Version=0.1.0
+Encoding=UTF-8
+Name=EIP
+Comment=Anonymity and privacy
+Comment[en]=Anonymity and privacy
+Comment[es]=Anonimato y privacidad
+Comment[sv]=Anonymitet och avlyssningsskydd
+Exec=leap
+Terminal=false
+Type=Application
+Icon=leap.png
+Categories=Network;
diff --git a/setup/linux/polkit/net.openvpn.gui.leap.policy b/setup/linux/polkit/net.openvpn.gui.leap.policy
new file mode 100644
index 0000000..70a22b6
--- /dev/null
+++ b/setup/linux/polkit/net.openvpn.gui.leap.policy
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+<policyconfig>
+
+ <vendor>LEAP Project</vendor>
+ <vendor_url>http://leap.se/</vendor_url>
+
+ <action id="net.openvpn,gui.leap.run-openvpn">
+ <description>Runs the openvpn binary</description>
+ <description xml:lang="es">Ejecuta el binario openvpn</description>
+ <message>OpenVPN needs that you authenticate to start</message>
+ <message xml:lang="es">OpenVPN necesita autorizacion para comenzar</message>
+ <icon_name>package-x-generic</icon_name>
+ <defaults>
+ <allow_any>auth_self_keep</allow_any>
+ <allow_inactive>auth_self_keep</allow_inactive>
+ <allow_active>auth_self_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/openvpn</annotate>
+ </action>
+</policyconfig>
diff --git a/setup/requirements.pip b/setup/requirements.pip
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/setup/requirements.pip
diff --git a/setup/scripts/leap b/setup/scripts/leap
new file mode 100755
index 0000000..6e62b59
--- /dev/null
+++ b/setup/scripts/leap
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from leap.app import main
+
+if __name__ == "__main__":
+ main()
diff --git a/src/eip_client.egg-info/PKG-INFO b/src/eip_client.egg-info/PKG-INFO
new file mode 100644
index 0000000..e4bc754
--- /dev/null
+++ b/src/eip_client.egg-info/PKG-INFO
@@ -0,0 +1,11 @@
+Metadata-Version: 1.0
+Name: eip-client
+Version: 0.1dev
+Summary: the internet encryption toolkit
+Home-page: http://leap.se
+Author: leap project
+Author-email: info@leap.se
+License: GPL
+Description: UNKNOWN
+Keywords: leap,client,qt,encryption
+Platform: all
diff --git a/src/eip_client.egg-info/SOURCES.txt b/src/eip_client.egg-info/SOURCES.txt
new file mode 100644
index 0000000..05688ff
--- /dev/null
+++ b/src/eip_client.egg-info/SOURCES.txt
@@ -0,0 +1,28 @@
+MANIFEST.in
+README.txt
+setup.cfg
+setup.py
+docs/LICENSE.txt
+docs/leap.1
+setup/linux/polkit/net.openvpn.gui.leap.policy
+setup/scripts/leap
+src/eip_client.egg-info/PKG-INFO
+src/eip_client.egg-info/SOURCES.txt
+src/eip_client.egg-info/dependency_links.txt
+src/eip_client.egg-info/entry_points.txt
+src/eip_client.egg-info/not-zip-safe
+src/eip_client.egg-info/top_level.txt
+src/leap/__init__.py
+src/leap/app.py
+src/leap/baseapp/__init__.py
+src/leap/baseapp/config.py
+src/leap/baseapp/mainwindow.py
+src/leap/eip/__init__.py
+src/leap/eip/conductor.py
+src/leap/eip/vpnmanager.py
+src/leap/eip/vpnwatcher.py
+src/leap/gui/__init__.py
+src/leap/gui/mainwindow_rc.py
+src/leap/utils/__init__.py
+src/leap/utils/coroutines.py
+src/leap/utils/leap_argparse.py \ No newline at end of file
diff --git a/src/eip_client.egg-info/dependency_links.txt b/src/eip_client.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/eip_client.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/eip_client.egg-info/entry_points.txt b/src/eip_client.egg-info/entry_points.txt
new file mode 100644
index 0000000..a184cd0
--- /dev/null
+++ b/src/eip_client.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+
+ # -*- Entry points: -*-
+ \ No newline at end of file
diff --git a/src/eip_client.egg-info/not-zip-safe b/src/eip_client.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/eip_client.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/src/eip_client.egg-info/top_level.txt b/src/eip_client.egg-info/top_level.txt
new file mode 100644
index 0000000..2905ed7
--- /dev/null
+++ b/src/eip_client.egg-info/top_level.txt
@@ -0,0 +1 @@
+leap
diff --git a/src/leap/__init__.py b/src/leap/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/__init__.py
diff --git a/src/leap/app.py b/src/leap/app.py
new file mode 100644
index 0000000..0a61fd4
--- /dev/null
+++ b/src/leap/app.py
@@ -0,0 +1,41 @@
+import logging
+# This is only needed for Python v2 but is harmless for Python v3.
+import sip
+sip.setapi('QVariant', 2)
+from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox)
+
+from leap.baseapp.mainwindow import LeapWindow
+
+logger = logging.getLogger(name=__name__)
+
+
+def main():
+ """
+ launches the main event loop
+ long live to the (hidden) leap window!
+ """
+ import sys
+ from leap.utils import leap_argparse
+ parser, opts = leap_argparse.init_leapc_args()
+ debug = getattr(opts, 'debug', False)
+
+ #XXX get debug level and set logger accordingly
+ if debug:
+ logger.debug('args: ', opts)
+
+ app = QApplication(sys.argv)
+
+ if not QSystemTrayIcon.isSystemTrayAvailable():
+ QMessageBox.critical(None, "Systray",
+ "I couldn't detect any \
+system tray on this system.")
+ sys.exit(1)
+ if not debug:
+ QApplication.setQuitOnLastWindowClosed(False)
+
+ window = LeapWindow(opts)
+ window.show()
+ sys.exit(app.exec_())
+
+if __name__ == "__main__":
+ main()
diff --git a/src/leap/baseapp/__init__.py b/src/leap/baseapp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/baseapp/__init__.py
diff --git a/src/leap/baseapp/config.py b/src/leap/baseapp/config.py
new file mode 100644
index 0000000..efdb472
--- /dev/null
+++ b/src/leap/baseapp/config.py
@@ -0,0 +1,40 @@
+import ConfigParser
+import os
+
+
+def get_config(config_file=None):
+ """
+ temporary method for getting configs,
+ mainly for early stage development process.
+ in the future we will get preferences
+ from the storage api
+ """
+ config = ConfigParser.ConfigParser()
+ #config.readfp(open('defaults.cfg'))
+ #XXX does this work on win / mac also???
+ conf_path_list = ['eip.cfg', # XXX build a
+ # proper path with platform-specific places
+ # XXX make .config/foo
+ os.path.expanduser('~/.eip.cfg')]
+ if config_file:
+ config.readfp(config_file)
+ else:
+ config.read(conf_path_list)
+ return config
+
+
+# XXX wrapper around config? to get default values
+
+def get_with_defaults(config, section, option):
+ if config.has_option(section, option):
+ return config.get(section, option)
+ else:
+ # XXX lookup in defaults dict???
+ pass
+
+
+def get_vpn_stdout_mockup():
+ command = "python"
+ args = ["-u", "-c", "from eip_client import fakeclient;\
+fakeclient.write_output()"]
+ return command, args
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py
new file mode 100644
index 0000000..68b6de8
--- /dev/null
+++ b/src/leap/baseapp/mainwindow.py
@@ -0,0 +1,398 @@
+# vim: set fileencoding=utf-8 :
+#!/usr/bin/env python
+import logging
+import time
+logger = logging.getLogger(name=__name__)
+
+from PyQt4.QtGui import (QMainWindow, QWidget, QVBoxLayout, QMessageBox,
+ QSystemTrayIcon, QGroupBox, QLabel, QPixmap,
+ QHBoxLayout, QIcon,
+ QPushButton, QGridLayout, QAction, QMenu,
+ QTextBrowser, qApp)
+from PyQt4.QtCore import (pyqtSlot, pyqtSignal, QTimer)
+
+from leap.gui import mainwindow_rc
+from leap.eip.conductor import EIPConductor
+
+
+class LeapWindow(QMainWindow):
+ #XXX tbd: refactor into model / view / controller
+ #and put in its own modules...
+
+ newLogLine = pyqtSignal([str])
+ statusChange = pyqtSignal([object])
+
+ def __init__(self, opts):
+ super(LeapWindow, self).__init__()
+ self.debugmode = getattr(opts, 'debug', False)
+
+ self.vpn_service_started = False
+
+ self.createWindowHeader()
+ self.createIconGroupBox()
+
+ self.createActions()
+ self.createTrayIcon()
+ if self.debugmode:
+ self.createLogBrowser()
+
+ # create timer
+ self.timer = QTimer()
+
+ # bind signals
+
+ self.trayIcon.activated.connect(self.iconActivated)
+ self.newLogLine.connect(self.onLoggerNewLine)
+ self.statusChange.connect(self.onStatusChange)
+ self.timer.timeout.connect(self.onTimerTick)
+
+ widget = QWidget()
+ self.setCentralWidget(widget)
+
+ # add widgets to layout
+ mainLayout = QVBoxLayout()
+ mainLayout.addWidget(self.headerBox)
+ mainLayout.addWidget(self.statusIconBox)
+ if self.debugmode:
+ mainLayout.addWidget(self.statusBox)
+ mainLayout.addWidget(self.loggerBox)
+ widget.setLayout(mainLayout)
+
+ #
+ # conductor is in charge of all
+ # vpn-related configuration / monitoring.
+ # we pass a tuple of signals that will be
+ # triggered when status changes.
+ #
+ config_file = getattr(opts, 'config_file', None)
+ self.conductor = EIPConductor(
+ watcher_cb=self.newLogLine.emit,
+ config_file=config_file,
+ status_signals=(self.statusChange.emit, ))
+
+ self.trayIcon.show()
+
+ self.setWindowTitle("Leap")
+ self.resize(400, 300)
+
+ self.set_statusbarMessage('ready')
+
+ if self.conductor.autostart:
+ self.start_or_stopVPN()
+
+ def closeEvent(self, event):
+ """
+ redefines close event (persistent window behaviour)
+ """
+ if self.trayIcon.isVisible() and not self.debugmode:
+ QMessageBox.information(self, "Systray",
+ "The program will keep running "
+ "in the system tray. To "
+ "terminate the program, choose "
+ "<b>Quit</b> in the "
+ "context menu of the system tray entry.")
+ self.hide()
+ event.ignore()
+ if self.debugmode:
+ self.cleanupAndQuit()
+
+ def setIcon(self, name):
+ icon = self.Icons.get(name)
+ self.trayIcon.setIcon(icon)
+ self.setWindowIcon(icon)
+
+ def setToolTip(self):
+ """
+ get readable status and place it on systray tooltip
+ """
+ status = self.conductor.status.get_readable_status()
+ self.trayIcon.setToolTip(status)
+
+ def iconActivated(self, reason):
+ """
+ handles left click, left double click
+ showing the trayicon menu
+ """
+ #XXX there's a bug here!
+ #menu shows on (0,0) corner first time,
+ #until double clicked at least once.
+ if reason in (QSystemTrayIcon.Trigger,
+ QSystemTrayIcon.DoubleClick):
+ self.trayIconMenu.show()
+
+ def createWindowHeader(self):
+ """
+ description lines for main window
+ """
+ #XXX good candidate to refactor out! :)
+ self.headerBox = QGroupBox()
+ self.headerLabel = QLabel("<font size=40><b>E</b>ncryption \
+<b>I</b>nternet <b>P</b>roxy</font>")
+ self.headerLabelSub = QLabel("<i>trust your \
+technolust</i>")
+
+ pixmap = QPixmap(':/images/leapfrog.jpg')
+ frog_lbl = QLabel()
+ frog_lbl.setPixmap(pixmap)
+
+ headerLayout = QHBoxLayout()
+ headerLayout.addWidget(frog_lbl)
+ headerLayout.addWidget(self.headerLabel)
+ headerLayout.addWidget(self.headerLabelSub)
+ headerLayout.addStretch()
+ self.headerBox.setLayout(headerLayout)
+
+ def getIcon(self, icon_name):
+ # XXX get from connection dict
+ icons = {'disconnected': 0,
+ 'connecting': 1,
+ 'connected': 2}
+ return icons.get(icon_name, None)
+
+ def createIconGroupBox(self):
+ """
+ dummy icongroupbox
+ (to be removed from here -- reference only)
+ """
+ icons = {
+ 'disconnected': ':/images/conn_error.png',
+ 'connecting': ':/images/conn_connecting.png',
+ 'connected': ':/images/conn_connected.png'
+ }
+ con_widgets = {
+ 'disconnected': QLabel(),
+ 'connecting': QLabel(),
+ 'connected': QLabel(),
+ }
+ con_widgets['disconnected'].setPixmap(
+ QPixmap(icons['disconnected']))
+ con_widgets['connecting'].setPixmap(
+ QPixmap(icons['connecting']))
+ con_widgets['connected'].setPixmap(
+ QPixmap(icons['connected'])),
+ self.ConnectionWidgets = con_widgets
+
+ con_icons = {
+ 'disconnected': QIcon(icons['disconnected']),
+ 'connecting': QIcon(icons['connecting']),
+ 'connected': QIcon(icons['connected'])
+ }
+ self.Icons = con_icons
+
+ self.statusIconBox = QGroupBox("Connection Status")
+ statusIconLayout = QHBoxLayout()
+ statusIconLayout.addWidget(self.ConnectionWidgets['disconnected'])
+ statusIconLayout.addWidget(self.ConnectionWidgets['connecting'])
+ statusIconLayout.addWidget(self.ConnectionWidgets['connected'])
+ statusIconLayout.itemAt(1).widget().hide()
+ statusIconLayout.itemAt(2).widget().hide()
+ self.statusIconBox.setLayout(statusIconLayout)
+
+ def createActions(self):
+ """
+ creates actions to be binded to tray icon
+ """
+ self.connectVPNAction = QAction("Connect to &VPN", self,
+ triggered=self.hide)
+ # XXX change action name on (dis)connect
+ self.dis_connectAction = QAction("&(Dis)connect", self,
+ triggered=self.start_or_stopVPN)
+ self.minimizeAction = QAction("Mi&nimize", self,
+ triggered=self.hide)
+ self.maximizeAction = QAction("Ma&ximize", self,
+ triggered=self.showMaximized)
+ self.restoreAction = QAction("&Restore", self,
+ triggered=self.showNormal)
+ self.quitAction = QAction("&Quit", self,
+ triggered=self.cleanupAndQuit)
+
+ def createTrayIcon(self):
+ """
+ creates the tray icon
+ """
+ self.trayIconMenu = QMenu(self)
+
+ self.trayIconMenu.addAction(self.connectVPNAction)
+ self.trayIconMenu.addAction(self.dis_connectAction)
+ self.trayIconMenu.addSeparator()
+ self.trayIconMenu.addAction(self.minimizeAction)
+ self.trayIconMenu.addAction(self.maximizeAction)
+ self.trayIconMenu.addAction(self.restoreAction)
+ self.trayIconMenu.addSeparator()
+ self.trayIconMenu.addAction(self.quitAction)
+
+ self.trayIcon = QSystemTrayIcon(self)
+ self.trayIcon.setContextMenu(self.trayIconMenu)
+
+ def createLogBrowser(self):
+ """
+ creates Browser widget for displaying logs
+ (in debug mode only).
+ """
+ self.loggerBox = QGroupBox()
+ logging_layout = QVBoxLayout()
+ self.logbrowser = QTextBrowser()
+
+ startStopButton = QPushButton("&Connect")
+ startStopButton.clicked.connect(self.start_or_stopVPN)
+ self.startStopButton = startStopButton
+
+ logging_layout.addWidget(self.logbrowser)
+ logging_layout.addWidget(self.startStopButton)
+ self.loggerBox.setLayout(logging_layout)
+
+ # status box
+
+ self.statusBox = QGroupBox()
+ grid = QGridLayout()
+
+ self.updateTS = QLabel('')
+ self.status_label = QLabel('Disconnected')
+ self.ip_label = QLabel('')
+ self.remote_label = QLabel('')
+
+ tun_read_label = QLabel("tun read")
+ self.tun_read_bytes = QLabel("0")
+ tun_write_label = QLabel("tun write")
+ self.tun_write_bytes = QLabel("0")
+
+ grid.addWidget(self.updateTS, 0, 0)
+ grid.addWidget(self.status_label, 0, 1)
+ grid.addWidget(self.ip_label, 1, 0)
+ grid.addWidget(self.remote_label, 1, 1)
+ grid.addWidget(tun_read_label, 2, 0)
+ grid.addWidget(self.tun_read_bytes, 2, 1)
+ grid.addWidget(tun_write_label, 3, 0)
+ grid.addWidget(self.tun_write_bytes, 3, 1)
+
+ self.statusBox.setLayout(grid)
+
+ @pyqtSlot(str)
+ def onLoggerNewLine(self, line):
+ """
+ simple slot: writes new line to logger Pane.
+ """
+ if self.debugmode:
+ self.logbrowser.append(line[:-1])
+
+ def set_statusbarMessage(self, msg):
+ self.statusBar().showMessage(msg)
+
+ @pyqtSlot(object)
+ def onStatusChange(self, status):
+ """
+ slot for status changes. triggers new signals for
+ updating icon, status bar, etc.
+ """
+
+ print('STATUS CHANGED! (on Qt-land)')
+ print('%s -> %s' % (status.previous, status.current))
+ icon_name = self.conductor.get_icon_name()
+ self.setIcon(icon_name)
+ print 'icon = ', icon_name
+
+ # change connection pixmap widget
+ self.setConnWidget(icon_name)
+
+ def setConnWidget(self, icon_name):
+ #print 'changing icon to %s' % icon_name
+ oldlayout = self.statusIconBox.layout()
+
+ # XXX reuse with icons
+ # XXX move states to StateWidget
+ states = {"disconnected": 0,
+ "connecting": 1,
+ "connected": 2}
+
+ for i in range(3):
+ oldlayout.itemAt(i).widget().hide()
+ new = states[icon_name]
+ oldlayout.itemAt(new).widget().show()
+
+ @pyqtSlot()
+ def start_or_stopVPN(self):
+ """
+ stub for running child process with vpn
+ """
+ if self.vpn_service_started is False:
+ self.conductor.connect()
+ if self.debugmode:
+ self.startStopButton.setText('&Disconnect')
+ self.vpn_service_started = True
+
+ # XXX what is optimum polling interval?
+ # too little is overkill, too much
+ # will miss transition states..
+
+ self.timer.start(250.0)
+ return
+ if self.vpn_service_started is True:
+ self.conductor.disconnect()
+ # FIXME this should trigger also
+ # statuschange event. why isn't working??
+ if self.debugmode:
+ self.startStopButton.setText('&Connect')
+ self.vpn_service_started = False
+ self.timer.stop()
+ return
+
+ @pyqtSlot()
+ def onTimerTick(self):
+ self.statusUpdate()
+
+ @pyqtSlot()
+ def statusUpdate(self):
+ """
+ called on timer tick
+ polls status and updates ui with real time
+ info about transferred bytes / connection state.
+ """
+ # XXX it's too expensive to poll
+ # continously. move to signal events instead.
+
+ if not self.vpn_service_started:
+ return
+
+ # XXX remove all access to manager layer
+ # from here.
+ if self.conductor.manager.with_errors:
+ #XXX how to wait on pkexec???
+ #something better that this workaround, plz!!
+ time.sleep(10)
+ print('errors. disconnect.')
+ self.start_or_stopVPN() # is stop
+
+ state = self.conductor.poll_connection_state()
+ if not state:
+ return
+
+ ts, con_status, ok, ip, remote = state
+ self.set_statusbarMessage(con_status)
+ self.setToolTip()
+
+ ts = time.strftime("%a %b %d %X", ts)
+ if self.debugmode:
+ self.updateTS.setText(ts)
+ self.status_label.setText(con_status)
+ self.ip_label.setText(ip)
+ self.remote_label.setText(remote)
+
+ # status i/o
+
+ status = self.conductor.manager.get_status_io()
+ if status and self.debugmode:
+ #XXX move this to systray menu indicators
+ ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) = status
+ ts = time.strftime("%a %b %d %X", ts)
+ self.updateTS.setText(ts)
+ self.tun_read_bytes.setText(tun_read)
+ self.tun_write_bytes.setText(tun_write)
+
+ def cleanupAndQuit(self):
+ """
+ cleans state before shutting down app.
+ """
+ # TODO:make sure to shutdown all child process / threads
+ # in conductor
+ self.conductor.cleanup()
+ qApp.quit()
diff --git a/src/leap/eip/__init__.py b/src/leap/eip/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/eip/__init__.py
diff --git a/src/leap/eip/conductor.py b/src/leap/eip/conductor.py
new file mode 100644
index 0000000..e3adadc
--- /dev/null
+++ b/src/leap/eip/conductor.py
@@ -0,0 +1,272 @@
+"""
+stablishes a vpn connection and monitors its state
+"""
+from __future__ import (division, unicode_literals, print_function)
+#import threading
+from functools import partial
+import logging
+
+from leap.utils.coroutines import spawn_and_watch_process
+from leap.baseapp.config import get_config, get_vpn_stdout_mockup
+from leap.eip.vpnwatcher import EIPConnectionStatus, status_watcher
+from leap.eip.vpnmanager import OpenVPNManager, ConnectionRefusedError
+
+logger = logging.getLogger(name=__name__)
+
+
+# TODO Move exceptions to their own module
+
+
+class ConnectionError(Exception):
+ """
+ generic connection error
+ """
+ pass
+
+
+class EIPClientError(Exception):
+ """
+ base EIPClient exception
+ """
+ def __str__(self):
+ if len(self.args) >= 1:
+ return repr(self.args[0])
+ else:
+ return ConnectionError
+
+
+class UnrecoverableError(EIPClientError):
+ """
+ we cannot do anything about it, sorry
+ """
+ pass
+
+
+class OpenVPNConnection(object):
+ """
+ All related to invocation
+ of the openvpn binary
+ """
+ # Connection Methods
+
+ def __init__(self, config_file=None, watcher_cb=None):
+ #XXX FIXME
+ #change watcher_cb to line_observer
+ """
+ :param config_file: configuration file to read from
+ :param watcher_cb: callback to be \
+called for each line in watched stdout
+ :param signal_map: dictionary of signal names and callables \
+to be triggered for each one of them.
+ :type config_file: str
+ :type watcher_cb: function
+ :type signal_map: dict
+ """
+ # XXX get host/port from config
+ self.manager = OpenVPNManager()
+
+ self.config_file = config_file
+ self.watcher_cb = watcher_cb
+ #self.signal_maps = signal_maps
+
+ self.subp = None
+ self.watcher = None
+
+ self.server = None
+ self.port = None
+ self.proto = None
+
+ self.autostart = True
+
+ self._get_config()
+
+ def _set_command_mockup(self):
+ """
+ sets command and args for a command mockup
+ that just mimics the output from the real thing
+ """
+ command, args = get_vpn_stdout_mockup()
+ self.command, self.args = command, args
+
+ def _get_config(self):
+ """
+ retrieves the config options from defaults or
+ home file, or config file passed in command line.
+ """
+ config = get_config(config_file=self.config_file)
+ self.config = config
+
+ if config.has_option('openvpn', 'command'):
+ commandline = config.get('openvpn', 'command')
+ if commandline == "mockup":
+ self._set_command_mockup()
+ return
+ command_split = commandline.split(' ')
+ command = command_split[0]
+ if len(command_split) > 1:
+ args = command_split[1:]
+ else:
+ args = []
+ self.command = command
+ #print("debug: command = %s" % command)
+ self.args = args
+ else:
+ self._set_command_mockup()
+
+ if config.has_option('openvpn', 'autostart'):
+ autostart = config.get('openvpn', 'autostart')
+ self.autostart = autostart
+
+ def _launch_openvpn(self):
+ """
+ invocation of openvpn binaries in a subprocess.
+ """
+ #XXX TODO:
+ #deprecate watcher_cb,
+ #use _only_ signal_maps instead
+
+ if self.watcher_cb is not None:
+ linewrite_callback = self.watcher_cb
+ else:
+ #XXX get logger instead
+ linewrite_callback = lambda line: print('watcher: %s' % line)
+
+ observers = (linewrite_callback,
+ partial(status_watcher, self.status))
+ subp, watcher = spawn_and_watch_process(
+ self.command,
+ self.args,
+ observers=observers)
+ self.subp = subp
+ self.watcher = watcher
+
+ conn_result = self.status.CONNECTED
+ return conn_result
+
+ def _try_connection(self):
+ """
+ attempts to connect
+ """
+ if self.subp is not None:
+ print('cowardly refusing to launch subprocess again')
+ return
+ self._launch_openvpn()
+
+ def cleanup(self):
+ """
+ terminates child subprocess
+ """
+ if self.subp:
+ self.subp.terminate()
+
+
+class EIPConductor(OpenVPNConnection):
+ """
+ Manages the execution of the OpenVPN process, auto starts, monitors the
+ network connection, handles configuration, fixes leaky hosts, handles
+ errors, etc.
+ Preferences will be stored via the Storage API. (TBD)
+ Status updates (connected, bandwidth, etc) are signaled to the GUI.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.settingsfile = kwargs.get('settingsfile', None)
+ self.logfile = kwargs.get('logfile', None)
+ self.error_queue = []
+ self.desired_con_state = None # ???
+
+ status_signals = kwargs.pop('status_signals', None)
+ self.status = EIPConnectionStatus(callbacks=status_signals)
+
+ super(EIPConductor, self).__init__(*args, **kwargs)
+
+ def connect(self):
+ """
+ entry point for connection process
+ """
+ self.manager.forget_errors()
+ self._try_connection()
+ # XXX should capture errors?
+
+ def disconnect(self):
+ """
+ disconnects client
+ """
+ self._disconnect()
+ self.status.change_to(self.status.DISCONNECTED)
+ pass
+
+ def shutdown(self):
+ """
+ shutdown and quit
+ """
+ self.desired_con_state = self.status.DISCONNECTED
+
+ def connection_state(self):
+ """
+ returns the current connection state
+ """
+ return self.status.current
+
+ def desired_connection_state(self):
+ """
+ returns the desired_connection state
+ """
+ return self.desired_con_state
+
+ def poll_connection_state(self):
+ """
+ """
+ try:
+ state = self.manager.get_connection_state()
+ except ConnectionRefusedError:
+ # connection refused. might be not ready yet.
+ return
+ if not state:
+ return
+ (ts, status_step,
+ ok, ip, remote) = state
+ self.status.set_vpn_state(status_step)
+ status_step = self.status.get_readable_status()
+ return (ts, status_step, ok, ip, remote)
+
+ def get_icon_name(self):
+ """
+ get icon name from status object
+ """
+ return self.status.get_state_icon()
+
+ #
+ # private methods
+ #
+
+ def _disconnect(self):
+ """
+ private method for disconnecting
+ """
+ if self.subp is not None:
+ self.subp.terminate()
+ self.subp = None
+ # XXX signal state changes! :)
+
+ def _is_alive(self):
+ """
+ don't know yet
+ """
+ pass
+
+ def _connect(self):
+ """
+ entry point for connection cascade methods.
+ """
+ #conn_result = ConState.DISCONNECTED
+ try:
+ conn_result = self._try_connection()
+ except UnrecoverableError as except_msg:
+ logger.error("FATAL: %s" % unicode(except_msg))
+ conn_result = self.status.UNRECOVERABLE
+ except Exception as except_msg:
+ self.error_queue.append(except_msg)
+ logger.error("Failed Connection: %s" %
+ unicode(except_msg))
+ return conn_result
diff --git a/src/leap/eip/vpnmanager.py b/src/leap/eip/vpnmanager.py
new file mode 100644
index 0000000..78777cf
--- /dev/null
+++ b/src/leap/eip/vpnmanager.py
@@ -0,0 +1,262 @@
+from __future__ import (print_function)
+import logging
+import os
+import socket
+import telnetlib
+import time
+
+logger = logging.getLogger(name=__name__)
+
+TELNET_PORT = 23
+
+
+class MissingSocketError(Exception):
+ pass
+
+
+class ConnectionRefusedError(Exception):
+ pass
+
+
+class UDSTelnet(telnetlib.Telnet):
+
+ def open(self, host, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
+ """Connect to a host. If port is 'unix', it
+ will open a connection over unix docmain sockets.
+
+ The optional second argument is the port number, which
+ defaults to the standard telnet port (23).
+
+ Don't try to reopen an already connected instance.
+ """
+ self.eof = 0
+ if not port:
+ port = TELNET_PORT
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+
+ if self.port == "unix":
+ # unix sockets spoken
+ if not os.path.exists(self.host):
+ raise MissingSocketError
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ self.sock.connect(self.host)
+ except socket.error:
+ raise ConnectionRefusedError
+ else:
+ self.sock = socket.create_connection((host, port), timeout)
+
+
+# this class based in code from cube-routed project
+
+class OpenVPNManager(object):
+ """
+ Run commands over OpenVPN management interface
+ and parses the output.
+ """
+ # XXX might need a lock to avoid
+ # race conditions here...
+
+ def __init__(self, host="/tmp/.eip.sock", port="unix", password=None):
+ #XXX hardcoded host here. change.
+ self.host = host
+ if isinstance(port, str) and port.isdigit():
+ port = int(port)
+ self.port = port
+ self.password = password
+ self.tn = None
+
+ #XXX workaround for signaling
+ #the ui that we don't know how to
+ #manage a connection error
+ self.with_errors = False
+
+ def forget_errors(self):
+ print('forgetting errors')
+ self.with_errors = False
+
+ def connect(self):
+ """Connect to openvpn management interface"""
+ try:
+ self.close()
+ except:
+ #XXX don't like this general
+ #catch here.
+ pass
+ if self.connected():
+ return True
+ self.tn = UDSTelnet(self.host, self.port)
+
+ # XXX make password optional
+ # specially for win plat. we should generate
+ # the pass on the fly when invoking manager
+ # from conductor
+
+ #self.tn.read_until('ENTER PASSWORD:', 2)
+ #self.tn.write(self.password + '\n')
+ #self.tn.read_until('SUCCESS:', 2)
+
+ self._seek_to_eof()
+ self.forget_errors()
+ return True
+
+ def _seek_to_eof(self):
+ """
+ Read as much as available. Position seek pointer to end of stream
+ """
+ b = self.tn.read_eager()
+ while b:
+ b = self.tn.read_eager()
+
+ def connected(self):
+ """
+ Returns True if connected
+ rtype: bool
+ """
+ #return bool(getattr(self, 'tn', None))
+ try:
+ assert self.tn
+ return True
+ except:
+ #XXX get rid of
+ #this pokemon exception!!!
+ return False
+
+ def close(self, announce=True):
+ """
+ Close connection to openvpn management interface
+ """
+ if announce:
+ self.tn.write("quit\n")
+ self.tn.read_all()
+ self.tn.get_socket().close()
+ del self.tn
+
+ def _send_command(self, cmd, tries=0):
+ """
+ Send a command to openvpn and return response as list
+ """
+ if tries > 3:
+ return []
+ if not self.connected():
+ try:
+ self.connect()
+ except MissingSocketError:
+ #XXX capture more helpful error
+ #messages
+ #pass
+ return self.make_error()
+ try:
+ self.tn.write(cmd + "\n")
+ except socket.error:
+ logger.error('socket error')
+ print('socket error!')
+ self.close(announce=False)
+ self._send_command(cmd, tries=tries + 1)
+ return []
+ buf = self.tn.read_until(b"END", 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ if blist[-1].startswith('END'):
+ del blist[-1]
+ return blist
+ else:
+ return []
+
+ def _send_short_command(self, cmd):
+ """
+ parse output from commands that are
+ delimited by "success" instead
+ """
+ if not self.connected():
+ self.connect()
+ self.tn.write(cmd + "\n")
+ # XXX not working?
+ buf = self.tn.read_until(b"SUCCESS", 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ return blist
+
+ #
+ # useful vpn commands
+ #
+
+ def pid(self):
+ #XXX broken
+ return self._send_short_command("pid")
+
+ def make_error(self):
+ """
+ capture error and wrap it in an
+ understandable format
+ """
+ #XXX get helpful error codes
+ self.with_errors = True
+ now = int(time.time())
+ return '%s,LAUNCHER ERROR,ERROR,-,-' % now
+
+ def state(self):
+ """
+ OpenVPN command: state
+ """
+ state = self._send_command("state")
+ if not state:
+ return None
+ if isinstance(state, str):
+ return state
+ if isinstance(state, list):
+ if len(state) == 1:
+ return state[0]
+ else:
+ return state[-1]
+
+ def status(self):
+ """
+ OpenVPN command: status
+ """
+ status = self._send_command("status")
+ return status
+
+ def status2(self):
+ """
+ OpenVPN command: last 2 statuses
+ """
+ return self._send_command("status 2")
+
+ #
+ # parse info
+ #
+
+ def get_status_io(self):
+ status = self.status()
+ if isinstance(status, str):
+ lines = status.split('\n')
+ if isinstance(status, list):
+ lines = status
+ try:
+ (header, when, tun_read, tun_write,
+ tcp_read, tcp_write, auth_read) = tuple(lines)
+ except ValueError:
+ return None
+
+ when_ts = time.strptime(when.split(',')[1], "%a %b %d %H:%M:%S %Y")
+ sep = ','
+ # XXX cleanup!
+ tun_read = tun_read.split(sep)[1]
+ tun_write = tun_write.split(sep)[1]
+ tcp_read = tcp_read.split(sep)[1]
+ tcp_write = tcp_write.split(sep)[1]
+ auth_read = auth_read.split(sep)[1]
+
+ # XXX this could be a named tuple. prettier.
+ return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read)
+
+ def get_connection_state(self):
+ state = self.state()
+ if state is not None:
+ ts, status_step, ok, ip, remote = state.split(',')
+ ts = time.gmtime(float(ts))
+ # XXX this could be a named tuple. prettier.
+ return ts, status_step, ok, ip, remote
diff --git a/src/leap/eip/vpnwatcher.py b/src/leap/eip/vpnwatcher.py
new file mode 100644
index 0000000..09bd581
--- /dev/null
+++ b/src/leap/eip/vpnwatcher.py
@@ -0,0 +1,169 @@
+"""generic watcher object that keeps track of connection status"""
+# This should be deprecated in favor of daemon mode + management
+# interface. But we can leave it here for debug purposes.
+
+
+class EIPConnectionStatus(object):
+ """
+ Keep track of client (gui) and openvpn
+ states.
+
+ These are the OpenVPN states:
+ CONNECTING -- OpenVPN's initial state.
+ WAIT -- (Client only) Waiting for initial response
+ from server.
+ AUTH -- (Client only) Authenticating with server.
+ GET_CONFIG -- (Client only) Downloading configuration options
+ from server.
+ ASSIGN_IP -- Assigning IP address to virtual network
+ interface.
+ ADD_ROUTES -- Adding routes to system.
+ CONNECTED -- Initialization Sequence Completed.
+ RECONNECTING -- A restart has occurred.
+ EXITING -- A graceful exit is in progress.
+
+ We add some extra states:
+
+ DISCONNECTED -- GUI initial state.
+ UNRECOVERABLE -- An unrecoverable error has been raised
+ while invoking openvpn service.
+ """
+ CONNECTING = 1
+ WAIT = 2
+ AUTH = 3
+ GET_CONFIG = 4
+ ASSIGN_IP = 5
+ ADD_ROUTES = 6
+ CONNECTED = 7
+ RECONNECTING = 8
+ EXITING = 9
+
+ # gui specific states:
+ UNRECOVERABLE = 11
+ DISCONNECTED = 0
+
+ def __init__(self, callbacks=None):
+ """
+ EIPConnectionStatus is initialized with a tuple
+ of signals to be triggered.
+ :param callbacks: a tuple of (callable) observers
+ :type callbacks: tuple
+ """
+ # (callbacks to connect to signals in Qt-land)
+ self.current = self.DISCONNECTED
+ self.previous = None
+ self.callbacks = callbacks
+
+ def get_readable_status(self):
+ # XXX DRY status / labels a little bit.
+ # think we'll want to i18n this.
+ human_status = {
+ 0: 'disconnected',
+ 1: 'connecting',
+ 2: 'waiting',
+ 3: 'authenticating',
+ 4: 'getting config',
+ 5: 'assigning ip',
+ 6: 'adding routes',
+ 7: 'connected',
+ 8: 'reconnecting',
+ 9: 'exiting',
+ 11: 'unrecoverable error',
+ }
+ return human_status[self.current]
+
+ def get_state_icon(self):
+ """
+ returns the high level icon
+ for each fine-grain openvpn state
+ """
+ connecting = (self.CONNECTING,
+ self.WAIT,
+ self.AUTH,
+ self.GET_CONFIG,
+ self.ASSIGN_IP,
+ self.ADD_ROUTES)
+ connected = (self.CONNECTED,)
+ disconnected = (self.DISCONNECTED,
+ self.UNRECOVERABLE)
+
+ # this can be made smarter,
+ # but it's like it'll change,
+ # so +readability.
+
+ if self.current in connecting:
+ return "connecting"
+ if self.current in connected:
+ return "connected"
+ if self.current in disconnected:
+ return "disconnected"
+
+ def set_vpn_state(self, status):
+ """
+ accepts a state string from the management
+ interface, and sets the internal state.
+ :param status: openvpn STATE (uppercase).
+ :type status: str
+ """
+ if hasattr(self, status):
+ self.change_to(getattr(self, status))
+
+ def set_current(self, to):
+ """
+ setter for the 'current' property
+ :param to: destination state
+ :type to: int
+ """
+ self.current = to
+
+ def change_to(self, to):
+ """
+ :param to: destination state
+ :type to: int
+ """
+ if to == self.current:
+ return
+ changed = False
+ from_ = self.current
+ self.current = to
+
+ # We can add transition restrictions
+ # here to ensure no transitions are
+ # allowed outside the fsm.
+
+ self.set_current(to)
+ changed = True
+
+ #trigger signals (as callbacks)
+ #print('current state: %s' % self.current)
+ if changed:
+ self.previous = from_
+ if self.callbacks:
+ for cb in self.callbacks:
+ if callable(cb):
+ cb(self)
+
+
+def status_watcher(cs, line):
+ """
+ a wrapper that calls to ConnectionStatus object
+ :param cs: a EIPConnectionStatus instance
+ :type cs: EIPConnectionStatus object
+ :param line: a single line of the watched output
+ :type line: str
+ """
+ #print('status watcher watching')
+
+ # from the mullvad code, should watch for
+ # things like:
+ # "Initialization Sequence Completed"
+ # "With Errors"
+ # "Tap-Win32"
+
+ if "Completed" in line:
+ cs.change_to(cs.CONNECTED)
+ return
+
+ if "Initial packet from" in line:
+ cs.change_to(cs.CONNECTING)
+ return
diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/gui/__init__.py
diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py
new file mode 100644
index 0000000..e5a671f
--- /dev/null
+++ b/src/leap/gui/mainwindow_rc.py
@@ -0,0 +1,789 @@
+# -*- coding: utf-8 -*-
+
+# Resource object code
+#
+# Created: Sun Jul 22 17:08:49 2012
+# by: The Resource Compiler for PyQt (Qt v4.8.2)
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore
+
+qt_resource_data = "\
+\x00\x00\x0d\xf3\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\
+\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
+\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0e\x74\x45\
+\x58\x74\x54\x69\x74\x6c\x65\x00\x43\x6f\x6d\x70\x75\x74\x65\x72\
+\xf8\x18\x12\x76\x00\x00\x00\x17\x74\x45\x58\x74\x41\x75\x74\x68\
+\x6f\x72\x00\x4c\x61\x70\x6f\x20\x43\x61\x6c\x61\x6d\x61\x6e\x64\
+\x72\x65\x69\xdf\x91\x1a\x2a\x00\x00\x0d\x33\x49\x44\x41\x54\x68\
+\xde\xdd\x9a\x7b\x8c\x5d\xc5\x7d\xc7\x3f\xbf\x99\x39\xe7\x3e\xf6\
+\xe9\x5d\x7b\x8d\x6d\xd6\x6b\x9b\xda\x98\xd2\x42\x03\xf1\xda\x4b\
+\x83\x09\x46\x94\x04\xa8\x54\x29\x75\x84\x92\x86\x54\x04\x35\x81\
+\x4a\x50\x55\xb4\x85\xb4\x55\x4b\xd3\xfe\x55\x12\x25\x0a\x6d\x95\
+\x84\x14\xa9\x4a\x68\x28\x49\x09\x34\x60\x4a\x52\xe1\x40\xa1\x60\
+\x43\x40\x25\x80\xcd\xc3\xac\xf1\xe2\xc7\xae\xf7\xe1\xdd\xbd\xf7\
+\xee\xbd\xe7\xcc\xfc\xfa\xc7\x39\xf7\xee\xae\x03\xc6\x3c\x54\x4a\
+\x8f\x34\x3b\x73\xe7\xcc\x39\xe7\xf7\xfc\x7e\x7f\x33\x5a\x51\x55\
+\x3e\xc8\x97\xf9\x40\x4b\xff\xff\x41\x01\xf7\x6e\x1e\xde\xf1\xe3\
+\x7b\xbf\x8e\xca\xef\xa3\x48\xd0\x40\x08\x81\x10\x14\x5d\x38\xf6\
+\x81\x40\xc0\x7b\x45\x43\x20\xa8\xcf\xe6\x43\xbe\x46\x15\xef\x7d\
+\xfe\x5b\x09\x1a\x08\xde\xb7\xc6\xd9\xbd\x7c\x3e\x04\x34\x28\xde\
+\x87\x20\x12\xfe\xe8\xcf\xff\xf4\xaf\xbe\xf2\x8e\x15\xd8\xb1\x63\
+\x47\x27\x86\xcf\xff\xfa\x79\x5b\xc5\x18\x83\x88\x41\x44\x10\x04\
+\x11\x20\x1f\x83\xa2\x9a\xb5\x4c\xe0\x5c\xb1\xe0\xf1\x4d\x25\x82\
+\xc7\xfb\xac\x4f\xbd\x27\xf8\x94\xd4\x7b\x7c\x9a\xf7\xf9\x6f\x0d\
+\x01\x55\xa5\x56\xab\x99\x87\x76\x3e\xf4\x25\xe0\x9d\x2b\x50\xaf\
+\xd7\x4d\xa1\x6c\x43\xa5\x5a\xe7\xcc\xcb\xfe\x84\xf0\xae\xb1\x40\
+\xd1\xac\x3b\xe1\xd5\x5e\x72\xdc\xfd\xb5\xcf\x11\xbc\x9a\x77\x1d\
+\x42\xad\x30\x41\xe9\x3b\x6d\x10\x05\x34\x97\x42\x95\xcc\xf2\xcd\
+\xb9\x05\xe3\x81\x15\x5d\xa0\xe0\xbd\x27\x0d\x4a\x23\xf1\x8c\x4e\
+\x56\x50\x85\xb6\x52\xcc\x86\xd5\x3d\x84\xdc\x22\x59\x28\x65\x2d\
+\x49\x12\xfe\x7b\xd7\x4f\x89\xa2\x88\xa0\x81\xf7\x46\x01\x55\xc0\
+\xa0\x62\x17\x59\x52\xf3\x91\x1e\x37\xe7\x9c\xc1\x5a\x47\xea\x03\
+\x2a\xa0\xa2\x54\xea\x09\x01\x8b\x0a\x2c\xef\xed\x20\x04\x21\xe4\
+\xca\x86\x3c\x0c\xb3\x96\x61\x4e\x14\xc5\x84\xf0\x9e\x28\x90\xc5\
+\x32\x2c\xb6\x76\x73\x8c\xe6\x9e\x68\xde\x57\x88\x8b\x86\xd4\x67\
+\x9e\x4b\x43\x20\xf5\xca\x6c\x2d\x41\x55\x29\xc6\x8e\x38\xb2\xcc\
+\x25\x1e\x14\x42\xee\x49\x50\x42\x96\xbc\x00\xc4\xef\x8d\x02\x53\
+\xa4\x69\x1b\x8d\x24\x69\x7d\x60\x5e\x89\x4c\xea\x85\x82\x6b\x3e\
+\x11\x1b\x93\x85\x8e\xcf\x14\xa8\xd4\x1a\xa4\x3e\x00\x4a\x4f\x67\
+\x91\x7a\xc3\x67\x9e\xd3\xf9\x7c\x08\x9a\xf9\xd2\xa7\x3e\xf7\x40\
+\xf4\xde\x78\xc0\xfb\x94\xa4\x91\x80\x2a\xde\xa7\x2d\x21\xe7\x3d\
+\xc1\x22\x61\x14\xc5\x18\xa5\x91\x24\x78\x0f\x5e\x61\xb6\x5a\x47\
+\x83\x12\xc7\x16\x6b\x0d\x8d\x34\x83\xcd\xd6\x3b\xf2\xe7\x83\x2a\
+\xea\xdf\x43\x05\xa6\x00\x5b\xaf\x33\x33\x3b\x8d\xa6\x09\x3e\x99\
+\xcb\x43\x69\xfe\xc3\x20\xad\xf5\x39\xb2\x22\xa4\xf8\x44\x09\x01\
+\xea\x69\x20\x4d\x53\x44\x2c\x9d\xe5\x02\x49\xea\x51\x95\x96\xc5\
+\x51\x5a\xc9\xac\x79\xd2\x03\x38\xe7\x5a\xa1\xeb\xde\x45\x04\x51\
+\x49\x66\x65\x6c\x6c\x14\x4d\xe7\x08\x8d\xd9\x5c\x50\x83\x08\x98\
+\x8c\x0c\xe6\xa5\x07\x22\x6b\xd1\x7a\x05\x9f\x27\x67\x52\xf7\x44\
+\x62\x70\xb1\xa5\x18\x3b\x14\xc9\xd7\x4a\x06\x10\x3e\x27\x2e\x0d\
+\xf8\x30\xef\x01\x6b\x1d\xbe\xe9\x81\x77\xca\xa6\xe2\x94\xa2\x29\
+\x72\xe4\xf0\x01\xbe\x7c\xc3\x6f\xbc\x0d\x36\xad\xa1\x41\x49\x7d\
+\x60\xf7\xb0\xf2\xec\x91\x12\xed\xc5\x32\x18\x45\xb1\xf3\x80\x93\
+\x2b\xae\xa2\xad\x3c\xca\x53\x00\x6b\x0c\xc1\x6b\xee\x81\xf0\xbe\
+\xb1\x29\x89\x7f\x88\xbd\xa3\x1e\x67\x00\x35\x18\x63\x50\x11\x24\
+\xf3\x05\x06\x21\x10\xf2\x9c\x50\x54\x9a\x1e\x15\xb4\xc5\x03\x22\
+\xef\x1b\x9b\xaa\x57\x22\x97\x3d\x20\xc6\x20\x62\x09\x22\xa0\x8a\
+\x48\x2e\x7c\x33\x04\x8d\x6f\xc5\x62\x66\xa8\x5c\x81\xf7\x9b\x4d\
+\x0d\x20\x22\x18\x31\xa8\xc9\xc8\x50\x0c\xa0\x92\x59\xbc\x69\x11\
+\x35\xb9\x4f\xc0\x87\xf4\x38\x05\xde\x47\x36\xb5\x51\x84\x73\x16\
+\xe3\x2c\x18\x47\x40\x5a\x84\xd5\x72\x64\x00\x15\xc1\x64\xe5\x0f\
+\x69\xb2\x48\x81\xf7\x8f\x4d\x55\x03\x91\xb3\xb8\x28\xc2\xb9\x18\
+\xb5\x0e\xaf\x64\xb9\x68\x04\x31\x01\x11\x8f\x78\x01\x3c\x84\xcc\
+\xc0\x49\x92\xcc\x2b\xe0\x7d\x20\xcd\xe1\x29\x2c\x0a\x99\x13\xb0\
+\xa9\xb5\x64\xcf\x29\x69\xd0\x77\xcc\xa6\x69\x9a\xb2\xf7\xc9\xff\
+\xc0\x44\x65\x24\x2a\x22\xc6\xe5\xf1\xd3\x0c\x04\x5d\x14\xd2\x22\
+\x59\x6e\x24\x69\xa3\x15\x9e\x2e\x04\x4f\xc8\xad\xd2\xcc\x85\x37\
+\x12\x7c\x21\x9b\x5a\x6b\x48\x7c\x06\xab\x3e\x28\xb3\xb5\x06\xaa\
+\x4a\x1c\xbd\x3d\x36\x35\x02\x5f\xbb\xe9\x93\xf4\xf4\xf6\xd0\xd5\
+\xd5\x4d\xa1\x10\x63\x8c\x69\xc1\xb0\x0f\x01\xef\x43\x8e\x76\x59\
+\x23\xcf\xa3\x45\x39\x90\xdd\x20\xcf\x85\xe3\x2c\x9e\x71\x68\xc6\
+\xa2\x22\x20\x8a\x8b\x6c\x0b\x87\x1b\xde\xe3\x43\xe6\xf6\xce\xb6\
+\x02\x49\x1a\xe6\x85\x7d\x0b\x36\x45\x95\xae\x8e\x76\x7a\xba\x3b\
+\x59\xb2\xa4\x8b\x52\xa9\x84\x31\x19\x44\xfa\x90\x41\x76\x16\xc2\
+\x81\xe7\x9e\x7f\x81\x27\x9f\x7c\x92\x6d\xdb\xb6\x51\xab\x56\x09\
+\xf9\x4b\x5d\x86\xcf\xc9\x3c\xe5\x0b\x18\xb1\xd0\x14\x98\x79\xec\
+\x05\x25\x8e\x1c\xd6\xd8\x2c\xa9\x45\xf1\xf5\x94\x38\x8a\xb1\x56\
+\x28\x15\xe2\xdc\x08\x7a\x52\x6c\x9a\xa6\x29\xf7\xdc\x73\x37\xe5\
+\xf6\x36\x4a\xe5\x12\x91\x73\xb5\x8c\x6b\x32\xd2\x5b\x48\x8c\xd5\
+\x4a\x25\x2e\x95\xca\x72\xe7\xf7\xfe\x39\x20\x12\x50\xfe\x2e\x53\
+\x20\x28\x3e\x4d\x5b\x6e\xcd\xea\x15\x03\x86\x79\x52\x23\xb3\x3c\
+\x08\xc5\x42\x84\xb1\x36\x77\x4c\x40\x8c\xa5\x50\x30\xb4\x15\x23\
+\xc4\x66\x88\x91\x27\xd3\x5b\xb2\xa9\x2a\x9c\xf5\x6b\x67\xd1\xb7\
+\xfc\x14\xf6\xef\x1f\xae\x3f\xff\xec\x9e\xd3\xcb\xe5\xb2\x2f\x16\
+\x8b\x01\xaa\x54\x81\xec\x0f\x80\xa1\x3a\x3b\x07\xd4\xa8\xd7\x5d\
+\x7a\xc7\x1d\x77\x1c\x05\x70\x1a\xb2\x24\x56\x20\x72\x76\x9e\x8d\
+\x8d\xb4\x14\x40\x72\x66\x56\xa1\x54\x88\x30\x56\x10\x03\x69\xc3\
+\x13\x45\x31\x56\x20\x8e\x1d\x21\x28\xc6\x64\xf4\x23\x39\x78\x9e\
+\x88\x4d\x41\xf9\xd5\x33\xcf\x66\xf5\xea\x01\xfa\x96\xf5\x6a\x14\
+\xb9\xf3\x6e\xfa\xe3\x3f\xbb\x0f\x98\x03\xbc\x9e\xc4\xa1\x95\xcb\
+\xe2\x2c\x7b\x9f\x73\x0e\x63\x72\x08\x93\x8c\x34\xc4\x98\xbc\x30\
+\xcb\x3c\x12\xc7\x31\x82\x66\xac\x6d\xa1\x10\x0b\xce\x99\xfc\x1d\
+\x8a\xf1\x82\x1a\x05\x95\xb7\x64\x53\xef\x3d\x2f\xbf\xfc\x0a\x95\
+\x6a\x95\xb6\xf6\xb6\xe2\xca\x15\x2b\xbf\xfb\x8d\x6f\xfe\xfd\x70\
+\xb1\x58\x78\xa1\xab\xa7\xdb\x3d\xf0\xe0\x8f\xda\x8c\x31\x91\x08\
+\x36\x64\x85\x67\x8a\x6a\x25\x68\x78\x58\x93\xb9\x7f\xb8\xf4\xd2\
+\xed\x87\x5d\x08\xa1\xe5\x6d\x17\x39\x8c\x64\x02\x8b\xcd\x7b\x31\
+\x18\x93\x07\x92\x11\x5c\xe4\x5a\x21\x12\x47\x92\xbd\x36\x67\x59\
+\x54\xf1\x6a\x20\x78\xc4\xc8\x49\xb1\x69\x5c\xb4\x88\xf1\xc4\x05\
+\xcb\xe0\xe0\xa0\x0d\x9e\x75\xdf\xfe\xc7\xdb\xa2\xa0\x7c\x61\xf5\
+\xaa\x15\xe3\xcb\x96\x2d\x4b\xa3\x28\xb6\x59\xd4\x19\x67\x62\xbb\
+\xa4\x5c\x2a\x5d\x61\xe3\xf2\x1d\xc0\xb6\x4c\x81\x1c\xac\xad\xb1\
+\x18\x63\xb0\xd6\xe4\x82\x1b\x6c\xd3\x1b\x26\x53\xc4\x5a\x03\x16\
+\x08\x81\xa2\xc9\x43\x3e\xaf\x5e\xbd\x0f\x88\x51\xc4\x9b\x16\xda\
+\x9c\x88\x4d\xd3\x34\xe5\x87\x3f\xbc\x27\x8d\xa2\x48\xad\xcd\xbe\
+\x0d\x62\x66\x66\x66\x76\xef\x7a\x7c\xf7\x81\xa1\xa1\xa1\x99\xab\
+\xbf\xf0\xd9\xbf\x70\x36\xfe\xa8\x20\x25\xd0\xa2\x0a\x3e\x84\x10\
+\x19\x23\x3f\x12\x11\x71\x4d\xa8\xca\x90\xc1\x60\x16\x28\x61\x8d\
+\xc1\x58\x83\xc9\x15\x70\x56\xf2\xd8\x15\xac\x4a\x6e\xd8\x0c\x5d\
+\x7c\x08\x58\xa3\xa4\xc1\x67\xec\xd9\xcc\xa3\x13\xb3\x69\xe3\xc5\
+\x3d\x2f\x0f\x01\x9c\x61\x4c\xf9\x0f\x55\x2f\x59\x11\xc2\x87\xdb\
+\xd2\x74\x8b\xed\xe8\xf8\x59\xfd\x95\x97\x67\xa6\x6f\xfd\x56\x2d\
+\x99\xa9\x7c\xf3\x89\x8b\x2e\xba\x6b\xbf\x6b\xcc\x74\xba\xc8\x2e\
+\x59\xd2\x55\xdf\xb9\x73\xf7\x58\x2b\x89\x51\xa5\x5e\xaf\xf3\xd2\
+\xee\x07\x10\xe6\xd1\x63\x61\x55\x79\x7c\x36\xc9\x9b\xcc\xff\x62\
+\x81\xfa\xe6\x6c\xaa\x4a\xbc\x69\xcb\xe0\x53\x9b\x9c\x65\xfb\x53\
+\xcf\x68\xb4\xa4\x4b\x4c\x77\x17\x2c\xed\x45\x3a\x3b\x30\x93\x53\
+\x3d\x85\x23\xa3\xd4\xaa\x95\x9b\xcf\xfb\xce\x77\xfe\x32\xbd\xe8\
+\xa3\xbb\x1e\x38\x78\xe4\x82\x1d\x3b\x76\xa4\x19\xb9\xab\xba\x10\
+\x02\xe5\x72\xc4\x03\xdf\xb8\x86\xa6\x1b\xb3\x50\xca\xc6\x0b\x9b\
+\x48\x13\x99\x9a\x4c\x9d\xe3\x74\xbe\x1f\xc8\xc6\x9e\x26\x39\x9e\
+\x0c\x9b\xfe\xf6\xe8\x28\x43\x87\x8e\x10\xad\xe9\x17\x2d\x95\xd0\
+\xc3\x47\x08\x07\x5e\x47\x2b\x55\x4c\x7b\x1b\xa6\xbd\x8d\xf6\x8d\
+\x1b\x28\x8e\x1d\x95\x8f\xdd\xf7\xc0\xa6\xb5\xa7\xae\xbc\xed\x7e\
+\xd5\xcf\xb4\x50\x28\x4d\x52\xa3\xaa\xf4\xf5\xf6\x64\xe1\x62\x04\
+\x2b\x16\xb1\x16\x23\xd2\x12\xfc\x8d\x15\x68\x6e\x72\xe6\x77\x70\
+\xd9\x6f\x9f\x6d\x70\xde\x82\x4d\xd7\x4e\x4c\xb2\xe9\xb5\x11\xdc\
+\xc6\xf5\xe8\xc4\x24\xe1\xb9\x3d\x39\xf4\xe6\xb5\xd8\xc4\x24\x3a\
+\x3e\x41\x78\x6d\x04\x33\xd0\x4f\xc7\x87\xce\x32\x6b\x77\x3d\xf5\
+\xe9\x87\xfb\xfa\xbe\xb2\x75\x74\xf4\x69\x00\xf9\xfa\xad\x5f\xfd\
+\xbe\xd7\x70\x39\x68\x60\x11\x0b\x2e\x14\x4e\x21\x28\x81\x40\xea\
+\xd3\x52\x56\x1e\x84\x7c\x9b\xd8\xdc\x66\x6a\x66\xf5\x90\x6f\x1f\
+\xdf\x80\x4d\x6b\xd5\x2a\xa5\x52\x99\x5a\xb5\x4a\x21\x04\xae\xff\
+\xe9\xa3\x2c\x19\xe8\xc7\xcc\xd5\x61\xec\x68\x5e\xaa\x48\x2b\x27\
+\x9b\x3b\x40\xcd\xbf\x21\xa7\xae\xa4\x31\x57\xe7\xc8\xf0\xfe\xd1\
+\xe1\x89\x89\x95\xdb\x55\xbd\xa8\x2a\xb7\xdc\x72\x4b\x9f\x6a\x0e\
+\x0f\x4d\x06\x5c\xc0\x82\x8d\x46\x43\xd2\x74\x5a\x8e\xcd\x25\x6d\
+\x6b\xfa\x57\xbf\x70\xde\x96\x21\x5b\xad\x55\x5b\x59\x20\x22\x44\
+\x71\x91\x27\x1e\x7f\x9c\xbe\xde\xe5\x74\x76\x76\xd1\xd9\xd9\x41\
+\xb1\x58\xc4\xc5\x11\x86\xdc\x73\x62\x20\xdf\xf0\xb7\xff\xe0\x5f\
+\xe9\xdc\x71\x3f\xf1\xd2\x5e\xd8\x7f\x20\x4f\x7a\x93\x97\x2f\xb4\
+\x84\x9e\xf7\x6a\xc6\x27\xb2\x76\x80\xf1\xbd\x2f\x06\x3f\x3a\xf6\
+\xa9\xb3\xd3\xf4\x4e\x07\x70\xc3\x0d\x37\x8c\xbe\x59\x0e\x4a\xf6\
+\x46\x07\x94\xbe\xfc\xd5\xbf\xfd\xcd\xa1\xcd\x5b\x1a\x9b\x3e\xbc\
+\xb9\x54\xaf\xd7\x17\xad\x2b\x14\x8a\x84\x34\xe1\xce\x7f\xb9\x6b\
+\xee\xe8\xd1\xa3\xbe\x50\x28\x84\xc8\x45\x39\x34\x82\x88\x2c\xca\
+\xf7\x2b\x1f\xdf\x55\xee\x2e\x95\x9d\x8e\x4f\xe6\x07\x11\x92\x1f\
+\xbd\x48\x2b\xf1\x5b\x8c\xdd\x24\xaa\x10\xd0\xc9\x29\x8a\xe5\xb2\
+\xa9\xc0\x47\x80\x3b\x9d\x2c\xaa\xd8\x7e\xe1\x32\x40\xbc\x65\xeb\
+\x96\x81\x2b\x3e\xf1\x89\x2f\xf5\x2e\x5b\x7a\xb9\x11\x57\x78\xfa\
+\x99\x67\x68\x34\x1a\xc7\x29\x50\xa0\x5c\xee\xe4\x82\xad\x5b\xa3\
+\x6a\xa5\x72\x28\x8a\xa2\x17\x3b\x97\x74\xbb\x62\x21\x2e\x1b\x63\
+\x22\x63\xc4\x68\xce\xa6\xea\xb5\x7a\xea\xfd\x0f\x9e\xcb\x69\x4b\
+\x1c\xb5\x3a\xcb\x5e\x7a\x29\x87\xe7\xb7\x38\xca\x9c\x9c\xe4\xd0\
+\x96\x2d\xb8\x72\x89\xa0\x7a\x7e\xf3\x5c\xc8\x6c\xdf\xbe\xdd\x8e\
+\x8c\x8c\xd8\x85\x8b\x1b\x8d\x86\x8c\x8d\x8d\x15\xa6\xa6\xa6\x7a\
+\x87\x36\x6d\xbe\xff\xf2\xcb\x7f\x6b\x40\x25\xc8\xb1\x63\x93\x40\
+\xa0\xe8\x8e\x3f\x52\xf2\x14\xac\x65\x68\x68\xc8\x36\xea\xe9\x9a\
+\xdb\xbe\xfd\x2d\xfb\x66\x6c\x1a\xcd\x1c\x5b\x1e\xaa\xd5\xef\x8b\
+\xb5\x59\x75\x7a\x12\xc2\xe7\x35\x38\xda\xa8\x63\xda\xcb\xa8\xea\
+\x1a\x00\x77\xee\xb9\xe7\x9a\x7d\xfb\xf6\xb9\x62\xb1\x68\x8f\xb7\
+\xbe\x88\x14\x8d\x31\xed\x7b\x5e\xd8\xf3\xf3\x9b\xbe\x78\x63\x7f\
+\x5e\x6b\xbe\xf5\xd9\x84\x72\x62\x36\x6d\x2f\xf9\xb4\x5c\xd2\xb4\
+\x56\x13\x27\x39\x49\x9c\x8c\x12\x69\x8a\x3a\x47\x5a\xa9\x02\x0c\
+\x03\xb8\x75\xeb\xd6\x85\x91\x91\x11\x9d\x9a\x9a\xd2\xc5\x6b\xd3\
+\xe0\xbd\xaf\x3b\xe7\x46\x1f\x7b\xf4\xf1\xeb\x7a\x7a\x7a\x3a\xac\
+\xb5\xf6\x04\xe1\xd6\x92\xdf\x7b\xef\x27\x27\x27\xa7\xa3\x28\x9a\
+\xfb\xcc\x67\x3f\x75\x9d\xc1\xac\x18\x3b\x7c\xe4\x93\xe3\xe3\xd3\
+\xc7\x8e\x56\x8f\xd6\x3a\x5d\x64\xd7\x54\xab\xff\x94\x4c\xcf\x6c\
+\x15\x31\x8c\xf4\xf7\xb7\x10\x68\x11\x41\xe6\x30\xdd\xec\x55\x15\
+\xe9\xee\xa2\x31\x33\xa3\xa8\x3e\x02\xe0\xee\xba\xeb\xae\x00\x34\
+\x6e\xbc\xf1\xc6\xb5\xd6\xf1\x93\x99\xd9\xd9\x15\xb5\x7a\x23\xee\
+\xed\xed\x61\x7c\x7c\x82\xb7\xd3\xd7\x6b\xb5\x7a\xa1\x54\x2a\x8c\
+\x8f\x4f\xb0\xb4\xa7\x87\xc4\x37\xe6\x9e\xdf\xb3\xa7\x30\x35\x7e\
+\x4c\xbb\x7b\xbb\x2e\xae\xd7\xe6\x1a\x51\x64\x1f\x7b\xf4\x89\x9f\
+\x5d\xf6\xf1\x4a\xf5\xdf\xeb\x93\x53\x9b\xa3\xfe\x55\x85\x30\x3d\
+\xdd\x42\x9f\x37\x62\xf1\x96\x02\x80\x29\x95\x48\x46\x5e\xaf\x00\
+\xff\x09\x60\x34\xbb\x7c\x1a\xea\x7f\xb3\x72\xd5\xaa\xe1\xee\x25\
+\xdd\xd1\x19\x1b\x37\x60\x44\x78\xbb\xfd\xe0\xe0\x66\xe7\x9c\xd1\
+\xbe\xde\x5e\x35\xd6\xe8\xc6\x0d\x67\x58\x9f\x7a\x59\x7b\xda\x80\
+\xf1\xa9\xb7\x6b\xd7\xae\x29\x46\x51\x3c\x70\xe6\x59\x67\x5c\x99\
+\x84\x70\x6b\x75\x7c\x7c\xa6\x51\xab\x41\x47\x7b\x76\x14\x79\x7c\
+\x0b\xf3\x7b\xe3\x10\x02\xf4\x74\x33\x37\x39\xa9\x8d\x4a\xe5\xf5\
+\x69\xf8\x41\x13\x65\xb8\xfe\xfa\x6b\x7e\xc5\x88\xdd\x76\xda\xba\
+\xd3\x36\xad\xee\xef\x17\x9f\xa4\xac\x5b\xbb\x86\xa4\xd1\xa0\xb3\
+\xa3\xfd\x4d\xfb\xe6\xba\x66\x3f\x3b\x33\x6d\xcf\x39\xfb\x43\xd2\
+\xd3\xdb\x23\xa7\x9f\xbe\x41\xba\xba\x3b\xa3\xd5\xab\xfb\x5b\xf7\
+\xa7\xa7\xa7\xe5\xfc\x8f\x6c\x2d\x25\xf5\xc6\xcd\xd7\x0d\x0e\x46\
+\xcf\x85\x70\xed\xd4\x2b\xaf\x56\x43\x7b\x1b\xda\xdd\x95\x97\x24\
+\x59\xf3\xde\xe3\x9b\x4a\x68\x40\x7a\x7b\x08\x51\xcc\xcc\x81\x03\
+\xb5\x47\x55\xaf\xbe\x30\x3f\x98\x76\x00\xf5\x46\xfa\xd7\x9f\xbb\
+\xfa\x77\x5e\x3b\x78\xe8\xe0\x39\xcf\x3c\xfd\x0c\x81\xc0\xf0\x6b\
+\xfb\xb3\x5d\x24\x82\xe6\x67\x95\xe4\xa7\xc7\xb2\xa0\x9c\x1b\x19\
+\x19\x41\x55\x39\x70\x60\x84\xcd\x83\x9b\x99\x9e\x99\xe5\xe0\xeb\
+\xaf\x33\x31\x31\xce\xe9\x1b\x36\x32\x76\x64\x8c\x43\x87\x0e\x31\
+\x3c\x3c\x4c\x50\xe5\x92\x4b\x3e\xbe\x6a\xd3\xe0\xe0\x73\x87\x0e\
+\x1f\xbc\xfe\xca\x5d\xbb\x6e\xbf\xaf\xaf\xef\xc7\xfa\xc2\xde\xcb\
+\xba\xd6\xac\x76\x6e\xf5\xa9\x84\x63\xd3\xe8\x5c\x1d\x1a\x0d\x88\
+\x63\xa4\x58\xc0\x74\x77\x51\x9f\x9a\x62\xf6\xd5\xe1\xe4\x95\x95\
+\xab\x6e\xbf\xfe\xc0\x81\x03\x40\x59\x44\x12\x77\xd5\x55\x57\x2d\
+\x2b\x94\xe2\x0b\xd7\xaf\xdf\xd0\xbe\xfe\x97\xd6\xb3\xf5\xfc\x0b\
+\x5a\x02\x8a\x2c\x16\xba\x49\x19\xad\xf9\xe6\xce\x70\x41\x5e\x2b\
+\x0a\x57\x7c\x7a\xc1\x39\xa9\x2e\x38\x24\xcb\x96\x0e\xf4\x0f\x9c\
+\x72\xef\xbd\xff\xf6\xbb\xc0\xed\x23\x37\x7f\xf1\xd5\xd9\xff\xda\
+\xfd\xe0\xc0\x77\xbf\xb7\xad\xd0\xde\x16\x95\x3a\x3b\xad\x5d\xda\
+\x83\x29\x15\x09\x95\x2a\xc9\xcc\x0c\xb5\x7d\xc3\xbe\xee\x7d\xf5\
+\xd5\x6b\x3f\xff\x93\x89\xf5\xeb\xc6\xb9\xf6\x0f\xe6\x91\x55\x55\
+\x67\xa6\xa6\x26\x0f\x5f\x73\xed\xef\x2d\xb7\xce\x9d\x24\x20\xbf\
+\xbb\x2b\x69\x24\x85\xe0\xfd\xc3\x40\xf5\xb5\xfd\xfb\x77\xca\x79\
+\x5b\xdc\xf3\x67\xfe\xf2\xd8\xb2\xfb\x77\x6c\xec\xd8\x37\xbc\xa2\
+\xf8\xf2\xbe\xa5\xb6\x52\x29\xa5\x9d\x1d\x95\xca\x29\xcb\xc7\x66\
+\x87\x36\x1f\x1c\xbd\xf4\x63\x7b\x4d\xa9\x78\x6c\xef\xb3\x3f\x7f\
+\x24\x2f\x72\xaa\xda\xac\x85\x2e\xbc\xf0\x42\xd7\xdd\xdd\xdd\xce\
+\xff\xe2\x75\xf7\xdd\x77\x4f\x89\x88\x05\xda\x2f\xbe\xf8\xe2\xbe\
+\x73\x36\x9d\xb3\xa1\xbd\xbd\xdc\xd7\x56\x2e\x2e\x8d\x8b\xa5\xde\
+\x38\x8e\xda\x1a\xf5\x64\xba\x3e\x57\x1d\xaf\x54\xe7\x8e\xce\x55\
+\x6a\x47\x76\xee\x7c\x64\xcf\x63\x8f\x3d\x36\x01\x54\x34\x3f\x5f\
+\x97\xff\x2b\xff\xad\x22\x22\x06\x88\xf2\xbc\x6c\xf2\x4d\xc8\xb6\
+\x71\xa4\x40\xda\x14\x7a\xd1\x73\x1f\xf4\x7f\xb7\xf9\x1f\xc2\x26\
+\x56\xd5\x70\x45\xfc\x8a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\
+\x60\x82\
+\x00\x00\x0b\xd7\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\
+\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
+\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0e\x74\x45\
+\x58\x74\x54\x69\x74\x6c\x65\x00\x43\x6f\x6d\x70\x75\x74\x65\x72\
+\xf8\x18\x12\x76\x00\x00\x00\x17\x74\x45\x58\x74\x41\x75\x74\x68\
+\x6f\x72\x00\x4c\x61\x70\x6f\x20\x43\x61\x6c\x61\x6d\x61\x6e\x64\
+\x72\x65\x69\xdf\x91\x1a\x2a\x00\x00\x0b\x17\x49\x44\x41\x54\x68\
+\xde\xe5\x9a\x6b\x6c\x1c\xd7\x75\xc7\x7f\xe7\xde\x3b\xb3\x0f\x52\
+\x24\x45\x9a\x54\x15\xc3\x7a\x50\xf1\x0b\x6e\xed\x24\xaa\x25\x51\
+\xa9\x95\x5a\x40\x12\xd4\x71\x81\x02\x89\x03\xa3\xaf\x00\x4e\xd0\
+\xda\x75\x61\x03\x82\xd3\xd8\x7d\xa0\x68\xd3\x7e\x28\xea\x18\x0d\
+\x8a\x3e\xd2\xc4\xf5\x97\xc4\x4e\xea\x0f\x8a\x8d\x54\x34\x1a\x19\
+\x74\x5b\xc4\x70\x22\xb7\x31\x1a\xc7\x52\x52\x47\xa1\x64\x5a\x12\
+\xc5\xa7\x44\x72\x97\xbb\x33\xf7\x9e\x7e\xb8\xb3\x4b\x52\x92\xf5\
+\xa2\x10\xc1\xe8\x00\xc3\xb9\x3b\x3b\x3b\x7b\xfe\xf7\x3c\x7e\xe7\
+\xce\xd2\xa9\x2a\xef\xe6\xcd\xbd\xab\xad\xff\x7f\x2f\x60\xf8\xdb\
+\xcf\xff\x2d\x2a\x0f\xa2\x48\xd0\x40\x08\x81\x10\x14\x5d\x3e\xf6\
+\x81\x40\xc0\x7b\x45\x43\x20\xa8\x8f\xe7\x43\x71\x8d\x2a\xde\xfb\
+\xe2\xb5\x12\x34\x10\xbc\x6f\x8f\xe3\x7b\xc5\xf9\x10\xd0\xa0\x78\
+\x1f\x82\x48\xf8\xec\x9f\xfc\xd1\x9f\x3f\x71\xd9\x02\x86\x87\x87\
+\xbb\x30\xfc\xee\x07\x77\xee\x12\x63\x0c\x22\x06\x11\x41\x10\x44\
+\x80\x62\x0c\x8a\x6a\xdc\xa3\xc1\x85\xb0\xe0\xf1\x2d\x11\xc1\xe3\
+\x7d\x3c\xe6\xde\x13\x7c\x4e\xee\x3d\x3e\x2f\x8e\xc5\x6b\x0d\x01\
+\x55\xa5\x5e\xaf\x9b\x91\x97\x46\x3e\x0f\x5c\xbe\x80\x46\xa3\x61\
+\x4a\x55\x1b\x16\x6a\x0d\x6e\xf9\xd8\xe7\x08\xab\xae\x05\x8a\xc6\
+\xc3\x79\xb7\xce\x8a\x63\xef\x17\x3f\x4d\xf0\x6a\x56\x1d\x42\xed\
+\x30\x41\x19\xd8\xb2\x0d\x05\xb4\xb0\x42\x95\x38\xf3\xad\x73\xcb\
+\xc6\x1b\xd7\x77\x83\x82\xf7\x9e\x3c\x28\xcd\xcc\x73\x72\x66\x01\
+\x55\xe8\xa8\xa4\xdc\xb0\xa1\x97\x50\xcc\x48\x0c\xa5\xb8\x67\x59\
+\xc6\xff\x7c\xef\xdf\x49\x92\x84\xa0\x81\x2b\x23\x40\x15\x30\xa8\
+\xd8\x15\x33\xa9\xc5\x48\xcf\x38\xe7\x9c\xc1\x5a\x47\xee\x03\x2a\
+\xa0\xa2\x2c\x34\x32\x02\x16\x15\x58\xd7\xb7\x86\x10\x84\x50\x88\
+\x0d\x45\x18\xc6\xdd\x00\x90\x24\x29\x21\x5c\x11\x01\x31\x96\x61\
+\xe5\x6c\xb7\xc6\x68\xe1\x89\xd6\xfb\x0a\x69\xd9\x90\xfb\xe8\xb9\
+\x3c\x04\x72\xaf\xcc\xd7\x33\x54\x95\x72\xea\x48\x13\xcb\x62\xe6\
+\x41\x21\x14\x9e\x04\x25\xc4\xe4\x05\x20\xbd\x32\x02\x66\xc9\xf3\
+\x0e\x9a\x59\xd6\xfe\x82\x25\x11\xd1\xea\xe5\x86\x6b\x71\x22\x35\
+\x26\x86\x8e\x8f\x02\x16\xea\x4d\x72\x1f\x00\xa5\xb7\xab\x4c\xa3\
+\xe9\xa3\xe7\x74\x29\x1f\x82\x46\x5f\xfa\xdc\x17\x1e\x48\xae\x8c\
+\x07\xbc\xcf\xc9\x9a\x19\xa8\xe2\x7d\xde\x36\x72\xc9\x13\xac\x30\
+\x46\x51\x8c\x51\x9a\x59\x86\xf7\xe0\x15\xe6\x6b\x0d\x34\x28\x69\
+\x6a\xb1\xd6\xd0\xcc\x63\xd9\x6c\xdf\xa3\xf8\x7c\x50\x45\xfd\x15\
+\x14\x30\x0b\xd8\x46\x83\xb9\xf9\xd3\x68\x9e\xe1\xb3\xc5\x22\x94\
+\x96\xbe\x18\xa4\x7d\x7d\x51\x59\x11\x72\x7c\xa6\x84\x00\x8d\x3c\
+\x90\xe7\x39\x22\x96\xae\x6a\x89\x2c\xf7\xa8\x4a\x7b\xc6\x51\xda\
+\xc9\xac\x45\xd2\x03\x38\xe7\xda\xa1\xeb\x56\x11\x41\x2c\x64\xf3\
+\x32\x31\x71\x12\xcd\x17\x09\xcd\xf9\xc2\x50\x83\x08\x98\x08\x83\
+\x25\xeb\x81\xc4\x5a\xb4\xb1\x80\x2f\x92\x33\x6b\x78\x12\x31\xb8\
+\xd4\x52\x4e\x1d\x8a\xd0\xca\xd9\xa0\x10\x7c\x01\x2e\x0d\xf8\xb0\
+\xe4\x01\x6b\x1d\xbe\xe5\x81\xcb\xa5\xa9\x38\xa5\x6c\xca\x8c\x9f\
+\x78\x8b\x2f\x3c\xf2\x91\x4b\xa0\x69\x1d\x0d\x4a\xee\x03\x07\x46\
+\x95\x1f\x8c\x57\xe8\x2c\x57\xc1\x28\x8a\xa5\xed\x38\x0d\x50\x54\
+\xa9\x56\x1e\x15\x29\x80\x35\x86\xe0\xb5\xf0\x40\xb8\x6a\x34\x25\
+\xf3\x23\xfc\xe8\xa4\xc7\x19\x40\x0d\xc6\x18\x54\x04\x89\xbe\xc0\
+\x20\x04\x42\x91\x13\x8a\x4a\xcb\xa3\x82\xb6\x39\x20\x72\xd5\x68\
+\xaa\x5e\x49\x5c\xfc\x80\x18\x83\x88\x25\x88\x80\x2a\x22\x85\xf1\
+\xad\x10\x34\xbe\x1d\x8b\x71\xa2\x0a\x01\x57\x9b\xa6\x06\x10\x11\
+\x8c\x18\xd4\x44\x18\x8a\x01\x54\xe2\x8c\xb7\x66\x44\x4d\xe1\x13\
+\xf0\x21\x3f\x43\xc0\x55\xa4\xa9\x4d\x12\x9c\xb3\x18\x67\xc1\x38\
+\x02\xd2\x06\x56\xdb\x91\x01\x54\x04\x13\xdb\x1f\xf2\x6c\x85\x80\
+\xab\x47\x53\xd5\x40\xe2\x2c\x2e\x49\x70\x2e\x45\xad\xc3\x2b\x31\
+\x17\x8d\x20\x26\x20\xe2\x11\x2f\x80\x87\x10\x27\x38\xcb\xb2\x25\
+\x01\xde\x07\xf2\xa2\x3c\x85\x15\x21\x73\x1e\x9a\x5a\x4b\xfc\x9c\
+\x92\x07\xbd\x6c\x9a\xe6\x79\xce\x8f\x5e\x7d\x11\x93\x54\x91\xa4\
+\x8c\x18\x57\xc4\x4f\x2b\x10\x74\x45\x48\x8b\xc4\xdc\xc8\xf2\x66\
+\x3b\x3c\x5d\x08\x9e\x50\xcc\x4a\x2b\x17\xce\x65\xf8\x72\x9a\x5a\
+\x6b\xc8\x7c\x2c\xab\x3e\x28\xf3\xf5\x26\xaa\x4a\x9a\x5c\x1a\x4d\
+\x8d\xc0\x17\x1f\xfb\x24\xbd\x7d\xbd\x74\x77\xf7\x50\x2a\xa5\x18\
+\x63\xda\x65\xd8\x87\x80\xf7\xa1\xa8\x76\x71\xa7\xc8\xa3\x15\x39\
+\x10\xdf\xa0\xc8\x85\x33\x66\x3c\x32\x34\x52\x54\x04\x44\x71\x89\
+\x6d\xd7\xe1\xa6\xf7\xf8\x10\xdd\xde\xd5\x51\x22\xcb\xc3\x92\xb1\
+\x17\xa0\x29\xaa\x74\xaf\xe9\xa4\xb7\xa7\x8b\xb5\x6b\xbb\xa9\x54\
+\x2a\x18\x13\x4b\xa4\x0f\xb1\x64\xc7\x10\x0e\xfc\xf0\x8d\x83\xbc\
+\xfa\xea\xab\xec\xde\xbd\x9b\x7a\xad\x46\x28\x6e\xea\x62\x7d\xce\
+\x96\x90\x2f\x60\xc4\x42\xcb\x60\x96\x6a\x2f\x28\x69\xe2\xb0\xc6\
+\xc6\xa4\x16\xc5\x37\x72\xd2\x24\xc5\x5a\xa1\x52\x4a\x8b\x49\xd0\
+\x8b\xa2\x69\x9e\xe7\x3c\xf7\xdc\x5e\xaa\x9d\x1d\x54\xaa\x15\x12\
+\xe7\xea\x91\x35\x11\x7a\xcb\xc1\x58\x5b\x58\x48\x2b\x95\xaa\x7c\
+\xe3\xeb\xcf\x04\x44\x02\xca\xdf\x45\x01\x41\xf1\x79\xde\x76\x6b\
+\xec\x57\x0c\x18\x96\xa0\x46\x9c\x79\x10\xca\xa5\x04\x63\x6d\xe1\
+\x98\x80\x18\x4b\xa9\x64\xe8\x28\x27\x88\x8d\x15\x83\xa0\xf1\xfa\
+\x0b\xd0\x54\x15\x6e\x7d\xdf\xad\x0c\xac\xfb\x39\x8e\x1c\x19\x6d\
+\xbc\xf1\x83\x43\x37\x56\xab\x55\x5f\x2e\x97\x03\xd4\xa8\x01\xf1\
+\x0f\x80\xa1\x36\xbf\x08\xd4\x69\x34\x5c\xfe\xf4\xd3\x4f\x4f\x02\
+\x38\x0d\x31\x89\x15\x48\x9c\x5d\xa2\xb1\x91\xb6\x00\xa4\x20\xb3\
+\x0a\x95\x52\x82\xb1\x82\x18\xc8\x9b\x9e\x24\x49\xb1\x02\x69\xea\
+\x08\x41\x31\x26\xe2\x47\x8a\xe2\x79\x3e\x9a\x82\xf2\x0b\xb7\xdc\
+\xc6\x86\x0d\x1b\x19\xe8\xef\xd3\x24\x71\x3b\x1f\xfb\x83\x3f\xfe\
+\x57\x60\x11\xf0\x7a\x11\x0f\xad\x5c\x8c\xb3\x78\x3f\xe7\x1c\xc6\
+\x14\x25\x4c\x4c\x11\xf7\xa6\x68\xcc\xa2\x47\xd2\x34\x45\xd0\x48\
+\x6d\x0b\xa5\x54\x70\xce\x14\xf7\x50\x8c\x17\xd4\x28\xa8\x5c\x90\
+\xa6\xde\x7b\xde\x7c\xf3\x27\x2c\xd4\x6a\x74\x74\x76\x94\xdf\xb3\
+\xfe\x3d\x5f\xfb\xd2\x3f\xfd\xfd\x68\xb9\x5c\x3a\xd8\xdd\xdb\xe3\
+\x5e\xf8\xb7\x6f\x75\x18\x63\x12\x11\x6c\x88\x8d\x67\x8e\xea\x42\
+\xd0\xf0\x1f\x9a\x2d\xfe\xc3\x5d\x77\xdd\x73\xc2\x85\x10\x68\xe9\
+\x74\x89\xc3\x14\x06\x8b\x2d\x8e\x62\x30\xa6\x08\x24\x23\xb8\xc4\
+\xb5\xea\x2d\x69\x22\xf1\xb6\x05\x65\x51\xc5\xab\x81\xe0\x11\x23\
+\x17\x45\xd3\xb4\x6c\x11\xe3\x49\x4b\x96\x6d\xdb\xb6\xd9\xe0\x19\
+\x7c\xf2\x9f\xbf\x92\x04\xe5\xfe\x0d\xd7\xae\x9f\xea\xef\xef\xcf\
+\x93\x24\xb5\x31\xea\x8c\x33\xa9\x5d\x5b\xad\x54\xee\xb5\x69\xf5\
+\x69\x60\x77\x14\x50\x14\x6b\x6b\x2c\xc6\x18\xac\x35\x85\xe1\x06\
+\xdb\xf2\x86\x89\x42\xac\x35\x60\x81\x10\x28\x9b\x22\xe4\x8b\xee\
+\xd5\xfb\x80\x18\x45\xbc\x69\x57\x9b\xf3\xd1\x34\xcf\x73\xbe\xf9\
+\xcd\xe7\xf2\x24\x49\xd4\xda\xf8\xdd\x20\x66\x6e\x6e\xee\xc0\xf7\
+\x5e\x39\xf0\xd6\xd0\xd0\xd0\xdc\x67\xee\xff\xd4\x9f\x3a\x9b\xfe\
+\xb2\x20\x15\xd0\xb2\x0a\x3e\x84\x90\x18\x23\xdf\x12\x11\x71\xad\
+\x52\x15\x2b\x83\xc1\x2c\x13\x61\x8d\xc1\x58\x83\x29\x04\x38\x2b\
+\x45\xec\x0a\x56\xa5\x30\x6e\x89\x07\xd6\x04\xf2\xe0\x23\x3d\x5b\
+\x79\x74\x7e\x9a\x36\x7f\x7c\xe8\xcd\xa1\xe5\xdd\xa0\xf7\xde\xcf\
+\xcc\xcc\x9c\x4e\x92\x64\xf1\xb7\x3e\xf5\xeb\x0f\x19\xcc\xfa\x89\
+\x13\xe3\x9f\x9c\x9a\x3a\x7d\x6a\xb2\x36\x59\xef\x72\x89\x5d\xbb\
+\xb6\xbb\xf1\xd2\x4b\x07\x26\xda\x49\x8c\x2a\x8d\x46\x83\xff\x3d\
+\xf0\x02\x2d\xb3\xda\xed\x4b\xbb\x3b\x5a\xb9\xc9\x3b\x9c\x3f\xbb\
+\x41\x7d\x67\x9a\xaa\x92\xde\xbe\x63\xdb\x7f\xad\x5b\x37\x40\xc8\
+\xf3\x46\xa5\xb3\xa3\x34\x3d\x3d\xc3\x35\xbd\xbd\x64\xbe\xb9\xf8\
+\xc6\xa1\x43\xa5\xd9\xa9\x53\xda\xd3\xd7\xfd\xe1\x46\x7d\xb1\x99\
+\x24\xf6\xe5\xef\x7c\xf7\xbf\x3f\x36\x3c\x3c\x9c\x47\xb8\xab\xba\
+\x10\x02\xd5\x6a\xc2\x0b\x5f\x7a\x80\x96\x1b\x63\x28\xc5\xf1\xf2\
+\x5d\xa4\x55\x99\x5a\xa4\x2e\xea\x74\xb1\x1e\x88\x63\x4f\x0b\x8e\
+\x17\x43\xd3\xfe\x6b\x7a\x69\x36\x1a\x6c\xdd\xba\xd5\xfd\xe4\xf0\
+\x9b\x3a\xd0\xd7\x07\x46\xb8\x69\xcb\xcd\xf6\xf0\x4f\x0f\xcb\xe6\
+\x2d\x1b\x65\x72\x72\x8a\xcd\x9b\x37\x95\xc7\x4f\x9c\xdc\x78\xcb\
+\xad\x37\xff\xf6\xbe\x7d\xfb\xbe\xdc\xae\x42\x79\x96\x1b\x55\x65\
+\xa0\xaf\x37\x86\x8b\x11\xac\x58\xc4\x5a\x8c\x48\xdb\xf0\x73\x0b\
+\x68\x2d\x72\x96\x56\x70\xf1\xb5\x8f\x0b\x9c\x0b\xd0\x54\x55\x31\
+\x22\x6c\xda\xb4\x91\xda\xc2\xbc\x7d\xff\x6d\xef\xe3\xc8\xd1\xa3\
+\x0c\xac\x1b\xa0\x52\xa9\x24\x1b\x36\x5c\xc7\xdc\xa9\xd3\x0c\x6e\
+\xde\xc4\xd4\xd4\x94\xdc\x71\xc7\x1d\x95\xe7\x9f\x7f\xee\xcf\xf6\
+\xec\xd9\xf3\xd5\x27\x9e\x78\xa2\x5e\xac\x89\xe5\xf9\xfd\x2f\xbe\
+\x78\x37\x68\x60\x05\x05\x97\x1b\x17\xb1\x1a\x08\xe4\x3e\xaf\xc4\
+\xf6\x20\x14\xcb\xc4\xd6\x32\x53\xe3\xac\x87\x62\xf9\x78\x0e\x9a\
+\xd6\x6b\x35\x2a\x95\x2a\x5f\x7f\xe6\x19\xc4\x08\x79\xee\x99\x9e\
+\x9e\x66\x61\xa1\xc6\xce\xa1\x9d\xcc\xcd\x2f\x70\xfc\xd8\x31\x66\
+\x66\xa6\xb9\xf1\x86\x9b\x98\x18\x9f\xe0\xf8\xf1\xe3\x8c\x8e\x8e\
+\x12\x54\xf9\xe8\x47\xef\xba\x76\x68\xe7\x07\x5f\x1f\x19\xd9\xff\
+\x10\xf0\x57\x00\xee\xf7\x1f\x7c\xf8\x13\x8f\x3f\xfe\xf8\x80\x6a\
+\x51\x1e\x5a\x04\x5c\x46\xc1\x66\xb3\x29\x79\x7e\x5a\x4e\x2d\x66\
+\x1d\x9b\xae\xdb\x70\x70\xe7\x8e\x21\x5b\xab\xd7\xda\x59\x20\x22\
+\x24\x69\x99\xef\xbe\xf2\x0a\x03\x7d\xeb\xe8\xea\xea\xa6\xab\x6b\
+\x0d\xe5\x72\x19\x97\x26\x18\x0a\xcf\x89\x81\x62\xc1\x6f\xad\xa3\
+\xa7\xa7\x87\x72\xb9\x4c\xb5\x5a\xa5\x54\x2a\x15\x05\x43\x96\xad\
+\xea\x74\x59\x5b\x1f\x13\x6f\x70\xf3\xe0\xfa\xe1\xe1\x7d\xf7\xb7\
+\x05\x00\x3c\xf2\xc8\x23\x27\xdf\x29\x07\x25\x36\x44\x0e\xa8\x7c\
+\xe1\x6f\xfe\xfa\x57\x87\xb6\xef\x68\xde\xfe\x8b\xdb\x2b\x8d\x46\
+\x63\xc5\x75\xa5\x52\x99\x90\x67\x7c\xe3\x5f\x9e\x5d\x9c\x9c\x9c\
+\xf4\xa5\x52\x29\x24\x2e\x29\x4a\x23\x88\xc8\x39\xf2\x7d\xa9\x49\
+\x14\x23\x17\xb5\x68\x6d\x36\x9a\xa5\x3c\xcb\x47\xda\x39\x20\x2b\
+\x3a\xb6\xb3\x36\x03\xa4\x3b\x76\xed\xd8\x78\xef\xc7\x3f\xfe\xf9\
+\xbe\xfe\x6b\xee\x36\xe2\x4a\xdf\x7f\xed\x35\x9a\xcd\xe6\x19\x02\
+\x4a\x54\xab\x5d\x7c\x68\xd7\xae\xa4\xb6\xb0\x70\x3c\x49\x92\x1f\
+\x77\xad\xed\x71\xe5\x52\x5a\x35\xc6\x24\xc6\x88\xd1\x82\xa6\xea\
+\xb5\xd6\xc8\x1a\x2f\xbf\x7d\xe4\xe8\x93\xfb\xf7\xff\xe7\xc9\x4b\
+\x5d\x79\xef\xdd\xbb\x77\xb6\x2d\x00\x30\xf7\xdc\x73\x8f\x1d\x1b\
+\x1b\xb3\x2b\x94\x36\x9b\x32\x31\x31\x51\x9a\x9d\x9d\xed\x1b\xba\
+\x7d\xfb\xbe\xbb\xef\xfe\xb5\x8d\x2a\x41\x4e\x9d\x9a\x01\x02\x65\
+\x77\xe6\x23\x25\x4f\xc9\x5a\x86\x86\x86\x6c\xb3\x91\x6f\xfa\xca\
+\x93\x5f\xb6\x17\xa2\xe9\x7b\x6f\xbc\xf1\x1f\x1f\x7c\x70\xcf\xee\
+\xd5\x3c\x46\x70\x5b\xb7\x6e\x35\x87\x0f\x1f\x76\xe5\x72\xd9\x9e\
+\x39\xfb\x22\x52\x36\xc6\x74\x1e\x3a\x78\xe8\xf5\xc7\xfe\xf0\xd1\
+\xeb\x8a\x5e\xf3\xc2\xcf\x26\x94\x4b\xa2\xa9\xae\xe2\x97\x46\x37\
+\x38\x38\x18\xc6\xc6\xc6\x74\x76\x76\x76\xc5\x4d\xf2\x3c\x0f\xde\
+\xfb\x86\x73\xee\xe4\xcb\xdf\x79\xe5\xa1\xde\xde\xde\x35\xd6\x5a\
+\x7b\x9e\x70\xbb\x6c\x9a\xae\xca\x03\xcf\x3e\xfb\x6c\x00\x9a\x8f\
+\x3e\xfa\xe8\x66\xeb\xd8\x3f\x37\x3f\xbf\xbe\xde\x68\xa6\x7d\x7d\
+\xbd\x4c\x4d\x4d\x73\x29\xc7\x46\xbd\xde\x28\x55\x2a\xa5\xa9\xa9\
+\xe9\x4b\xa2\xe9\xaa\x04\x14\x37\xf0\x9f\xfd\xdc\x9e\xbf\xbc\xee\
+\xda\xc1\xd1\xf1\x93\xe3\x1b\x36\xf5\xf4\x30\x39\x39\xc5\xcd\x37\
+\xdd\x70\x49\xc7\x6d\xdb\xb6\xbb\x1f\x1e\x7c\xfd\xfc\x34\x1d\x1f\
+\x3f\x8b\xa6\xab\x12\x00\xf0\xf0\xc3\x0f\xfc\x7c\xb9\xd2\xb9\x7b\
+\xcb\xe0\x96\x6a\xa9\x94\xca\xcc\xf4\x4c\x8b\x7e\x74\xad\xe9\x24\
+\x6b\x36\xcf\x79\xf4\x59\xce\xe0\xe6\x4d\xb4\xae\x9f\x9f\x3b\x6d\
+\x3f\x70\xdb\xfb\xf9\xe9\xe8\x28\xfd\x03\xfd\xe7\xa6\xe9\x2f\xed\
+\x3a\x8b\xa6\xab\x16\xd0\x68\xe6\x7f\xf1\xe9\xcf\xfc\xe6\xd1\x63\
+\xc7\x8f\x7d\xe0\xb5\xef\xbf\x46\x20\x30\x7a\xf4\x48\x5c\x45\x22\
+\x68\xf1\xac\x92\xe2\xe9\xb1\x2c\x6b\xe7\xc6\xc6\xc6\x50\x55\xde\
+\x7a\x6b\x8c\xed\xdb\xb6\x73\x7a\x6e\x9e\x63\x6f\xbf\xcd\xf4\xf4\
+\xd4\x3b\xd0\xf4\x57\xce\xa2\xe9\xaa\x04\xdc\x77\xdf\x7d\xfd\xa5\
+\x4a\x7a\xe7\xf5\xd7\xdf\xd0\x79\xfd\x7b\xaf\x67\xd7\x1d\x1f\x6a\
+\x1b\x18\x17\x62\x4b\x46\xb7\x90\xd1\x3e\xdf\x5a\x19\x2e\xcb\x6b\
+\x45\xe1\xde\xdf\x60\xe9\x39\xe9\xd9\x34\xdd\xb2\x79\xcb\x0a\x9a\
+\xae\x36\x07\xe6\x66\x67\x67\x4e\x3c\xf0\x7b\xbf\xb3\xce\x3a\x27\
+\xfc\x0c\xb6\xac\x99\x95\xb2\x66\x36\x72\x45\x72\xe0\xa9\xa7\x9e\
+\x5a\xbc\xf3\xce\x3b\x6f\xe9\xe9\xe9\xe9\xe4\x67\xb8\x2d\xa7\xe9\
+\xaa\x73\x60\x64\x64\x24\x27\xfe\x6a\xf4\xae\xdb\xde\xf5\xff\xec\
+\xf1\x7f\x9d\x3d\x46\xc4\x32\x49\xfc\x0b\x00\x00\x00\x00\x49\x45\
+\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x06\xe7\
+\xff\
+\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\
+\x48\x00\x00\xff\xdb\x00\x43\x00\x02\x01\x01\x01\x01\x01\x02\x01\
+\x01\x01\x02\x02\x02\x02\x02\x04\x03\x02\x02\x02\x02\x05\x04\x04\
+\x03\x04\x06\x05\x06\x06\x06\x05\x06\x06\x06\x07\x09\x08\x06\x07\
+\x09\x07\x06\x06\x08\x0b\x08\x09\x0a\x0a\x0a\x0a\x0a\x06\x08\x0b\
+\x0c\x0b\x0a\x0c\x09\x0a\x0a\x0a\xff\xdb\x00\x43\x01\x02\x02\x02\
+\x02\x02\x02\x05\x03\x03\x05\x0a\x07\x06\x07\x0a\x0a\x0a\x0a\x0a\
+\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\
+\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\
+\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\xff\xc0\x00\
+\x11\x08\x00\x30\x00\x30\x03\x01\x22\x00\x02\x11\x01\x03\x11\x01\
+\xff\xc4\x00\x1c\x00\x00\x02\x02\x02\x03\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x07\x08\x06\x09\x00\x05\x02\x03\x04\xff\xc4\
+\x00\x30\x10\x00\x01\x03\x03\x03\x03\x02\x05\x04\x02\x03\x00\x00\
+\x00\x00\x00\x01\x02\x03\x04\x05\x06\x11\x00\x07\x12\x08\x21\x31\
+\x09\x13\x14\x22\x41\x51\x71\x23\x42\x61\x81\x15\x32\x43\x92\xa1\
+\xff\xc4\x00\x19\x01\x00\x02\x03\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x05\x06\x02\x03\x04\x00\xff\xc4\x00\x2f\x11\
+\x00\x01\x02\x04\x04\x03\x06\x07\x01\x00\x00\x00\x00\x00\x00\x00\
+\x01\x02\x11\x00\x03\x04\x05\x12\x21\x31\x41\x51\x61\x91\x06\x13\
+\x14\x22\x81\xb1\x23\x32\x42\x71\x82\xa1\xc1\xd1\xff\xda\x00\x0c\
+\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xbf\x0d\x66\xb5\xf7\x25\
+\xd9\x6c\x59\xf0\x17\x54\xba\xee\x18\x54\xd8\xed\xb6\xa7\x16\xec\
+\xd9\x29\x6c\x04\xa4\x65\x47\xe6\x3d\xf1\xfc\x69\x20\xea\x83\xd6\
+\x4a\xc8\xb6\xab\x2f\xd8\xbb\x2b\x12\x4b\xfc\x1c\x53\x4e\xd7\xdd\
+\x6c\x21\x2a\x23\xea\xd0\x57\x84\xe7\xf7\x11\x93\xfc\x76\xd6\x0a\
+\xeb\x95\x1d\xbd\x18\xa7\x2d\xb9\x6e\x62\x2a\x5a\x51\xa9\x87\xb9\
+\xc7\x10\xd0\x25\xd5\xa5\x20\x0c\x9e\x4a\xc6\x07\xf7\xa8\x95\xf7\
+\xbf\x1b\x3d\xb6\x8b\x0c\x5e\xbb\x87\x4d\x86\xfa\x81\x29\x88\x1e\
+\xf7\x5f\x57\xe1\xb6\xc2\x95\xff\x00\x9a\xa7\xcb\xf3\xd4\x1a\xea\
+\xac\xbd\x29\xfa\xac\xd6\x3d\xd7\x1c\x2a\x5b\xb2\x6a\x0b\x5a\x97\
+\xf9\xe7\xe4\xff\x00\x1a\x14\x5d\xdd\x64\x5e\x95\x52\xd2\xa9\xf5\
+\xc6\xd8\xf7\xd6\x0c\x62\xd2\xb8\xe5\x49\xc1\x1c\x70\x06\x49\xed\
+\xdb\xb9\x3e\x34\xad\x51\xdb\x4a\x69\x63\xc8\x8c\xf6\xcd\xff\x00\
+\x59\x7b\xc5\x7d\xeb\xfc\xa1\xe2\xd3\x7a\xb3\xf5\x08\xad\x5b\xd6\
+\xfc\x78\xdd\x3e\xd4\xe9\x54\xc7\x94\xf7\x29\x75\xbb\xad\xa0\xda\
+\x3d\xb1\xe1\x2c\xb6\xb3\xdc\x93\xe5\x4b\xf1\x8f\x1d\xf3\xae\xcd\
+\xbd\xf5\x5c\xd9\x3b\x5e\xd5\xa4\x52\x3a\x97\xbb\x21\x52\xee\x17\
+\x92\x94\xca\x9f\x4e\x4a\x3e\x0d\xf0\x55\x84\xbc\x94\xf3\xe6\x91\
+\x82\x09\x00\x11\x9c\x91\xdb\x03\x55\x2f\x71\xc9\xde\x4d\xc7\xa8\
+\xb7\x3e\xb9\x74\xd6\x22\x30\xcc\x30\x65\xdd\x14\xf9\xb1\xd9\x11\
+\xc2\xdc\x07\xe1\x13\x2a\x53\x6a\x69\xa5\x29\x20\x85\x06\x52\xb7\
+\x4a\x4f\xcb\xe0\xe4\xf3\xd0\xaf\xa3\xa6\xe2\x75\x1d\x29\x8b\xef\
+\x70\xea\x4d\x52\xec\xb1\x39\x4a\xf8\xf7\x1b\x77\xdf\x9a\x80\x73\
+\xed\x44\x43\xa0\x38\x1b\x19\xe3\xee\x2c\x36\x0f\x9e\x2a\xc6\x34\
+\x36\x4f\x69\x2f\x35\x15\xd8\x29\xa5\x95\xad\x5f\x4b\xf9\x40\xe7\
+\x93\x0e\xbf\xe4\x14\x97\x40\xaf\x0e\x99\xeb\x53\x02\x73\x1a\x30\
+\xfe\xbf\x00\x3f\xb1\xb8\xea\x7a\xbb\xbf\x1b\xc1\x6b\x5b\x95\x6b\
+\x7f\x76\xe5\xb9\x2a\xd9\x84\xf4\x09\xb6\xf5\x51\xd2\xeb\x33\xff\
+\x00\xe7\x69\x21\x5f\xec\x1c\x50\xe4\x39\x12\x42\xb8\xa1\x3f\x6c\
+\xa8\x15\x2b\xee\x9f\xb8\x55\xa4\x3b\x31\x4a\x8c\xb0\xe1\xf8\x86\
+\x56\x3f\x77\xd4\x24\x9f\xc6\x30\x71\xa6\x87\xaa\x9a\x8d\xcf\xb0\
+\x5b\xbc\x29\xb2\xdb\x4f\xb2\x92\xb8\x93\xd8\x43\xb8\x38\x8e\xe0\
+\x53\x6b\xc1\xf0\xa4\x82\xd9\x4a\xbc\x10\xac\x13\xe3\x48\xf5\xeb\
+\x7c\x6d\x72\xfa\xac\xaa\xd6\xe4\xc1\xac\xc4\xb6\xab\xce\x09\x60\
+\xc5\x8a\x4a\x5a\x25\x64\x28\x14\x27\x3c\x7b\x83\x82\x3b\x11\xa0\
+\x77\x29\x0b\xab\xab\xee\xd2\x7e\x20\xd5\x24\xea\xcd\xd3\x81\x8c\
+\x52\xad\x93\xea\xea\x17\x29\x45\x8a\x77\xf6\xea\x22\x7b\x72\xd8\
+\x76\x04\x9b\x65\xa9\xb5\xfd\xc1\x94\xca\xde\x6d\x7c\x20\x53\x69\
+\xe6\x4c\xae\x41\x58\xc1\x2a\xe0\xd2\x33\xf4\xe4\xbf\x1d\xf1\xdf\
+\x1a\xe3\xb7\xf4\x86\xe5\xd9\xb3\x2f\x8a\x25\xa7\x06\x9b\x4e\xb3\
+\x96\x1c\x8f\x51\xbb\xa4\x99\xf2\x14\xbc\x10\x12\xdb\x49\x08\x8f\
+\xee\xf2\x50\x23\xe4\x50\x1e\x3b\xe3\x46\xab\x19\x1d\x11\x49\xa4\
+\x44\x90\xfd\xdf\x52\xa8\xad\x08\x0a\x54\x46\x29\x4f\x29\xc5\x2f\
+\xfe\xb8\x1a\xf6\x35\x1f\x6b\xf7\x9b\xa8\x0a\x46\xd6\x50\xe9\x12\
+\xa8\xd4\x08\xe8\x52\xe2\xd2\x26\x41\xc2\x25\xcc\x0d\x85\xa5\x6b\
+\x1c\xb0\xa2\x13\xcc\x84\x9f\x2a\xe3\x9c\xf8\xd0\xf9\x94\x53\x25\
+\x90\x7b\xb6\x52\xb2\xd7\x73\xa7\x43\x9e\x70\xc3\x41\x46\x8a\x2c\
+\x25\x69\x4b\xb8\xd4\x13\xea\x49\xd3\xf1\x68\xdf\xf4\x71\x4b\xe9\
+\xfe\xef\xbe\x2c\x7a\x67\x55\x57\x7d\x66\x9d\x54\xac\xa5\xc9\x21\
+\x75\x94\x2d\x70\x29\xe1\x43\x9b\x04\x0c\xa1\xa6\x3d\xd4\x0c\x8c\
+\x23\x8a\x72\x9c\x81\x91\xab\x72\xb6\xb7\xe3\xa5\x1b\x06\xc5\x85\
+\x12\x87\xbd\x36\x6d\x3a\x89\x09\xa0\xc4\x40\xbb\x81\x84\x24\x01\
+\xf4\xc2\x94\x14\x49\x39\x24\xe3\xb9\xc9\xd2\x6e\xdf\xa7\xcd\xb5\
+\x79\xd9\x53\xee\x2b\x32\xab\x2a\x45\xca\xe4\x15\x06\x9b\xad\x4b\
+\x49\x8f\x35\x58\x1c\x5b\x51\xe3\xfa\x40\x90\x9f\x98\x64\x0c\x0e\
+\xc4\x0d\x18\x76\xf3\xa3\xfb\xa3\x69\x2d\x8a\x5d\xe3\xb6\xb6\x95\
+\xa1\x2a\xec\xf6\x58\x15\x9a\x45\x79\x29\x54\x09\x25\x44\x7b\xc0\
+\x39\xed\x28\xb6\xb4\xe4\xf1\x71\xa4\xb6\x17\xc7\xe6\x49\xce\x9a\
+\xfb\x3f\x4b\x74\xb3\x95\x35\x30\x24\x80\x4a\xdc\x92\x47\x01\xc4\
+\x8d\x48\x0c\xfb\x3b\x08\xdf\x75\x16\xca\xac\x38\x26\x90\x37\x4b\
+\x04\x8c\x5b\x9e\x40\xe8\x1c\x16\x85\x03\xd6\x73\x64\xf7\x57\x73\
+\x2a\x48\xdc\xd9\x56\xaa\xad\x96\x5f\xae\xb3\x02\x90\x12\xe0\x2f\
+\xc7\x6d\x2a\x08\x72\x4b\xee\xa3\x92\x39\x3a\x8e\x4a\x0d\xa4\xa8\
+\x06\xd0\x33\x92\x70\x09\x9d\x1c\xfa\x4c\x52\xad\x3d\xbb\x60\xef\
+\x5c\x7a\x7d\xc5\x54\x94\x9f\x79\x75\x31\x11\x09\x4b\x8d\xa8\x65\
+\xb0\x94\x8f\x00\x20\x80\x3c\x67\xec\x3c\x69\xf0\xdc\x8d\x90\xb3\
+\x77\x46\xbf\x6b\xdc\x17\x1c\x8a\x93\x6b\xb4\xeb\x2a\xa9\xc0\x8b\
+\x06\x71\x69\x89\x2f\x14\x70\xe3\x21\xb0\x08\x79\x03\xcf\x13\x8e\
+\xe3\xf2\x35\x29\xf8\x06\x92\x9e\x0d\xa4\x00\x06\x00\xc6\x9a\x69\
+\x6c\x49\x95\x70\x9b\x51\x30\xe2\xc4\xcc\xfa\xf3\xe4\xcf\xa0\xe5\
+\x01\xd7\x73\x4f\x86\x4c\xb9\x61\x8e\xfe\xc0\x71\xd0\x3c\x28\x12\
+\xbd\x31\xb6\x26\x22\x84\x8a\x6d\xa4\xc4\x65\x0e\xe3\xd9\x4f\x1e\
+\xff\x00\xd1\xd2\x4d\xea\x0d\xb2\x75\xee\x98\xba\x95\xb2\xf7\x1e\
+\xd2\xa5\x3c\xe5\x3e\x2c\xc8\x4f\x70\x8c\x82\xb5\x3c\x86\x96\x0b\
+\x80\x24\x77\x2a\x09\x0b\x49\x03\x24\x82\x93\xf7\xd5\xc9\x3b\x4d\
+\x4a\xd4\x41\xc1\x1f\x8d\x0a\x7a\x87\xe9\x13\x6f\x7a\x8c\xa2\x7f\
+\x88\xba\xa4\x48\x61\x69\x1f\xa4\xfb\x00\x1e\x07\x39\x07\x07\xea\
+\x0f\x70\x46\x08\xfb\xea\xab\xcd\x91\x55\x74\x98\x69\xc0\x0b\x04\
+\x11\xb6\x86\x3a\x96\xb6\x52\x94\x53\x50\x4e\x13\xfa\x3b\x18\x89\
+\xec\x6e\xe3\x6d\xad\xd3\x75\x37\x64\x6d\xfd\x69\xda\xba\x85\x2c\
+\x4f\x7a\x6d\x3e\x32\x9c\x89\x15\xb5\x63\x83\x6e\xbc\x3e\x56\xdd\
+\x50\x3d\x9b\x3f\x37\x63\x9c\x63\x47\x8a\x6c\x15\x32\x12\x8c\x1e\
+\xe3\xeb\xa4\x8b\x6d\xba\x11\xb8\x3a\x74\xdf\x48\xb4\x49\xb7\x1d\
+\xf6\x29\xd7\x0a\xc3\x34\x3b\xdb\x6f\xa7\xa9\x85\xb0\xe0\xee\xa6\
+\x2a\x6c\xa8\x29\x21\x1c\x7b\xa5\xec\x29\x3d\x88\xec\x74\xdd\xed\
+\x3e\xce\x3b\xb6\x55\x29\xd3\x46\xe8\x5d\x95\xc6\x66\xb6\x90\x88\
+\x77\x15\x57\xe2\x91\x1d\x40\xe5\x4e\x20\xa8\x72\x0a\x57\xd7\xbe\
+\x3e\xc0\x6a\xfb\x54\xeb\x82\xc9\x44\xf9\x4c\xc4\x82\x41\x66\x1b\
+\x65\xa9\xfb\x8c\xb3\xca\x21\x5a\x8a\x60\x90\xa4\xad\xdc\x73\xcf\
+\x8e\x7b\x7a\xc7\xff\xd9\
+\x00\x00\x0c\x8d\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\
+\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
+\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
+\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0e\x74\x45\
+\x58\x74\x54\x69\x74\x6c\x65\x00\x43\x6f\x6d\x70\x75\x74\x65\x72\
+\xf8\x18\x12\x76\x00\x00\x00\x17\x74\x45\x58\x74\x41\x75\x74\x68\
+\x6f\x72\x00\x4c\x61\x70\x6f\x20\x43\x61\x6c\x61\x6d\x61\x6e\x64\
+\x72\x65\x69\xdf\x91\x1a\x2a\x00\x00\x0b\xcd\x49\x44\x41\x54\x68\
+\xde\xed\x9a\x7b\x70\x54\xd7\x7d\xc7\x3f\xbf\x73\xee\xdd\x97\x84\
+\x24\x24\x4b\x18\x1c\x40\x08\xe3\xc7\xb8\xe1\x61\x6c\x40\xb8\x60\
+\x43\x13\xc7\x75\xc8\xb4\x33\x29\x1d\xa7\xaf\x74\x9c\x4c\xeb\xd4\
+\x1d\x7b\x86\x71\x1a\xa7\x8f\xc9\xb4\x49\xa7\xd3\xa9\xeb\xc9\x4c\
+\xa7\x2f\x27\x2e\xff\xb4\xa4\x8d\xc7\x75\xec\xb1\x51\x1d\x03\xb2\
+\xe3\xc7\xe0\x40\x30\xf8\x05\x75\x1d\x22\x30\x46\x12\x48\x42\x20\
+\xb4\xd2\xee\xde\x73\x7e\xfd\xe3\xdc\x5d\x09\x1b\x61\x40\x8c\x99\
+\x4c\x7b\x67\xf7\xbe\xf6\xee\xdd\xdf\xf7\xfc\x1e\x9f\x73\xce\xdd\
+\x48\x55\xf9\x79\x5e\x22\x7e\xce\x97\xff\xdb\x02\xba\x9e\x7b\xea\
+\xef\x50\xb9\x57\x10\xf1\xea\xf1\xde\xe3\xbd\xa2\x93\xf7\x9d\xc7\
+\xe3\x71\x4e\x51\xef\xf1\xea\xc2\x79\x9f\x5e\xa3\x8a\x73\x2e\x3d\
+\x56\xbc\x7a\xbc\x73\xb5\xfd\xf0\x59\x7a\xde\x7b\xbc\xf3\x38\xa7\
+\x5e\xc4\x7d\xf5\xcf\xfe\xe4\x2f\x1e\xbe\x68\x01\x5d\x5d\x5d\x0d\
+\x18\x7e\xff\x96\xd5\x6b\xc5\x1a\x8b\x18\x83\x20\x00\x88\x84\x55\
+\x38\x56\x54\x15\x55\x82\x30\x4d\x85\x79\x87\xab\x8a\xf0\x0e\xe7\
+\x3c\xea\x3d\x89\x77\x78\x97\x90\x38\x87\x4b\x12\x12\xe7\xf1\x3e\
+\x1c\x57\x45\x17\x8b\x45\xd3\xfd\x7c\xf7\x37\x81\x8b\x17\x50\x2a\
+\x95\x4c\xb6\x60\x7d\x71\xac\xcc\xa7\x7e\xf7\xaf\xc8\x64\x62\x44\
+\x64\x1a\xfe\x54\x34\x6c\xa6\xbe\x42\x41\x70\x3c\xf2\x8d\x8d\x78\
+\xa7\x66\xda\x21\xa4\xaa\xa8\x57\xf2\xb9\x0c\x9f\x58\xb4\x1c\x24\
+\xfc\x48\x68\xf5\x09\x5b\x54\xc3\x2a\xdd\x30\xfb\x8a\x7a\x50\xf0\
+\xde\x91\x78\x48\x12\xc7\xd0\xc8\x18\xaa\x90\xcf\xc6\xcc\xbb\xb2\
+\x91\x6a\x75\x0c\x21\xa9\x78\x55\x2a\x49\xc2\x9e\x57\x5f\x26\x8e\
+\x33\x78\xf5\xd3\xcf\x01\xef\x3d\x1e\x30\xd6\x62\xac\x45\x44\xaa\
+\xed\x98\x86\x4c\x2a\x22\x35\x46\x01\x6b\x84\x38\x8e\x70\x5e\xc1\
+\x09\xd6\x28\xc5\x92\xc3\xd8\x18\x05\x5a\x9b\xeb\x31\x62\xf0\x12\
+\x1a\x41\x0c\x88\x2a\x82\xe2\xbd\x62\xad\x04\x01\xfe\x92\x08\x08\
+\xb1\x5c\xb3\x51\x74\x4a\xc3\xab\x79\x90\xcb\x58\x9c\x0f\x9e\x73\
+\xaa\x38\xa7\x8c\x95\x12\x54\x95\x6c\x6c\x89\x23\x43\x29\x71\xa0\
+\xd4\x3c\x06\x9a\x26\x7b\x30\x3a\x13\xc7\x97\x42\xc0\x30\x49\x52\
+\x4f\xb9\x52\x01\x0d\xc6\x88\x9b\xda\x70\x4d\x2d\xb2\x22\xf8\xc4\
+\xe1\xbc\xe2\xbc\x32\x56\xae\x04\x6f\xa0\x34\xd4\x65\xa8\x54\x3c\
+\x9a\x86\x60\x35\x06\x35\xf5\x6b\x92\x04\xa3\xe3\x4b\x23\x00\x9c\
+\x4b\xa8\x94\x2b\xa1\xba\x24\x09\xd5\x1c\x3e\x9b\xe1\xd5\x8d\x11\
+\xa5\x9c\x24\x69\x99\x84\xe2\x78\x05\xef\x95\x4c\x6c\x30\xc6\x50\
+\x71\x1e\xf5\x3a\x91\x3f\xd5\x7c\x52\xc5\x25\xc1\xdb\x71\x74\x09\
+\x04\x0c\x03\x51\xa9\xc4\xc8\xc8\x29\x5c\xa5\x4c\xa5\x3c\x86\x98\
+\x90\xc5\x35\xc3\x11\x7e\x65\xcd\xd5\xdc\xb1\x72\x01\x8f\x77\xbf\
+\xc3\xf6\x3d\x87\xf0\xce\xe0\x2a\x70\xfb\xca\x76\x6e\x5b\x36\x97\
+\xef\x77\xff\x0f\x3b\xf6\xf4\x52\xc8\x65\x48\x12\xc7\xa7\x6f\xfa\
+\x04\xb7\x2e\x99\xcd\x33\x3b\x0f\xf3\xd2\x1b\xfd\x78\xad\xb9\xa1\
+\xe6\x01\x1b\xd9\x5a\xe8\x46\xd3\x88\x20\x46\x93\xd3\x72\x7c\xe0\
+\x38\x49\xa9\x48\xa9\x78\x0a\x6b\x04\x90\xea\x0b\x10\x36\xac\xee\
+\x60\x46\x21\xcb\x1d\xab\xe6\xb3\x63\xd7\x7f\x53\x2e\x3a\x54\xe1\
+\x53\xcb\xe7\x51\x5f\xc8\xf0\xd9\x55\xf3\x79\xf1\xf5\x5e\x32\x56\
+\x50\x31\xfc\xd2\x8d\x73\xa8\xcb\xc5\xac\x5b\x36\x87\x17\xf6\xf5\
+\x05\x88\xe1\x71\x1e\x7c\x55\x80\x8d\x26\x3c\x70\xb1\x34\x35\xb1\
+\x92\xb3\x39\xfa\x7a\x0f\x71\xdf\x17\x6e\x9c\x92\xa6\x8f\x3f\xbe\
+\xa5\x46\xd3\xdb\xda\x27\x68\xba\x79\xf3\x23\x2c\x5d\x76\x13\x6f\
+\xf5\x45\xe4\x23\x50\x09\x82\x9f\xdd\xd5\xcb\xda\xc5\xb3\x78\xee\
+\x27\xbd\x20\x82\x8a\x84\x92\xaa\x4a\x9a\xc3\x58\x63\xd2\xbc\x81\
+\x08\x7f\xd9\x68\xca\xf6\x1d\xdd\xfc\xb8\x6f\x16\x33\x1a\x9b\x51\
+\x2f\xd8\x58\xe8\xde\xd7\xcf\x8e\x7d\x7d\x38\xef\x11\xb1\x60\x3c\
+\xea\x4c\xc8\x85\x2a\x27\x45\xd0\x5a\x0e\x88\x5c\x3e\x9a\x7a\xc5\
+\x8a\x47\xd5\x83\x01\xd4\x20\xc6\xa0\x5e\x53\xa6\x78\x14\x13\xbe\
+\x68\x3c\xd5\xc0\x74\xce\x4f\x84\xd0\xe5\xa6\xa9\xa4\xe1\x61\x30\
+\x60\x6c\x30\xd2\x00\xde\xa3\x22\xe0\x5d\x38\xa7\x02\x18\x04\x70\
+\x3e\x99\x10\x70\xb9\x69\x6a\xa2\x08\x1b\x59\x8c\x35\x98\xc8\x04\
+\x23\x7d\xf5\xb7\x3c\x60\xc1\x80\x3a\x41\x7c\x12\x3c\x50\x71\x93\
+\x05\x5c\x5e\x9a\x5a\x63\x30\x26\xc2\x9a\x08\x63\x2d\xaa\x92\xf2\
+\x02\x14\x41\xd5\x61\xbc\x45\xc5\x85\x46\xf1\x4a\x25\xa9\x4c\x08\
+\x70\xce\x93\xb8\x20\xc0\x03\xe2\x3f\x9a\xa6\x91\x31\xa1\x5f\x5e\
+\xa5\x69\xe9\xe2\x68\xea\x9c\x63\xff\x6b\x2f\x11\xe7\xea\x89\xb2\
+\x05\xac\x8d\x11\x63\xcf\x48\x98\x89\x5b\x84\xbd\xf1\x72\x85\x72\
+\xa5\x8c\xaf\x56\x21\xef\x1d\x3e\x6d\x15\xf5\x5a\xcd\x93\x73\xd2\
+\xd4\x5a\x43\xe2\x43\x38\x04\x01\xa1\xf5\xe3\xe8\xc2\x68\x2a\x02\
+\x5f\xfb\xd2\xed\xb4\xb4\xb4\xd0\xd8\xd4\x44\x36\x9b\xc1\x18\x13\
+\xf2\x25\xad\x70\xa1\xb2\x39\x5c\xfa\x56\xef\xa9\x54\xca\x67\xe6\
+\x80\xf3\xc1\xdd\x5e\x15\x93\xd6\xdd\xaa\xe1\x9f\xeb\x6c\xe7\xf6\
+\x9b\xae\xe2\x3f\x5f\xec\xe1\xf9\xbd\xbd\x88\x28\xd6\x1a\xd6\x2f\
+\x9d\xcd\x9a\xc5\x57\xf2\xf4\xce\xc3\x3c\xb3\xf3\x30\x22\x86\xba\
+\x7c\x86\x75\x4b\x67\x4f\x90\xf4\xcd\xfe\xda\x7d\xcf\x46\x53\xbc\
+\x67\x46\x5d\x81\xc6\x86\x3a\x66\x36\xd6\x93\xcf\xe7\x31\x46\x50\
+\x0d\xde\xf5\xde\xa7\x64\xf7\xbc\xbd\xff\x00\xbb\x77\xef\x66\xfd\
+\xfa\xf5\x14\x47\x47\x71\xce\x69\x1a\x42\x09\xce\x25\x35\xaf\x85\
+\xf2\x6f\xc0\x80\x88\x70\xe7\xca\xb9\xd4\xe7\x63\xee\x58\x31\x97\
+\x97\xde\x3a\x46\x1c\x59\x8c\xb1\xac\x5b\x36\x87\xba\x5c\xc4\xfa\
+\xa5\x73\xf8\xe1\xee\xa3\x18\x23\x64\xe3\xa8\x46\xd2\xf5\xcb\xe6\
+\xf0\xa3\x37\xfa\x83\x27\xa6\xa0\x69\x92\x24\x3c\xf9\xe4\x13\x14\
+\xea\xeb\xc8\x17\xf2\xc4\x51\x34\x16\x5a\x3f\xc0\x70\x32\x18\x8b\
+\xa3\xa3\x99\x7c\xbe\x20\xff\xfe\xbd\x2d\x1e\x11\x8f\xf2\xf7\xb5\
+\x24\xae\x0a\x88\xa2\x08\x63\x52\x78\xa5\x02\xba\x76\x1d\xe5\xb6\
+\x25\x57\xb2\x6d\x4f\x2f\x99\x28\x26\x93\x89\x30\xc6\xb0\xfd\xb5\
+\x7e\x6e\xb9\xa1\x95\x67\xf7\xf4\x12\xc7\x11\xf9\x4c\x04\x16\x9e\
+\xdd\xdd\xc7\xda\x4f\xb6\xf1\xdc\x9e\x5e\xc2\x4d\xfc\x94\x34\x55\
+\x85\xc5\x4b\x17\xd3\x36\xeb\x4a\x0e\x1d\xea\x29\xbd\xfd\xc6\x81\
+\x6b\x0b\x85\x82\xcb\xe5\x72\x1e\x8a\x14\x81\xb0\x02\x30\x14\x4f\
+\x8f\x03\x63\x94\x4a\x51\xb2\x65\xcb\x96\x81\x5a\x08\x55\x93\x38\
+\xb2\x06\x63\x2c\x22\x20\x46\x10\x11\x5e\x78\xfd\x18\x2f\xbc\x7e\
+\x1c\x24\x08\xcc\xc6\x11\x62\x85\x97\xde\x1e\xa0\x7b\xdf\x31\x4a\
+\x89\x23\x97\xc9\x10\xc7\x16\xef\x95\x17\xde\xe8\xa7\x7b\x5f\x3f\
+\xaa\x3e\x74\xee\xfc\xd4\x34\x05\xe5\x93\x37\x2c\x61\xde\xbc\xf9\
+\xb4\xb5\xb6\x68\x1c\x47\xab\xbf\xfe\x47\x7f\xfa\x0c\x30\x0e\x38\
+\x3d\x8f\x49\xab\xc8\x6b\x5a\x76\x24\xb8\xd5\x5a\x53\x33\x3e\x74\
+\x4f\x0c\x46\x26\xba\x17\x71\x1c\xd7\x20\x57\xb1\x10\x23\x44\xb6\
+\xfa\x79\xe0\x82\x18\x45\x1d\x1f\x49\x53\xe7\x1c\xef\xbe\xfb\x53\
+\x46\x8b\x45\xea\xea\xeb\x72\x73\x66\xcf\xf9\xb7\x7f\x7e\xe4\x1f\
+\x7a\x72\xb9\xec\xfe\xc6\xe6\xa6\xe8\xbf\x7e\xf8\x74\x9d\x31\x26\
+\x16\xc1\xfa\xd0\xf1\x4c\x50\x1d\xf5\xea\x7f\xa4\x95\xf1\x7f\xbc\
+\xf3\xce\x8d\x7d\x91\x77\xbe\x5a\x31\xb1\x91\x0d\x75\x59\x04\xb1\
+\x21\x94\x44\x0c\xc6\x84\xec\x10\x91\x34\xf9\x14\x07\xc4\x2a\x60\
+\x43\xb9\xf3\x1a\xcc\x74\x46\xc1\xfb\x50\xc8\xcf\x83\xa6\x99\x9c\
+\x45\x8c\x23\x93\xb5\xac\x58\xb1\xc2\x7a\x47\xc7\xa3\xff\xf2\xdd\
+\xd8\x2b\xf7\xcc\xbb\x6a\xf6\x60\x6b\x6b\x6b\x12\xc7\x19\x1b\xa2\
+\xce\x44\x26\x63\x67\x16\xf2\xf9\xbb\x6c\xa6\xb0\x05\x58\x1f\x79\
+\xef\x6b\xe4\xb5\xc6\x60\x6d\x28\x85\x22\xc1\x78\x9b\x7a\x03\x31\
+\x18\x23\x18\x93\xa2\x5e\x15\x23\x16\x9f\x96\x5b\xa3\x1e\xef\x14\
+\x8c\x22\x5e\x90\x6a\x69\x3e\x07\x4d\x93\x24\xe1\x07\x3f\x78\x32\
+\x89\xe3\x58\xad\xb5\x18\x63\x00\x31\x23\x23\x23\xbb\x7e\xbc\x73\
+\xd7\x7b\x9d\x9d\x9d\x23\x5f\xbe\xe7\x8b\xdf\x88\x6c\xe6\x36\x41\
+\xf2\xa0\x39\x15\x9c\xf7\x3e\x36\x46\x9e\x16\x11\x89\x6a\xa5\xca\
+\x6b\xe8\x7d\x8a\x99\x64\x6c\x08\x27\x23\x26\x88\xb1\xb5\x3a\x85\
+\x45\x50\x0b\x96\x60\xb8\x53\x8b\xe0\x21\x9d\x2d\x50\x95\xf3\xa1\
+\x69\xf9\x9d\x03\xef\x76\x4e\xee\xeb\x39\xe7\xdc\x89\x13\x27\x4e\
+\xc5\x71\x3c\xfe\xdb\x5f\xfc\x8d\xfb\x0c\x66\xf6\xf1\xbe\xfe\x5f\
+\x1f\x1c\x3c\x75\x72\xa0\x38\x30\xd6\x10\xc5\x76\xe6\xcc\xc6\xd2\
+\xf3\xcf\xef\x3a\x1e\x72\x20\xed\xe2\x0e\x9f\x3a\xcd\xfe\x5d\x3b\
+\x6a\xc3\xc2\xb4\x17\x3d\xfd\xe5\x1c\x34\x55\x25\x73\xf3\xaa\x15\
+\x3f\x99\x35\xab\x0d\x9f\x24\xa5\x7c\x7d\x5d\x76\x68\xe8\x04\x57\
+\x34\x37\x53\x71\xe5\xf1\xb7\x0f\x1c\xc8\x0e\x0f\x9e\xd4\xa6\x96\
+\xc6\x4f\x97\xc6\xc6\xcb\x71\x6c\x5f\x79\xf9\xd5\x3d\x9f\xed\xea\
+\xea\x4a\x00\xaf\xaa\x1a\x79\xef\xc9\xe7\x23\x36\x7f\xeb\x0b\x58\
+\x9b\xe6\x80\xb1\xe9\xdb\x9c\xf1\xae\x26\xb6\xd4\x4c\x49\x6b\x76\
+\x3a\x1e\x08\xfb\x2e\x3d\xf6\xe7\x45\xd3\xd6\x2b\x9a\x29\x97\x4a\
+\x2c\x5f\xbe\x3c\xfa\xe9\xc1\x77\xb5\xad\xa5\x05\x8c\x70\xdd\xc2\
+\xeb\xed\xc1\x9f\x1d\x94\x05\x0b\xe7\xcb\xc0\xc0\x20\x0b\x16\xb4\
+\xe7\xfa\xfb\x8e\xcd\xbf\x61\xf1\xf5\xbf\xb3\x75\xeb\xd6\xef\xd4\
+\xaa\x50\x52\x49\x8c\xf7\x9e\x96\xa6\xc6\xd0\x23\x34\x82\x15\x8b\
+\x58\x8b\x11\xa9\x19\x3e\x91\x17\x32\x21\x40\xd3\x6e\xf2\xa4\x11\
+\x5c\xb5\x1b\x10\x8c\x3f\x37\x4d\xbd\xf7\x18\x11\xda\xdb\xe7\x53\
+\x1c\x3d\x6d\x97\x2d\x59\xca\xa1\xc3\x87\x69\x9b\xd5\x46\x3e\x9f\
+\x8f\xe7\xcd\x9b\xcb\xc8\xc9\x53\x74\x2c\x68\x67\x70\x70\x50\xd6\
+\xac\x59\x93\x7f\xea\xa9\x27\xff\x7c\xd3\xa6\x4d\xff\xfa\xf0\xc3\
+\x0f\x8f\xa5\x63\x62\x79\x6a\xdb\xf6\xed\x1b\x40\x3d\x67\x50\x70\
+\xb2\x71\x0a\x29\x4d\x13\x97\xe4\xc3\x38\xc0\xa7\x93\xae\xd5\x61\
+\xa6\xe2\x52\xc3\xd5\x9f\x9d\xa6\x63\xc5\x22\xf9\x7c\x81\xef\x6d\
+\xd9\x82\x18\x09\x63\x88\xa1\x21\x46\x47\x8b\xac\xee\x5c\xcd\xc8\
+\xe9\x51\x7a\x8f\x1e\xe5\xc4\x89\x21\xae\xbd\xe6\x3a\x8e\xf7\x1f\
+\xa7\xb7\xb7\x97\x9e\x9e\x1e\xbc\x2a\x9f\xf9\xcc\x9d\x57\x75\xae\
+\xbe\xe5\xcd\xee\xee\x6d\xf7\x01\x7f\x0d\x10\xfd\xe1\xbd\xf7\xff\
+\xda\x43\x0f\x3d\xd4\xa6\xaa\x69\xb1\x2e\x4e\xc0\x2f\xdd\x29\x97\
+\xcb\x92\x24\xa7\xe4\xe4\x78\xa5\xae\x7d\xee\xbc\xfd\xab\x57\x75\
+\xda\xe2\x58\xb1\xd6\x55\x13\x11\xe2\x4c\x8e\x57\x77\xee\xa4\xad\
+\x65\x16\x0d\x0d\x8d\x34\x34\xcc\x20\x97\xcb\x11\x65\x62\x4c\x5a\
+\x82\x43\x81\x00\x23\x82\xb5\x11\x4d\x4d\x4d\xe4\x72\x39\x0a\x85\
+\x02\xd9\x6c\x36\x44\x80\xc8\xa4\x51\x5d\x3a\x94\xad\x1e\x0b\x74\
+\x2c\xe8\x98\xdd\xd5\xb5\xf5\x9e\x9a\x00\x80\x07\x1e\x78\xe0\xd8\
+\x54\x39\x28\x61\x8c\x19\x01\xf9\xbf\xfd\xf6\xdf\x7c\xae\x73\xe5\
+\xaa\xf2\xcd\x37\xad\xcc\x97\x4a\xa5\x33\xae\xcb\x66\x73\xf8\xa4\
+\xc2\x7f\x7c\xff\xb1\xf1\x81\x81\x01\x97\xcd\x66\x7d\x1c\xc5\x69\
+\x69\x04\x11\x39\x4b\x49\x90\x40\x7d\x91\x40\xed\xf3\x58\xca\xa5\
+\x72\x36\xa9\x24\xdd\xb5\x1c\x48\x0d\x9c\xea\xdb\x06\xc8\xac\x5a\
+\xbb\x6a\xfe\x5d\x9f\xff\xfc\x37\x5b\x5a\xaf\xd8\x60\x24\xca\xbe\
+\xb6\x77\x2f\xe5\x72\xf9\x03\x02\xb2\x14\x0a\x0d\xdc\xba\x76\x6d\
+\x5c\x1c\x1d\xed\x8d\xe3\xf8\x9d\x86\x99\x4d\x51\x2e\x9b\x29\x18\
+\x63\x62\x63\xc4\x68\x4a\x53\x75\x5a\x2c\x55\x4a\xaf\xbc\x7f\xe8\
+\xf0\xa3\xdb\xb6\xbd\x78\xec\x42\x0b\xdb\x13\x4f\x3c\x31\x3c\xf9\
+\x01\x87\xd9\xb8\x71\xa3\x3d\x72\xe4\x88\x3d\x43\x69\xb9\x2c\xc7\
+\x8f\x1f\xcf\x0e\x0f\x0f\xb7\x74\xde\xbc\x72\xeb\x86\x0d\xbf\x3a\
+\x5f\xc5\xcb\xc9\x93\x27\x00\x4f\x2e\xfa\xe0\x94\x92\x23\x6b\x2d\
+\x9d\x9d\x9d\xb6\x5c\x4a\xda\xbf\xfb\xe8\x77\xec\x47\xd1\xf4\xea\
+\x6b\xaf\xfd\xa7\x7b\xef\xdd\xb4\x7e\x5a\x4f\x68\x96\x2f\x5f\x6e\
+\x0e\x1e\x3c\x18\xe5\x72\x39\xfb\xc1\xd6\x17\x91\x9c\x31\xa6\xfe\
+\xc0\xfe\x03\x6f\x7e\xfd\x8f\x1f\x9c\x9b\x0e\xda\xce\xa7\xf4\x5f\
+\x10\x4d\x75\x1a\x4f\x1a\xa3\x8e\x8e\x0e\x7f\xe4\xc8\x11\x1d\x1e\
+\x1e\x3e\xe3\x26\x49\x92\x78\xe7\x5c\x29\x8a\xa2\x63\xaf\xbc\xbc\
+\xf3\xbe\xe6\xe6\xe6\x19\xd6\x5a\x7b\x8e\x70\xbb\x68\x9a\x4e\xcb\
+\x03\x8f\x3d\xf6\x98\x07\xca\x0f\x3e\xf8\xe0\x02\x1b\xb1\x6d\xe4\
+\xf4\xe9\xd9\x63\xa5\x72\xa6\xa5\xa5\x99\xc1\xc1\x21\x2e\x64\x5b\
+\x1a\x1b\x2b\x65\xf3\xf9\xec\xe0\xe0\xd0\x05\xd1\x74\x5a\x02\xd2\
+\x1b\xb8\xaf\x7e\x6d\xd3\x5f\xce\xbd\xaa\xa3\xa7\xff\x58\xff\xbc\
+\xf6\xa6\x26\x06\x06\x06\xb9\xfe\xba\x6b\x2e\x68\xbb\x62\xc5\xca\
+\xe8\xad\xfd\x6f\x9e\x9b\xa6\xfd\xfd\x1f\xa2\xe9\xb4\x9f\x52\xde\
+\x7f\xff\x57\x7e\x21\x97\xaf\x5f\xbf\xb0\x63\x61\x21\x9b\xcd\xc8\
+\x89\xa1\x13\x55\xfa\xd1\x30\xa3\x9e\x4a\xb9\x7c\xd6\xad\xab\x24\
+\x74\x2c\x68\xa7\x7a\xfd\xe9\x91\x53\xf6\xc6\x25\xcb\xf8\x59\x4f\
+\x0f\xad\x6d\xad\x67\xa7\xe9\x2f\xae\xfd\x10\x4d\xa7\x2d\xa0\x54\
+\x4e\xbe\xf5\xa5\x2f\xff\xd6\xe1\xa3\xbd\x47\x6f\xdc\xfb\xda\x5e\
+\x3c\x9e\x9e\xc3\x87\x08\x95\x3b\x4c\x74\x55\x67\x9b\x27\xcf\x3c\
+\x03\x1c\x39\x72\x04\x55\xe5\xbd\xf7\x8e\xb0\x72\xc5\x4a\x4e\x8d\
+\x9c\xe6\xe8\xfb\xef\x33\x34\x34\x38\x05\x4d\x7f\xf9\x43\x34\x9d\
+\x96\x80\xbb\xef\xbe\xbb\x35\x9b\xcf\xac\x5b\xb4\xe8\x9a\xfa\x45\
+\x57\x2f\x62\xed\x9a\x5b\x6b\x06\x8a\x9c\x69\x74\x15\x19\xb5\xf3\
+\xd5\x91\xe1\xa4\xbc\x56\x14\xee\xfa\xcd\x49\xf3\xa4\x1f\xa6\xe9\
+\xc2\x05\x0b\xcf\xa0\xe9\x74\x73\x60\x64\x78\xf8\x44\xdf\x57\xfe\
+\xe0\xf7\x66\xd9\x28\x12\x3e\x86\xa5\x52\xae\x64\x2b\xe5\x4a\xf7\
+\x25\xc9\x81\xcd\x9b\x37\x8f\xaf\x5b\xb7\xee\x86\xa6\xa6\xa6\xfa\
+\x8f\xf3\x2f\x02\x93\x69\x3a\xed\x1c\xe8\xee\xee\x4e\xd2\xa7\x46\
+\xff\xff\x67\x8f\x8f\x7b\xf9\x5f\x5a\xf1\x31\x65\xff\xe0\x15\x90\
+\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+"
+
+qt_resource_name = "\
+\x00\x06\
+\x07\x03\x7d\xc3\
+\x00\x69\
+\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
+\x00\x0e\
+\x05\xcd\xf4\xe7\
+\x00\x63\
+\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x12\
+\x04\xe4\x91\x47\
+\x00\x63\
+\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\
+\x00\x67\
+\x00\x0c\
+\x07\x11\x5c\xc7\
+\x00\x6c\
+\x00\x65\x00\x61\x00\x70\x00\x66\x00\x72\x00\x6f\x00\x67\x00\x2e\x00\x6a\x00\x70\x00\x67\
+\x00\x13\
+\x0d\x76\x37\xc7\
+\x00\x63\
+\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\
+\x00\x6e\x00\x67\
+"
+
+qt_resource_struct = "\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x02\
+\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf7\
+\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x19\xd2\
+\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x20\xbd\
+"
+
+def qInitResources():
+ QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+ QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
diff --git a/src/leap/tests/fakeclient.py b/src/leap/tests/fakeclient.py
new file mode 100644
index 0000000..45de2cd
--- /dev/null
+++ b/src/leap/tests/fakeclient.py
@@ -0,0 +1,63 @@
+fakeoutput = """
+mullvad Sun Jun 17 14:34:57 2012 OpenVPN 2.2.1 i486-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] [eurephia] [MH] [PF_INET6] [IPv6 payload 20110424-2 (2.2RC2)] built
+ on Mar 23 2012
+Sun Jun 17 14:34:57 2012 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:7505
+Sun Jun 17 14:34:57 2012 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
+Sun Jun 17 14:34:57 2012 WARNING: file 'ssl/1021380964266.key' is group or others accessible
+Sun Jun 17 14:34:57 2012 LZO compression initialized
+Sun Jun 17 14:34:57 2012 Control Channel MTU parms [ L:1542 D:138 EF:38 EB:0 ET:0 EL:0 ]
+Sun Jun 17 14:34:57 2012 Socket Buffers: R=[163840->131072] S=[163840->131072]
+Sun Jun 17 14:34:57 2012 Data Channel MTU parms [ L:1542 D:1450 EF:42 EB:135 ET:0 EL:0 AF:3/1 ]
+Sun Jun 17 14:34:57 2012 Local Options hash (VER=V4): '41690919'
+Sun Jun 17 14:34:57 2012 Expected Remote Options hash (VER=V4): '530fdded'
+Sun Jun 17 14:34:57 2012 UDPv4 link local: [undef]
+Sun Jun 17 14:34:57 2012 UDPv4 link remote: [AF_INET]46.21.99.25:1197
+Sun Jun 17 14:34:57 2012 TLS: Initial packet from [AF_INET]46.21.99.25:1197, sid=63c29ace 1d3060d0
+Sun Jun 17 14:34:58 2012 VERIFY OK: depth=2, /C=NA/ST=None/L=None/O=Mullvad/CN=Mullvad_CA/emailAddress=info@mullvad.net
+Sun Jun 17 14:34:58 2012 VERIFY OK: depth=1, /C=NA/ST=None/L=None/O=Mullvad/CN=master.mullvad.net/emailAddress=info@mullvad.net
+Sun Jun 17 14:34:58 2012 Validating certificate key usage
+Sun Jun 17 14:34:58 2012 ++ Certificate has key usage 00a0, expects 00a0
+Sun Jun 17 14:34:58 2012 VERIFY KU OK
+Sun Jun 17 14:34:58 2012 Validating certificate extended key usage
+Sun Jun 17 14:34:58 2012 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
+Sun Jun 17 14:34:58 2012 VERIFY EKU OK
+Sun Jun 17 14:34:58 2012 VERIFY OK: depth=0, /C=NA/ST=None/L=None/O=Mullvad/CN=se2.mullvad.net/emailAddress=info@mullvad.net
+Sun Jun 17 14:34:59 2012 Data Channel Encrypt: Cipher 'BF-CBC' initialized with 128 bit key
+Sun Jun 17 14:34:59 2012 Data Channel Encrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
+Sun Jun 17 14:34:59 2012 Data Channel Decrypt: Cipher 'BF-CBC' initialized with 128 bit key
+Sun Jun 17 14:34:59 2012 Data Channel Decrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
+Sun Jun 17 14:34:59 2012 Control Channel: TLSv1, cipher TLSv1/SSLv3 DHE-RSA-AES256-SHA, 2048 bit RSA
+Sun Jun 17 14:34:59 2012 [se2.mullvad.net] Peer Connection Initiated with [AF_INET]46.21.99.25:1197
+Sun Jun 17 14:35:01 2012 SENT CONTROL [se2.mullvad.net]: 'PUSH_REQUEST' (status=1)
+Sun Jun 17 14:35:02 2012 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1 bypass-dhcp,dhcp-option DNS 10.11.0.1,route 10.11.0.1,topology net30,ifconfig 10.11.0.202 10.11.0.201'
+Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: --ifconfig/up options modified
+Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: route options modified
+Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
+Sun Jun 17 14:35:02 2012 ROUTE default_gateway=192.168.0.1
+Sun Jun 17 14:35:02 2012 TUN/TAP device tun0 opened
+Sun Jun 17 14:35:02 2012 TUN/TAP TX queue length set to 100
+Sun Jun 17 14:35:02 2012 do_ifconfig, tt->ipv6=0, tt->did_ifconfig_ipv6_setup=0
+Sun Jun 17 14:35:02 2012 /sbin/ifconfig tun0 10.11.0.202 pointopoint 10.11.0.201 mtu 1500
+Sun Jun 17 14:35:02 2012 /etc/openvpn/update-resolv-conf tun0 1500 1542 10.11.0.202 10.11.0.201 init
+dhcp-option DNS 10.11.0.1
+Sun Jun 17 14:35:05 2012 /sbin/route add -net 46.21.99.25 netmask 255.255.255.255 gw 192.168.0.1
+Sun Jun 17 14:35:05 2012 /sbin/route add -net 0.0.0.0 netmask 128.0.0.0 gw 10.11.0.201
+Sun Jun 17 14:35:05 2012 /sbin/route add -net 128.0.0.0 netmask 128.0.0.0 gw 10.11.0.201
+Sun Jun 17 14:35:05 2012 /sbin/route add -net 10.11.0.1 netmask 255.255.255.255 gw 10.11.0.201
+Sun Jun 17 14:35:05 2012 Initialization Sequence Completed
+Sun Jun 17 14:34:57 2012 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:7505
+"""
+
+import time
+import sys
+
+
+def write_output():
+ for line in fakeoutput.split('\n'):
+ sys.stdout.write(line + '\n')
+ sys.stdout.flush()
+ #print(line)
+ time.sleep(0.1)
+
+if __name__ == "__main__":
+ write_output()
diff --git a/src/leap/tests/mocks/__init__.py b/src/leap/tests/mocks/__init__.py
new file mode 100644
index 0000000..06f9687
--- /dev/null
+++ b/src/leap/tests/mocks/__init__.py
@@ -0,0 +1 @@
+import manager
diff --git a/src/leap/tests/mocks/manager.py b/src/leap/tests/mocks/manager.py
new file mode 100644
index 0000000..564631c
--- /dev/null
+++ b/src/leap/tests/mocks/manager.py
@@ -0,0 +1,20 @@
+from mock import Mock
+
+from eip_client.vpnmanager import OpenVPNManager
+
+vpn_commands = {
+ 'status': [
+ 'OpenVPN STATISTICS', 'Updated,Mon Jun 25 11:51:21 2012',
+ 'TUN/TAP read bytes,306170', 'TUN/TAP write bytes,872102',
+ 'TCP/UDP read bytes,986177', 'TCP/UDP write bytes,439329',
+ 'Auth read bytes,872102'],
+ 'state': ['1340616463,CONNECTED,SUCCESS,172.28.0.2,198.252.153.38'],
+ # XXX add more tests
+ }
+
+
+def get_openvpn_manager_mocks():
+ manager = OpenVPNManager()
+ manager.status = Mock(return_value='\n'.join(vpn_commands['status']))
+ manager.state = Mock(return_value=vpn_commands['state'][0])
+ return manager
diff --git a/src/leap/utils/__init__.py b/src/leap/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/utils/__init__.py
diff --git a/src/leap/utils/coroutines.py b/src/leap/utils/coroutines.py
new file mode 100644
index 0000000..5e25eb6
--- /dev/null
+++ b/src/leap/utils/coroutines.py
@@ -0,0 +1,107 @@
+# the problem of watching a stdout pipe from
+# openvpn binary: using subprocess and coroutines
+# acting as event consumers
+
+from __future__ import division, print_function
+
+from subprocess import PIPE, Popen
+import sys
+from threading import Thread
+
+ON_POSIX = 'posix' in sys.builtin_module_names
+
+
+#
+# Coroutines goodies
+#
+
+def coroutine(func):
+ def start(*args, **kwargs):
+ cr = func(*args, **kwargs)
+ cr.next()
+ return cr
+ return start
+
+
+@coroutine
+def process_events(callback):
+ """
+ coroutine loop that receives
+ events sent and dispatch the callback.
+ :param callback: callback to be called\
+for each event
+ :type callback: callable
+ """
+ try:
+ while True:
+ m = (yield)
+ if callable(callback):
+ callback(m)
+ else:
+ #XXX log instead
+ print('not a callable passed')
+ except GeneratorExit:
+ return
+
+#
+# Threads
+#
+
+
+def launch_thread(target, args):
+ """
+ launch and demonize thread.
+ :param target: target function that will run in thread
+ :type target: function
+ :param args: args to be passed to thread
+ :type args: list
+ """
+ t = Thread(target=target,
+ args=args)
+ t.daemon = True
+ t.start()
+ return t
+
+
+def watch_output(out, observers):
+ """
+ initializes dict of observer coroutines
+ and pushes lines to each of them as they are received
+ from the watched output.
+ :param out: stdout of a process.
+ :type out: fd
+ :param observers: tuple of coroutines to send data\
+for each event
+ :type ovservers: tuple
+ """
+ observer_dict = {observer: process_events(observer)
+ for observer in observers}
+ for line in iter(out.readline, b''):
+ for obs in observer_dict:
+ observer_dict[obs].send(line)
+ out.close()
+
+
+def spawn_and_watch_process(command, args, observers=None):
+ """
+ spawns a subprocess with command, args, and launch
+ a watcher thread.
+ :param command: command to be executed in the subprocess
+ :type command: str
+ :param args: arguments
+ :type args: list
+ :param observers: tuple of observer functions to be called \
+for each line in the subprocess output.
+ :type observers: tuple
+ :return: a tuple containing the child process instance, and watcher_thread,
+ :rtype: (Subprocess, Thread)
+ """
+ subp = Popen([command] + args,
+ stdout=PIPE,
+ stderr=PIPE,
+ bufsize=1,
+ close_fds=ON_POSIX)
+ watcher = launch_thread(
+ watch_output,
+ (subp.stdout, observers))
+ return subp, watcher
diff --git a/src/leap/utils/leap_argparse.py b/src/leap/utils/leap_argparse.py
new file mode 100644
index 0000000..9c35513
--- /dev/null
+++ b/src/leap/utils/leap_argparse.py
@@ -0,0 +1,20 @@
+import argparse
+
+
+def build_parser():
+ epilog = "Copyright 2012 The Leap Project"
+ parser = argparse.ArgumentParser(description="""
+Launches main LEAP Client""", epilog=epilog)
+ parser.add_argument('--debug', action="store_true",
+ help='launches in debug mode')
+ parser.add_argument('--config', metavar="CONFIG FILE", nargs='?',
+ action="store", dest="config_file",
+ type=argparse.FileType('r'),
+ help='optional config file')
+ return parser
+
+
+def init_leapc_args():
+ parser = build_parser()
+ opts = parser.parse_args()
+ return parser, opts
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..ef0df1c
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+# XXX put here a sample BaseEIPTestCase
diff --git a/tests/support.py b/tests/support.py
new file mode 100644
index 0000000..8ac4966
--- /dev/null
+++ b/tests/support.py
@@ -0,0 +1,111 @@
+# code borrowed from python stdlib tests
+# I think we're not using it at the end...
+# XXX Review and Remove
+
+import contextlib
+import socket
+import sys
+import unittest
+
+
+HOST = "localhost"
+
+
+class TestFailed(Exception):
+ """Test failed."""
+
+
+def bind_port(sock, host=HOST):
+ """Bind the socket to a free port and return the port number. Relies on
+ ephemeral ports in order to ensure we are using an unbound port. This is
+ important as many tests may be running simultaneously, especially in a
+ buildbot environment. This method raises an exception if the sock.family
+ is AF_INET and sock.type is SOCK_STREAM, *and* the socket has SO_REUSEADDR
+ or SO_REUSEPORT set on it. Tests should *never* set these socket options
+ for TCP/IP sockets. The only case for setting these options is testing
+ multicasting via multiple UDP sockets.
+
+ Additionally, if the SO_EXCLUSIVEADDRUSE socket option is available (i.e.
+ on Windows), it will be set on the socket. This will prevent anyone else
+ from bind()'ing to our host/port for the duration of the test.
+ """
+
+ if sock.family == socket.AF_INET and sock.type == socket.SOCK_STREAM:
+ if hasattr(socket, 'SO_REUSEADDR'):
+ if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1:
+ raise TestFailed("tests should never set the SO_REUSEADDR " \
+ "socket option on TCP/IP sockets!")
+ if hasattr(socket, 'SO_REUSEPORT'):
+ if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1:
+ raise TestFailed("tests should never set the SO_REUSEPORT " \
+ "socket option on TCP/IP sockets!")
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+
+ sock.bind((host, 0))
+ port = sock.getsockname()[1]
+ return port
+
+
+def _run_suite(suite):
+ """Run tests from a unittest.TestSuite-derived class."""
+ runner = unittest.TextTestRunner(sys.stdout, verbosity=2,
+ failfast=False)
+ result = runner.run(suite)
+ if not result.wasSuccessful():
+ if len(result.errors) == 1 and not result.failures:
+ err = result.errors[0][1]
+ elif len(result.failures) == 1 and not result.errors:
+ err = result.failures[0][1]
+ else:
+ err = "multiple errors occurred"
+ raise TestFailed(err)
+
+
+def run_unittest(*classes):
+ """Run tests from unittest.TestCase-derived classes."""
+ valid_types = (unittest.TestSuite, unittest.TestCase)
+ suite = unittest.TestSuite()
+ for cls in classes:
+ if isinstance(cls, str):
+ if cls in sys.modules:
+ suite.addTest(unittest.findTestCases(sys.modules[cls]))
+ else:
+ raise ValueError("str arguments must be keys in sys.modules")
+ elif isinstance(cls, valid_types):
+ suite.addTest(cls)
+ else:
+ suite.addTest(unittest.makeSuite(cls))
+
+ _run_suite(suite)
+
+
+@contextlib.contextmanager
+def captured_output(stream_name):
+ """Return a context manager used by captured_stdout/stdin/stderr
+ that temporarily replaces the sys stream *stream_name* with a StringIO."""
+ import io
+ orig_stdout = getattr(sys, stream_name)
+ setattr(sys, stream_name, io.StringIO())
+ try:
+ yield getattr(sys, stream_name)
+ finally:
+ setattr(sys, stream_name, orig_stdout)
+
+
+def captured_stdout():
+ """Capture the output of sys.stdout:
+
+ with captured_stdout() as s:
+ print("hello")
+ self.assertEqual(s.getvalue(), "hello")
+ """
+ return captured_output("stdout")
+
+
+def captured_stderr():
+ return captured_output("stderr")
+
+
+def captured_stdin():
+ return captured_output("stdin")
diff --git a/tests/support_tests.py b/tests/support_tests.py
new file mode 100644
index 0000000..2c56e12
--- /dev/null
+++ b/tests/support_tests.py
@@ -0,0 +1,1725 @@
+"""Supporting definitions for the Python regression tests."""
+
+if __name__ != 'test.support':
+ raise ImportError('support must be imported from the test package')
+
+import contextlib
+import errno
+import functools
+import gc
+import socket
+import sys
+import os
+import platform
+import shutil
+import warnings
+import unittest
+import importlib
+import collections.abc
+import re
+import subprocess
+import imp
+import time
+import sysconfig
+import fnmatch
+import logging.handlers
+import struct
+
+try:
+ import _thread, threading
+except ImportError:
+ _thread = None
+ threading = None
+try:
+ import multiprocessing.process
+except ImportError:
+ multiprocessing = None
+
+try:
+ import faulthandler
+except ImportError:
+ faulthandler = None
+
+try:
+ import zlib
+except ImportError:
+ zlib = None
+
+__all__ = [
+ "Error", "TestFailed", "ResourceDenied", "import_module",
+ "verbose", "use_resources", "max_memuse", "record_original_stdout",
+ "get_original_stdout", "unload", "unlink", "rmtree", "forget",
+ "is_resource_enabled", "requires", "requires_freebsd_version",
+ "requires_linux_version", "requires_mac_ver", "find_unused_port", "bind_port",
+ "IPV6_ENABLED", "is_jython", "TESTFN", "HOST", "SAVEDCWD", "temp_cwd",
+ "findfile", "create_empty_file", "sortdict", "check_syntax_error", "open_urlresource",
+ "check_warnings", "CleanImport", "EnvironmentVarGuard", "TransientResource",
+ "captured_stdout", "captured_stdin", "captured_stderr", "time_out",
+ "socket_peer_reset", "ioerror_peer_reset", "run_with_locale", 'temp_umask',
+ "transient_internet", "set_memlimit", "bigmemtest", "bigaddrspacetest",
+ "BasicTestRunner", "run_unittest", "run_doctest", "threading_setup",
+ "threading_cleanup", "reap_children", "cpython_only", "check_impl_detail",
+ "get_attribute", "swap_item", "swap_attr", "requires_IEEE_754",
+ "TestHandler", "Matcher", "can_symlink", "skip_unless_symlink",
+ "import_fresh_module", "requires_zlib", "PIPE_MAX_SIZE", "failfast",
+ "anticipate_failure"
+ ]
+
+class Error(Exception):
+ """Base class for regression test exceptions."""
+
+class TestFailed(Error):
+ """Test failed."""
+
+class ResourceDenied(unittest.SkipTest):
+ """Test skipped because it requested a disallowed resource.
+
+ This is raised when a test calls requires() for a resource that
+ has not be enabled. It is used to distinguish between expected
+ and unexpected skips.
+ """
+
+@contextlib.contextmanager
+def _ignore_deprecated_imports(ignore=True):
+ """Context manager to suppress package and module deprecation
+ warnings when importing them.
+
+ If ignore is False, this context manager has no effect."""
+ if ignore:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", ".+ (module|package)",
+ DeprecationWarning)
+ yield
+ else:
+ yield
+
+
+def import_module(name, deprecated=False):
+ """Import and return the module to be tested, raising SkipTest if
+ it is not available.
+
+ If deprecated is True, any module or package deprecation messages
+ will be suppressed."""
+ with _ignore_deprecated_imports(deprecated):
+ try:
+ return importlib.import_module(name)
+ except ImportError as msg:
+ raise unittest.SkipTest(str(msg))
+
+
+def _save_and_remove_module(name, orig_modules):
+ """Helper function to save and remove a module from sys.modules
+
+ Raise ImportError if the module can't be imported."""
+ # try to import the module and raise an error if it can't be imported
+ if name not in sys.modules:
+ __import__(name)
+ del sys.modules[name]
+ for modname in list(sys.modules):
+ if modname == name or modname.startswith(name + '.'):
+ orig_modules[modname] = sys.modules[modname]
+ del sys.modules[modname]
+
+def _save_and_block_module(name, orig_modules):
+ """Helper function to save and block a module in sys.modules
+
+ Return True if the module was in sys.modules, False otherwise."""
+ saved = True
+ try:
+ orig_modules[name] = sys.modules[name]
+ except KeyError:
+ saved = False
+ sys.modules[name] = None
+ return saved
+
+
+def anticipate_failure(condition):
+ """Decorator to mark a test that is known to be broken in some cases
+
+ Any use of this decorator should have a comment identifying the
+ associated tracker issue.
+ """
+ if condition:
+ return unittest.expectedFailure
+ return lambda f: f
+
+
+def import_fresh_module(name, fresh=(), blocked=(), deprecated=False):
+ """Imports and returns a module, deliberately bypassing the sys.modules cache
+ and importing a fresh copy of the module. Once the import is complete,
+ the sys.modules cache is restored to its original state.
+
+ Modules named in fresh are also imported anew if needed by the import.
+ If one of these modules can't be imported, None is returned.
+
+ Importing of modules named in blocked is prevented while the fresh import
+ takes place.
+
+ If deprecated is True, any module or package deprecation messages
+ will be suppressed."""
+ # NOTE: test_heapq, test_json and test_warnings include extra sanity checks
+ # to make sure that this utility function is working as expected
+ with _ignore_deprecated_imports(deprecated):
+ # Keep track of modules saved for later restoration as well
+ # as those which just need a blocking entry removed
+ orig_modules = {}
+ names_to_remove = []
+ _save_and_remove_module(name, orig_modules)
+ try:
+ for fresh_name in fresh:
+ _save_and_remove_module(fresh_name, orig_modules)
+ for blocked_name in blocked:
+ if not _save_and_block_module(blocked_name, orig_modules):
+ names_to_remove.append(blocked_name)
+ fresh_module = importlib.import_module(name)
+ except ImportError:
+ fresh_module = None
+ finally:
+ for orig_name, module in orig_modules.items():
+ sys.modules[orig_name] = module
+ for name_to_remove in names_to_remove:
+ del sys.modules[name_to_remove]
+ return fresh_module
+
+
+def get_attribute(obj, name):
+ """Get an attribute, raising SkipTest if AttributeError is raised."""
+ try:
+ attribute = getattr(obj, name)
+ except AttributeError:
+ raise unittest.SkipTest("object %r has no attribute %r" % (obj, name))
+ else:
+ return attribute
+
+verbose = 1 # Flag set to 0 by regrtest.py
+use_resources = None # Flag set to [] by regrtest.py
+max_memuse = 0 # Disable bigmem tests (they will still be run with
+ # small sizes, to make sure they work.)
+real_max_memuse = 0
+failfast = False
+match_tests = None
+
+# _original_stdout is meant to hold stdout at the time regrtest began.
+# This may be "the real" stdout, or IDLE's emulation of stdout, or whatever.
+# The point is to have some flavor of stdout the user can actually see.
+_original_stdout = None
+def record_original_stdout(stdout):
+ global _original_stdout
+ _original_stdout = stdout
+
+def get_original_stdout():
+ return _original_stdout or sys.stdout
+
+def unload(name):
+ try:
+ del sys.modules[name]
+ except KeyError:
+ pass
+
+def unlink(filename):
+ try:
+ os.unlink(filename)
+ except OSError as error:
+ # The filename need not exist.
+ if error.errno not in (errno.ENOENT, errno.ENOTDIR):
+ raise
+
+def rmtree(path):
+ try:
+ shutil.rmtree(path)
+ except OSError as error:
+ if error.errno != errno.ENOENT:
+ raise
+
+def make_legacy_pyc(source):
+ """Move a PEP 3147 pyc/pyo file to its legacy pyc/pyo location.
+
+ The choice of .pyc or .pyo extension is done based on the __debug__ flag
+ value.
+
+ :param source: The file system path to the source file. The source file
+ does not need to exist, however the PEP 3147 pyc file must exist.
+ :return: The file system path to the legacy pyc file.
+ """
+ pyc_file = imp.cache_from_source(source)
+ up_one = os.path.dirname(os.path.abspath(source))
+ legacy_pyc = os.path.join(up_one, source + ('c' if __debug__ else 'o'))
+ os.rename(pyc_file, legacy_pyc)
+ return legacy_pyc
+
+def forget(modname):
+ """'Forget' a module was ever imported.
+
+ This removes the module from sys.modules and deletes any PEP 3147 or
+ legacy .pyc and .pyo files.
+ """
+ unload(modname)
+ for dirname in sys.path:
+ source = os.path.join(dirname, modname + '.py')
+ # It doesn't matter if they exist or not, unlink all possible
+ # combinations of PEP 3147 and legacy pyc and pyo files.
+ unlink(source + 'c')
+ unlink(source + 'o')
+ unlink(imp.cache_from_source(source, debug_override=True))
+ unlink(imp.cache_from_source(source, debug_override=False))
+
+# On some platforms, should not run gui test even if it is allowed
+# in `use_resources'.
+if sys.platform.startswith('win'):
+ import ctypes
+ import ctypes.wintypes
+ def _is_gui_available():
+ UOI_FLAGS = 1
+ WSF_VISIBLE = 0x0001
+ class USEROBJECTFLAGS(ctypes.Structure):
+ _fields_ = [("fInherit", ctypes.wintypes.BOOL),
+ ("fReserved", ctypes.wintypes.BOOL),
+ ("dwFlags", ctypes.wintypes.DWORD)]
+ dll = ctypes.windll.user32
+ h = dll.GetProcessWindowStation()
+ if not h:
+ raise ctypes.WinError()
+ uof = USEROBJECTFLAGS()
+ needed = ctypes.wintypes.DWORD()
+ res = dll.GetUserObjectInformationW(h,
+ UOI_FLAGS,
+ ctypes.byref(uof),
+ ctypes.sizeof(uof),
+ ctypes.byref(needed))
+ if not res:
+ raise ctypes.WinError()
+ return bool(uof.dwFlags & WSF_VISIBLE)
+else:
+ def _is_gui_available():
+ return True
+
+def is_resource_enabled(resource):
+ """Test whether a resource is enabled. Known resources are set by
+ regrtest.py."""
+ return use_resources is not None and resource in use_resources
+
+def requires(resource, msg=None):
+ """Raise ResourceDenied if the specified resource is not available.
+
+ If the caller's module is __main__ then automatically return True. The
+ possibility of False being returned occurs when regrtest.py is
+ executing.
+ """
+ if resource == 'gui' and not _is_gui_available():
+ raise unittest.SkipTest("Cannot use the 'gui' resource")
+ # see if the caller's module is __main__ - if so, treat as if
+ # the resource was set
+ if sys._getframe(1).f_globals.get("__name__") == "__main__":
+ return
+ if not is_resource_enabled(resource):
+ if msg is None:
+ msg = "Use of the %r resource not enabled" % resource
+ raise ResourceDenied(msg)
+
+def _requires_unix_version(sysname, min_version):
+ """Decorator raising SkipTest if the OS is `sysname` and the version is less
+ than `min_version`.
+
+ For example, @_requires_unix_version('FreeBSD', (7, 2)) raises SkipTest if
+ the FreeBSD version is less than 7.2.
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ if platform.system() == sysname:
+ version_txt = platform.release().split('-', 1)[0]
+ try:
+ version = tuple(map(int, version_txt.split('.')))
+ except ValueError:
+ pass
+ else:
+ if version < min_version:
+ min_version_txt = '.'.join(map(str, min_version))
+ raise unittest.SkipTest(
+ "%s version %s or higher required, not %s"
+ % (sysname, min_version_txt, version_txt))
+ return wrapper
+ return decorator
+
+def requires_freebsd_version(*min_version):
+ """Decorator raising SkipTest if the OS is FreeBSD and the FreeBSD version is
+ less than `min_version`.
+
+ For example, @requires_freebsd_version(7, 2) raises SkipTest if the FreeBSD
+ version is less than 7.2.
+ """
+ return _requires_unix_version('FreeBSD', min_version)
+
+def requires_linux_version(*min_version):
+ """Decorator raising SkipTest if the OS is Linux and the Linux version is
+ less than `min_version`.
+
+ For example, @requires_linux_version(2, 6, 32) raises SkipTest if the Linux
+ version is less than 2.6.32.
+ """
+ return _requires_unix_version('Linux', min_version)
+
+def requires_mac_ver(*min_version):
+ """Decorator raising SkipTest if the OS is Mac OS X and the OS X
+ version if less than min_version.
+
+ For example, @requires_mac_ver(10, 5) raises SkipTest if the OS X version
+ is lesser than 10.5.
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ if sys.platform == 'darwin':
+ version_txt = platform.mac_ver()[0]
+ try:
+ version = tuple(map(int, version_txt.split('.')))
+ except ValueError:
+ pass
+ else:
+ if version < min_version:
+ min_version_txt = '.'.join(map(str, min_version))
+ raise unittest.SkipTest(
+ "Mac OS X %s or higher required, not %s"
+ % (min_version_txt, version_txt))
+ return func(*args, **kw)
+ wrapper.min_version = min_version
+ return wrapper
+ return decorator
+
+
+HOST = 'localhost'
+
+def find_unused_port(family=socket.AF_INET, socktype=socket.SOCK_STREAM):
+ """Returns an unused port that should be suitable for binding. This is
+ achieved by creating a temporary socket with the same family and type as
+ the 'sock' parameter (default is AF_INET, SOCK_STREAM), and binding it to
+ the specified host address (defaults to 0.0.0.0) with the port set to 0,
+ eliciting an unused ephemeral port from the OS. The temporary socket is
+ then closed and deleted, and the ephemeral port is returned.
+
+ Either this method or bind_port() should be used for any tests where a
+ server socket needs to be bound to a particular port for the duration of
+ the test. Which one to use depends on whether the calling code is creating
+ a python socket, or if an unused port needs to be provided in a constructor
+ or passed to an external program (i.e. the -accept argument to openssl's
+ s_server mode). Always prefer bind_port() over find_unused_port() where
+ possible. Hard coded ports should *NEVER* be used. As soon as a server
+ socket is bound to a hard coded port, the ability to run multiple instances
+ of the test simultaneously on the same host is compromised, which makes the
+ test a ticking time bomb in a buildbot environment. On Unix buildbots, this
+ may simply manifest as a failed test, which can be recovered from without
+ intervention in most cases, but on Windows, the entire python process can
+ completely and utterly wedge, requiring someone to log in to the buildbot
+ and manually kill the affected process.
+
+ (This is easy to reproduce on Windows, unfortunately, and can be traced to
+ the SO_REUSEADDR socket option having different semantics on Windows versus
+ Unix/Linux. On Unix, you can't have two AF_INET SOCK_STREAM sockets bind,
+ listen and then accept connections on identical host/ports. An EADDRINUSE
+ socket.error will be raised at some point (depending on the platform and
+ the order bind and listen were called on each socket).
+
+ However, on Windows, if SO_REUSEADDR is set on the sockets, no EADDRINUSE
+ will ever be raised when attempting to bind two identical host/ports. When
+ accept() is called on each socket, the second caller's process will steal
+ the port from the first caller, leaving them both in an awkwardly wedged
+ state where they'll no longer respond to any signals or graceful kills, and
+ must be forcibly killed via OpenProcess()/TerminateProcess().
+
+ The solution on Windows is to use the SO_EXCLUSIVEADDRUSE socket option
+ instead of SO_REUSEADDR, which effectively affords the same semantics as
+ SO_REUSEADDR on Unix. Given the propensity of Unix developers in the Open
+ Source world compared to Windows ones, this is a common mistake. A quick
+ look over OpenSSL's 0.9.8g source shows that they use SO_REUSEADDR when
+ openssl.exe is called with the 's_server' option, for example. See
+ http://bugs.python.org/issue2550 for more info. The following site also
+ has a very thorough description about the implications of both REUSEADDR
+ and EXCLUSIVEADDRUSE on Windows:
+ http://msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx)
+
+ XXX: although this approach is a vast improvement on previous attempts to
+ elicit unused ports, it rests heavily on the assumption that the ephemeral
+ port returned to us by the OS won't immediately be dished back out to some
+ other process when we close and delete our temporary socket but before our
+ calling code has a chance to bind the returned port. We can deal with this
+ issue if/when we come across it.
+ """
+
+ tempsock = socket.socket(family, socktype)
+ port = bind_port(tempsock)
+ tempsock.close()
+ del tempsock
+ return port
+
+def bind_port(sock, host=HOST):
+ """Bind the socket to a free port and return the port number. Relies on
+ ephemeral ports in order to ensure we are using an unbound port. This is
+ important as many tests may be running simultaneously, especially in a
+ buildbot environment. This method raises an exception if the sock.family
+ is AF_INET and sock.type is SOCK_STREAM, *and* the socket has SO_REUSEADDR
+ or SO_REUSEPORT set on it. Tests should *never* set these socket options
+ for TCP/IP sockets. The only case for setting these options is testing
+ multicasting via multiple UDP sockets.
+
+ Additionally, if the SO_EXCLUSIVEADDRUSE socket option is available (i.e.
+ on Windows), it will be set on the socket. This will prevent anyone else
+ from bind()'ing to our host/port for the duration of the test.
+ """
+
+ if sock.family == socket.AF_INET and sock.type == socket.SOCK_STREAM:
+ if hasattr(socket, 'SO_REUSEADDR'):
+ if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1:
+ raise TestFailed("tests should never set the SO_REUSEADDR " \
+ "socket option on TCP/IP sockets!")
+ if hasattr(socket, 'SO_REUSEPORT'):
+ if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1:
+ raise TestFailed("tests should never set the SO_REUSEPORT " \
+ "socket option on TCP/IP sockets!")
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+
+ sock.bind((host, 0))
+ port = sock.getsockname()[1]
+ return port
+
+def _is_ipv6_enabled():
+ """Check whether IPv6 is enabled on this host."""
+ if socket.has_ipv6:
+ try:
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ sock.bind(('::1', 0))
+ except (socket.error, socket.gaierror):
+ pass
+ else:
+ sock.close()
+ return True
+ return False
+
+IPV6_ENABLED = _is_ipv6_enabled()
+
+
+# A constant likely larger than the underlying OS pipe buffer size.
+# Windows limit seems to be around 512B, and most Unix kernels have a 64K pipe
+# buffer size: take 1M to be sure.
+PIPE_MAX_SIZE = 1024 * 1024
+
+
+# decorator for skipping tests on non-IEEE 754 platforms
+requires_IEEE_754 = unittest.skipUnless(
+ float.__getformat__("double").startswith("IEEE"),
+ "test requires IEEE 754 doubles")
+
+requires_zlib = unittest.skipUnless(zlib, 'requires zlib')
+
+is_jython = sys.platform.startswith('java')
+
+# Filename used for testing
+if os.name == 'java':
+ # Jython disallows @ in module names
+ TESTFN = '$test'
+else:
+ TESTFN = '@test'
+
+# Disambiguate TESTFN for parallel testing, while letting it remain a valid
+# module name.
+TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid())
+
+
+# TESTFN_UNICODE is a non-ascii filename
+TESTFN_UNICODE = TESTFN + "-\xe0\xf2\u0258\u0141\u011f"
+if sys.platform == 'darwin':
+ # In Mac OS X's VFS API file names are, by definition, canonically
+ # decomposed Unicode, encoded using UTF-8. See QA1173:
+ # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html
+ import unicodedata
+ TESTFN_UNICODE = unicodedata.normalize('NFD', TESTFN_UNICODE)
+TESTFN_ENCODING = sys.getfilesystemencoding()
+
+# TESTFN_UNENCODABLE is a filename (str type) that should *not* be able to be
+# encoded by the filesystem encoding (in strict mode). It can be None if we
+# cannot generate such filename.
+TESTFN_UNENCODABLE = None
+if os.name in ('nt', 'ce'):
+ # skip win32s (0) or Windows 9x/ME (1)
+ if sys.getwindowsversion().platform >= 2:
+ # Different kinds of characters from various languages to minimize the
+ # probability that the whole name is encodable to MBCS (issue #9819)
+ TESTFN_UNENCODABLE = TESTFN + "-\u5171\u0141\u2661\u0363\uDC80"
+ try:
+ TESTFN_UNENCODABLE.encode(TESTFN_ENCODING)
+ except UnicodeEncodeError:
+ pass
+ else:
+ print('WARNING: The filename %r CAN be encoded by the filesystem encoding (%s). '
+ 'Unicode filename tests may not be effective'
+ % (TESTFN_UNENCODABLE, TESTFN_ENCODING))
+ TESTFN_UNENCODABLE = None
+# Mac OS X denies unencodable filenames (invalid utf-8)
+elif sys.platform != 'darwin':
+ try:
+ # ascii and utf-8 cannot encode the byte 0xff
+ b'\xff'.decode(TESTFN_ENCODING)
+ except UnicodeDecodeError:
+ # 0xff will be encoded using the surrogate character u+DCFF
+ TESTFN_UNENCODABLE = TESTFN \
+ + b'-\xff'.decode(TESTFN_ENCODING, 'surrogateescape')
+ else:
+ # File system encoding (eg. ISO-8859-* encodings) can encode
+ # the byte 0xff. Skip some unicode filename tests.
+ pass
+
+# Save the initial cwd
+SAVEDCWD = os.getcwd()
+
+@contextlib.contextmanager
+def temp_cwd(name='tempcwd', quiet=False, path=None):
+ """
+ Context manager that temporarily changes the CWD.
+
+ An existing path may be provided as *path*, in which case this
+ function makes no changes to the file system.
+
+ Otherwise, the new CWD is created in the current directory and it's
+ named *name*. If *quiet* is False (default) and it's not possible to
+ create or change the CWD, an error is raised. If it's True, only a
+ warning is raised and the original CWD is used.
+ """
+ saved_dir = os.getcwd()
+ is_temporary = False
+ if path is None:
+ path = name
+ try:
+ os.mkdir(name)
+ is_temporary = True
+ except OSError:
+ if not quiet:
+ raise
+ warnings.warn('tests may fail, unable to create temp CWD ' + name,
+ RuntimeWarning, stacklevel=3)
+ try:
+ os.chdir(path)
+ except OSError:
+ if not quiet:
+ raise
+ warnings.warn('tests may fail, unable to change the CWD to ' + name,
+ RuntimeWarning, stacklevel=3)
+ try:
+ yield os.getcwd()
+ finally:
+ os.chdir(saved_dir)
+ if is_temporary:
+ rmtree(name)
+
+
+if hasattr(os, "umask"):
+ @contextlib.contextmanager
+ def temp_umask(umask):
+ """Context manager that temporarily sets the process umask."""
+ oldmask = os.umask(umask)
+ try:
+ yield
+ finally:
+ os.umask(oldmask)
+
+
+def findfile(file, here=__file__, subdir=None):
+ """Try to find a file on sys.path and the working directory. If it is not
+ found the argument passed to the function is returned (this does not
+ necessarily signal failure; could still be the legitimate path)."""
+ if os.path.isabs(file):
+ return file
+ if subdir is not None:
+ file = os.path.join(subdir, file)
+ path = sys.path
+ path = [os.path.dirname(here)] + path
+ for dn in path:
+ fn = os.path.join(dn, file)
+ if os.path.exists(fn): return fn
+ return file
+
+def create_empty_file(filename):
+ """Create an empty file. If the file already exists, truncate it."""
+ fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
+ os.close(fd)
+
+def sortdict(dict):
+ "Like repr(dict), but in sorted order."
+ items = sorted(dict.items())
+ reprpairs = ["%r: %r" % pair for pair in items]
+ withcommas = ", ".join(reprpairs)
+ return "{%s}" % withcommas
+
+def make_bad_fd():
+ """
+ Create an invalid file descriptor by opening and closing a file and return
+ its fd.
+ """
+ file = open(TESTFN, "wb")
+ try:
+ return file.fileno()
+ finally:
+ file.close()
+ unlink(TESTFN)
+
+def check_syntax_error(testcase, statement):
+ testcase.assertRaises(SyntaxError, compile, statement,
+ '<test string>', 'exec')
+
+def open_urlresource(url, *args, **kw):
+ import urllib.request, urllib.parse
+
+ check = kw.pop('check', None)
+
+ filename = urllib.parse.urlparse(url)[2].split('/')[-1] # '/': it's URL!
+
+ fn = os.path.join(os.path.dirname(__file__), "data", filename)
+
+ def check_valid_file(fn):
+ f = open(fn, *args, **kw)
+ if check is None:
+ return f
+ elif check(f):
+ f.seek(0)
+ return f
+ f.close()
+
+ if os.path.exists(fn):
+ f = check_valid_file(fn)
+ if f is not None:
+ return f
+ unlink(fn)
+
+ # Verify the requirement before downloading the file
+ requires('urlfetch')
+
+ print('\tfetching %s ...' % url, file=get_original_stdout())
+ f = urllib.request.urlopen(url, timeout=15)
+ try:
+ with open(fn, "wb") as out:
+ s = f.read()
+ while s:
+ out.write(s)
+ s = f.read()
+ finally:
+ f.close()
+
+ f = check_valid_file(fn)
+ if f is not None:
+ return f
+ raise TestFailed('invalid resource %r' % fn)
+
+
+class WarningsRecorder(object):
+ """Convenience wrapper for the warnings list returned on
+ entry to the warnings.catch_warnings() context manager.
+ """
+ def __init__(self, warnings_list):
+ self._warnings = warnings_list
+ self._last = 0
+
+ def __getattr__(self, attr):
+ if len(self._warnings) > self._last:
+ return getattr(self._warnings[-1], attr)
+ elif attr in warnings.WarningMessage._WARNING_DETAILS:
+ return None
+ raise AttributeError("%r has no attribute %r" % (self, attr))
+
+ @property
+ def warnings(self):
+ return self._warnings[self._last:]
+
+ def reset(self):
+ self._last = len(self._warnings)
+
+
+def _filterwarnings(filters, quiet=False):
+ """Catch the warnings, then check if all the expected
+ warnings have been raised and re-raise unexpected warnings.
+ If 'quiet' is True, only re-raise the unexpected warnings.
+ """
+ # Clear the warning registry of the calling module
+ # in order to re-raise the warnings.
+ frame = sys._getframe(2)
+ registry = frame.f_globals.get('__warningregistry__')
+ if registry:
+ registry.clear()
+ with warnings.catch_warnings(record=True) as w:
+ # Set filter "always" to record all warnings. Because
+ # test_warnings swap the module, we need to look up in
+ # the sys.modules dictionary.
+ sys.modules['warnings'].simplefilter("always")
+ yield WarningsRecorder(w)
+ # Filter the recorded warnings
+ reraise = list(w)
+ missing = []
+ for msg, cat in filters:
+ seen = False
+ for w in reraise[:]:
+ warning = w.message
+ # Filter out the matching messages
+ if (re.match(msg, str(warning), re.I) and
+ issubclass(warning.__class__, cat)):
+ seen = True
+ reraise.remove(w)
+ if not seen and not quiet:
+ # This filter caught nothing
+ missing.append((msg, cat.__name__))
+ if reraise:
+ raise AssertionError("unhandled warning %s" % reraise[0])
+ if missing:
+ raise AssertionError("filter (%r, %s) did not catch any warning" %
+ missing[0])
+
+
+@contextlib.contextmanager
+def check_warnings(*filters, **kwargs):
+ """Context manager to silence warnings.
+
+ Accept 2-tuples as positional arguments:
+ ("message regexp", WarningCategory)
+
+ Optional argument:
+ - if 'quiet' is True, it does not fail if a filter catches nothing
+ (default True without argument,
+ default False if some filters are defined)
+
+ Without argument, it defaults to:
+ check_warnings(("", Warning), quiet=True)
+ """
+ quiet = kwargs.get('quiet')
+ if not filters:
+ filters = (("", Warning),)
+ # Preserve backward compatibility
+ if quiet is None:
+ quiet = True
+ return _filterwarnings(filters, quiet)
+
+
+class CleanImport(object):
+ """Context manager to force import to return a new module reference.
+
+ This is useful for testing module-level behaviours, such as
+ the emission of a DeprecationWarning on import.
+
+ Use like this:
+
+ with CleanImport("foo"):
+ importlib.import_module("foo") # new reference
+ """
+
+ def __init__(self, *module_names):
+ self.original_modules = sys.modules.copy()
+ for module_name in module_names:
+ if module_name in sys.modules:
+ module = sys.modules[module_name]
+ # It is possible that module_name is just an alias for
+ # another module (e.g. stub for modules renamed in 3.x).
+ # In that case, we also need delete the real module to clear
+ # the import cache.
+ if module.__name__ != module_name:
+ del sys.modules[module.__name__]
+ del sys.modules[module_name]
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *ignore_exc):
+ sys.modules.update(self.original_modules)
+
+
+class EnvironmentVarGuard(collections.abc.MutableMapping):
+
+ """Class to help protect the environment variable properly. Can be used as
+ a context manager."""
+
+ def __init__(self):
+ self._environ = os.environ
+ self._changed = {}
+
+ def __getitem__(self, envvar):
+ return self._environ[envvar]
+
+ def __setitem__(self, envvar, value):
+ # Remember the initial value on the first access
+ if envvar not in self._changed:
+ self._changed[envvar] = self._environ.get(envvar)
+ self._environ[envvar] = value
+
+ def __delitem__(self, envvar):
+ # Remember the initial value on the first access
+ if envvar not in self._changed:
+ self._changed[envvar] = self._environ.get(envvar)
+ if envvar in self._environ:
+ del self._environ[envvar]
+
+ def keys(self):
+ return self._environ.keys()
+
+ def __iter__(self):
+ return iter(self._environ)
+
+ def __len__(self):
+ return len(self._environ)
+
+ def set(self, envvar, value):
+ self[envvar] = value
+
+ def unset(self, envvar):
+ del self[envvar]
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *ignore_exc):
+ for (k, v) in self._changed.items():
+ if v is None:
+ if k in self._environ:
+ del self._environ[k]
+ else:
+ self._environ[k] = v
+ os.environ = self._environ
+
+
+class DirsOnSysPath(object):
+ """Context manager to temporarily add directories to sys.path.
+
+ This makes a copy of sys.path, appends any directories given
+ as positional arguments, then reverts sys.path to the copied
+ settings when the context ends.
+
+ Note that *all* sys.path modifications in the body of the
+ context manager, including replacement of the object,
+ will be reverted at the end of the block.
+ """
+
+ def __init__(self, *paths):
+ self.original_value = sys.path[:]
+ self.original_object = sys.path
+ sys.path.extend(paths)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *ignore_exc):
+ sys.path = self.original_object
+ sys.path[:] = self.original_value
+
+
+class TransientResource(object):
+
+ """Raise ResourceDenied if an exception is raised while the context manager
+ is in effect that matches the specified exception and attributes."""
+
+ def __init__(self, exc, **kwargs):
+ self.exc = exc
+ self.attrs = kwargs
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type_=None, value=None, traceback=None):
+ """If type_ is a subclass of self.exc and value has attributes matching
+ self.attrs, raise ResourceDenied. Otherwise let the exception
+ propagate (if any)."""
+ if type_ is not None and issubclass(self.exc, type_):
+ for attr, attr_value in self.attrs.items():
+ if not hasattr(value, attr):
+ break
+ if getattr(value, attr) != attr_value:
+ break
+ else:
+ raise ResourceDenied("an optional resource is not available")
+
+# Context managers that raise ResourceDenied when various issues
+# with the Internet connection manifest themselves as exceptions.
+# XXX deprecate these and use transient_internet() instead
+time_out = TransientResource(IOError, errno=errno.ETIMEDOUT)
+socket_peer_reset = TransientResource(socket.error, errno=errno.ECONNRESET)
+ioerror_peer_reset = TransientResource(IOError, errno=errno.ECONNRESET)
+
+
+@contextlib.contextmanager
+def transient_internet(resource_name, *, timeout=30.0, errnos=()):
+ """Return a context manager that raises ResourceDenied when various issues
+ with the Internet connection manifest themselves as exceptions."""
+ default_errnos = [
+ ('ECONNREFUSED', 111),
+ ('ECONNRESET', 104),
+ ('EHOSTUNREACH', 113),
+ ('ENETUNREACH', 101),
+ ('ETIMEDOUT', 110),
+ ]
+ default_gai_errnos = [
+ ('EAI_AGAIN', -3),
+ ('EAI_FAIL', -4),
+ ('EAI_NONAME', -2),
+ ('EAI_NODATA', -5),
+ # Encountered when trying to resolve IPv6-only hostnames
+ ('WSANO_DATA', 11004),
+ ]
+
+ denied = ResourceDenied("Resource %r is not available" % resource_name)
+ captured_errnos = errnos
+ gai_errnos = []
+ if not captured_errnos:
+ captured_errnos = [getattr(errno, name, num)
+ for (name, num) in default_errnos]
+ gai_errnos = [getattr(socket, name, num)
+ for (name, num) in default_gai_errnos]
+
+ def filter_error(err):
+ n = getattr(err, 'errno', None)
+ if (isinstance(err, socket.timeout) or
+ (isinstance(err, socket.gaierror) and n in gai_errnos) or
+ n in captured_errnos):
+ if not verbose:
+ sys.stderr.write(denied.args[0] + "\n")
+ raise denied from err
+
+ old_timeout = socket.getdefaulttimeout()
+ try:
+ if timeout is not None:
+ socket.setdefaulttimeout(timeout)
+ yield
+ except IOError as err:
+ # urllib can wrap original socket errors multiple times (!), we must
+ # unwrap to get at the original error.
+ while True:
+ a = err.args
+ if len(a) >= 1 and isinstance(a[0], IOError):
+ err = a[0]
+ # The error can also be wrapped as args[1]:
+ # except socket.error as msg:
+ # raise IOError('socket error', msg).with_traceback(sys.exc_info()[2])
+ elif len(a) >= 2 and isinstance(a[1], IOError):
+ err = a[1]
+ else:
+ break
+ filter_error(err)
+ raise
+ # XXX should we catch generic exceptions and look for their
+ # __cause__ or __context__?
+ finally:
+ socket.setdefaulttimeout(old_timeout)
+
+
+@contextlib.contextmanager
+def captured_output(stream_name):
+ """Return a context manager used by captured_stdout/stdin/stderr
+ that temporarily replaces the sys stream *stream_name* with a StringIO."""
+ import io
+ orig_stdout = getattr(sys, stream_name)
+ setattr(sys, stream_name, io.StringIO())
+ try:
+ yield getattr(sys, stream_name)
+ finally:
+ setattr(sys, stream_name, orig_stdout)
+
+def captured_stdout():
+ """Capture the output of sys.stdout:
+
+ with captured_stdout() as s:
+ print("hello")
+ self.assertEqual(s.getvalue(), "hello")
+ """
+ return captured_output("stdout")
+
+def captured_stderr():
+ return captured_output("stderr")
+
+def captured_stdin():
+ return captured_output("stdin")
+
+
+def gc_collect():
+ """Force as many objects as possible to be collected.
+
+ In non-CPython implementations of Python, this is needed because timely
+ deallocation is not guaranteed by the garbage collector. (Even in CPython
+ this can be the case in case of reference cycles.) This means that __del__
+ methods may be called later than expected and weakrefs may remain alive for
+ longer than expected. This function tries its best to force all garbage
+ objects to disappear.
+ """
+ gc.collect()
+ if is_jython:
+ time.sleep(0.1)
+ gc.collect()
+ gc.collect()
+
+@contextlib.contextmanager
+def disable_gc():
+ have_gc = gc.isenabled()
+ gc.disable()
+ try:
+ yield
+ finally:
+ if have_gc:
+ gc.enable()
+
+
+def python_is_optimized():
+ """Find if Python was built with optimizations."""
+ cflags = sysconfig.get_config_var('PY_CFLAGS') or ''
+ final_opt = ""
+ for opt in cflags.split():
+ if opt.startswith('-O'):
+ final_opt = opt
+ return final_opt != '' and final_opt != '-O0'
+
+
+#=======================================================================
+# Decorator for running a function in a different locale, correctly resetting
+# it afterwards.
+
+def run_with_locale(catstr, *locales):
+ def decorator(func):
+ def inner(*args, **kwds):
+ try:
+ import locale
+ category = getattr(locale, catstr)
+ orig_locale = locale.setlocale(category)
+ except AttributeError:
+ # if the test author gives us an invalid category string
+ raise
+ except:
+ # cannot retrieve original locale, so do nothing
+ locale = orig_locale = None
+ else:
+ for loc in locales:
+ try:
+ locale.setlocale(category, loc)
+ break
+ except:
+ pass
+
+ # now run the function, resetting the locale on exceptions
+ try:
+ return func(*args, **kwds)
+ finally:
+ if locale and orig_locale:
+ locale.setlocale(category, orig_locale)
+ inner.__name__ = func.__name__
+ inner.__doc__ = func.__doc__
+ return inner
+ return decorator
+
+#=======================================================================
+# Big-memory-test support. Separate from 'resources' because memory use
+# should be configurable.
+
+# Some handy shorthands. Note that these are used for byte-limits as well
+# as size-limits, in the various bigmem tests
+_1M = 1024*1024
+_1G = 1024 * _1M
+_2G = 2 * _1G
+_4G = 4 * _1G
+
+MAX_Py_ssize_t = sys.maxsize
+
+def set_memlimit(limit):
+ global max_memuse
+ global real_max_memuse
+ sizes = {
+ 'k': 1024,
+ 'm': _1M,
+ 'g': _1G,
+ 't': 1024*_1G,
+ }
+ m = re.match(r'(\d+(\.\d+)?) (K|M|G|T)b?$', limit,
+ re.IGNORECASE | re.VERBOSE)
+ if m is None:
+ raise ValueError('Invalid memory limit %r' % (limit,))
+ memlimit = int(float(m.group(1)) * sizes[m.group(3).lower()])
+ real_max_memuse = memlimit
+ if memlimit > MAX_Py_ssize_t:
+ memlimit = MAX_Py_ssize_t
+ if memlimit < _2G - 1:
+ raise ValueError('Memory limit %r too low to be useful' % (limit,))
+ max_memuse = memlimit
+
+class _MemoryWatchdog:
+ """An object which periodically watches the process' memory consumption
+ and prints it out.
+ """
+
+ def __init__(self):
+ self.procfile = '/proc/{pid}/statm'.format(pid=os.getpid())
+ self.started = False
+ self.thread = None
+ try:
+ self.page_size = os.sysconf('SC_PAGESIZE')
+ except (ValueError, AttributeError):
+ try:
+ self.page_size = os.sysconf('SC_PAGE_SIZE')
+ except (ValueError, AttributeError):
+ self.page_size = 4096
+
+ def consumer(self, fd):
+ HEADER = "l"
+ header_size = struct.calcsize(HEADER)
+ try:
+ while True:
+ header = os.read(fd, header_size)
+ if len(header) < header_size:
+ # Pipe closed on other end
+ break
+ data_len, = struct.unpack(HEADER, header)
+ data = os.read(fd, data_len)
+ statm = data.decode('ascii')
+ data = int(statm.split()[5])
+ print(" ... process data size: {data:.1f}G"
+ .format(data=data * self.page_size / (1024 ** 3)))
+ finally:
+ os.close(fd)
+
+ def start(self):
+ if not faulthandler or not hasattr(faulthandler, '_file_watchdog'):
+ return
+ try:
+ rfd = os.open(self.procfile, os.O_RDONLY)
+ except OSError as e:
+ warnings.warn('/proc not available for stats: {}'.format(e),
+ RuntimeWarning)
+ sys.stderr.flush()
+ return
+ pipe_fd, wfd = os.pipe()
+ # _file_watchdog() doesn't take the GIL in its child thread, and
+ # therefore collects statistics timely
+ faulthandler._file_watchdog(rfd, wfd, 1.0)
+ self.started = True
+ self.thread = threading.Thread(target=self.consumer, args=(pipe_fd,))
+ self.thread.daemon = True
+ self.thread.start()
+
+ def stop(self):
+ if not self.started:
+ return
+ faulthandler._cancel_file_watchdog()
+ self.thread.join()
+
+
+def bigmemtest(size, memuse, dry_run=True):
+ """Decorator for bigmem tests.
+
+ 'minsize' is the minimum useful size for the test (in arbitrary,
+ test-interpreted units.) 'memuse' is the number of 'bytes per size' for
+ the test, or a good estimate of it.
+
+ if 'dry_run' is False, it means the test doesn't support dummy runs
+ when -M is not specified.
+ """
+ def decorator(f):
+ def wrapper(self):
+ size = wrapper.size
+ memuse = wrapper.memuse
+ if not real_max_memuse:
+ maxsize = 5147
+ else:
+ maxsize = size
+
+ if ((real_max_memuse or not dry_run)
+ and real_max_memuse < maxsize * memuse):
+ raise unittest.SkipTest(
+ "not enough memory: %.1fG minimum needed"
+ % (size * memuse / (1024 ** 3)))
+
+ if real_max_memuse and verbose and faulthandler and threading:
+ print()
+ print(" ... expected peak memory use: {peak:.1f}G"
+ .format(peak=size * memuse / (1024 ** 3)))
+ watchdog = _MemoryWatchdog()
+ watchdog.start()
+ else:
+ watchdog = None
+
+ try:
+ return f(self, maxsize)
+ finally:
+ if watchdog:
+ watchdog.stop()
+
+ wrapper.size = size
+ wrapper.memuse = memuse
+ return wrapper
+ return decorator
+
+def bigaddrspacetest(f):
+ """Decorator for tests that fill the address space."""
+ def wrapper(self):
+ if max_memuse < MAX_Py_ssize_t:
+ if MAX_Py_ssize_t >= 2**63 - 1 and max_memuse >= 2**31:
+ raise unittest.SkipTest(
+ "not enough memory: try a 32-bit build instead")
+ else:
+ raise unittest.SkipTest(
+ "not enough memory: %.1fG minimum needed"
+ % (MAX_Py_ssize_t / (1024 ** 3)))
+ else:
+ return f(self)
+ return wrapper
+
+#=======================================================================
+# unittest integration.
+
+class BasicTestRunner:
+ def run(self, test):
+ result = unittest.TestResult()
+ test(result)
+ return result
+
+def _id(obj):
+ return obj
+
+def requires_resource(resource):
+ if resource == 'gui' and not _is_gui_available():
+ return unittest.skip("resource 'gui' is not available")
+ if is_resource_enabled(resource):
+ return _id
+ else:
+ return unittest.skip("resource {0!r} is not enabled".format(resource))
+
+def cpython_only(test):
+ """
+ Decorator for tests only applicable on CPython.
+ """
+ return impl_detail(cpython=True)(test)
+
+def impl_detail(msg=None, **guards):
+ if check_impl_detail(**guards):
+ return _id
+ if msg is None:
+ guardnames, default = _parse_guards(guards)
+ if default:
+ msg = "implementation detail not available on {0}"
+ else:
+ msg = "implementation detail specific to {0}"
+ guardnames = sorted(guardnames.keys())
+ msg = msg.format(' or '.join(guardnames))
+ return unittest.skip(msg)
+
+def _parse_guards(guards):
+ # Returns a tuple ({platform_name: run_me}, default_value)
+ if not guards:
+ return ({'cpython': True}, False)
+ is_true = list(guards.values())[0]
+ assert list(guards.values()) == [is_true] * len(guards) # all True or all False
+ return (guards, not is_true)
+
+# Use the following check to guard CPython's implementation-specific tests --
+# or to run them only on the implementation(s) guarded by the arguments.
+def check_impl_detail(**guards):
+ """This function returns True or False depending on the host platform.
+ Examples:
+ if check_impl_detail(): # only on CPython (default)
+ if check_impl_detail(jython=True): # only on Jython
+ if check_impl_detail(cpython=False): # everywhere except on CPython
+ """
+ guards, default = _parse_guards(guards)
+ return guards.get(platform.python_implementation().lower(), default)
+
+
+def no_tracing(func):
+ """Decorator to temporarily turn off tracing for the duration of a test."""
+ if not hasattr(sys, 'gettrace'):
+ return func
+ else:
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ original_trace = sys.gettrace()
+ try:
+ sys.settrace(None)
+ return func(*args, **kwargs)
+ finally:
+ sys.settrace(original_trace)
+ return wrapper
+
+
+def refcount_test(test):
+ """Decorator for tests which involve reference counting.
+
+ To start, the decorator does not run the test if is not run by CPython.
+ After that, any trace function is unset during the test to prevent
+ unexpected refcounts caused by the trace function.
+
+ """
+ return no_tracing(cpython_only(test))
+
+
+def _filter_suite(suite, pred):
+ """Recursively filter test cases in a suite based on a predicate."""
+ newtests = []
+ for test in suite._tests:
+ if isinstance(test, unittest.TestSuite):
+ _filter_suite(test, pred)
+ newtests.append(test)
+ else:
+ if pred(test):
+ newtests.append(test)
+ suite._tests = newtests
+
+def _run_suite(suite):
+ """Run tests from a unittest.TestSuite-derived class."""
+ if verbose:
+ runner = unittest.TextTestRunner(sys.stdout, verbosity=2,
+ failfast=failfast)
+ else:
+ runner = BasicTestRunner()
+
+ result = runner.run(suite)
+ if not result.wasSuccessful():
+ if len(result.errors) == 1 and not result.failures:
+ err = result.errors[0][1]
+ elif len(result.failures) == 1 and not result.errors:
+ err = result.failures[0][1]
+ else:
+ err = "multiple errors occurred"
+ if not verbose: err += "; run in verbose mode for details"
+ raise TestFailed(err)
+
+
+def run_unittest(*classes):
+ """Run tests from unittest.TestCase-derived classes."""
+ valid_types = (unittest.TestSuite, unittest.TestCase)
+ suite = unittest.TestSuite()
+ for cls in classes:
+ if isinstance(cls, str):
+ if cls in sys.modules:
+ suite.addTest(unittest.findTestCases(sys.modules[cls]))
+ else:
+ raise ValueError("str arguments must be keys in sys.modules")
+ elif isinstance(cls, valid_types):
+ suite.addTest(cls)
+ else:
+ suite.addTest(unittest.makeSuite(cls))
+ def case_pred(test):
+ if match_tests is None:
+ return True
+ for name in test.id().split("."):
+ if fnmatch.fnmatchcase(name, match_tests):
+ return True
+ return False
+ _filter_suite(suite, case_pred)
+ _run_suite(suite)
+
+
+#=======================================================================
+# doctest driver.
+
+def run_doctest(module, verbosity=None):
+ """Run doctest on the given module. Return (#failures, #tests).
+
+ If optional argument verbosity is not specified (or is None), pass
+ support's belief about verbosity on to doctest. Else doctest's
+ usual behavior is used (it searches sys.argv for -v).
+ """
+
+ import doctest
+
+ if verbosity is None:
+ verbosity = verbose
+ else:
+ verbosity = None
+
+ f, t = doctest.testmod(module, verbose=verbosity)
+ if f:
+ raise TestFailed("%d of %d doctests failed" % (f, t))
+ if verbose:
+ print('doctest (%s) ... %d tests with zero failures' %
+ (module.__name__, t))
+ return f, t
+
+
+#=======================================================================
+# Support for saving and restoring the imported modules.
+
+def modules_setup():
+ return sys.modules.copy(),
+
+def modules_cleanup(oldmodules):
+ # Encoders/decoders are registered permanently within the internal
+ # codec cache. If we destroy the corresponding modules their
+ # globals will be set to None which will trip up the cached functions.
+ encodings = [(k, v) for k, v in sys.modules.items()
+ if k.startswith('encodings.')]
+ sys.modules.clear()
+ sys.modules.update(encodings)
+ # XXX: This kind of problem can affect more than just encodings. In particular
+ # extension modules (such as _ssl) don't cope with reloading properly.
+ # Really, test modules should be cleaning out the test specific modules they
+ # know they added (ala test_runpy) rather than relying on this function (as
+ # test_importhooks and test_pkg do currently).
+ # Implicitly imported *real* modules should be left alone (see issue 10556).
+ sys.modules.update(oldmodules)
+
+#=======================================================================
+# Threading support to prevent reporting refleaks when running regrtest.py -R
+
+# NOTE: we use thread._count() rather than threading.enumerate() (or the
+# moral equivalent thereof) because a threading.Thread object is still alive
+# until its __bootstrap() method has returned, even after it has been
+# unregistered from the threading module.
+# thread._count(), on the other hand, only gets decremented *after* the
+# __bootstrap() method has returned, which gives us reliable reference counts
+# at the end of a test run.
+
+def threading_setup():
+ if _thread:
+ return _thread._count(), threading._dangling.copy()
+ else:
+ return 1, ()
+
+def threading_cleanup(*original_values):
+ if not _thread:
+ return
+ _MAX_COUNT = 10
+ for count in range(_MAX_COUNT):
+ values = _thread._count(), threading._dangling
+ if values == original_values:
+ break
+ time.sleep(0.1)
+ gc_collect()
+ # XXX print a warning in case of failure?
+
+def reap_threads(func):
+ """Use this function when threads are being used. This will
+ ensure that the threads are cleaned up even when the test fails.
+ If threading is unavailable this function does nothing.
+ """
+ if not _thread:
+ return func
+
+ @functools.wraps(func)
+ def decorator(*args):
+ key = threading_setup()
+ try:
+ return func(*args)
+ finally:
+ threading_cleanup(*key)
+ return decorator
+
+def reap_children():
+ """Use this function at the end of test_main() whenever sub-processes
+ are started. This will help ensure that no extra children (zombies)
+ stick around to hog resources and create problems when looking
+ for refleaks.
+ """
+
+ # Reap all our dead child processes so we don't leave zombies around.
+ # These hog resources and might be causing some of the buildbots to die.
+ if hasattr(os, 'waitpid'):
+ any_process = -1
+ while True:
+ try:
+ # This will raise an exception on Windows. That's ok.
+ pid, status = os.waitpid(any_process, os.WNOHANG)
+ if pid == 0:
+ break
+ except:
+ break
+
+@contextlib.contextmanager
+def swap_attr(obj, attr, new_val):
+ """Temporary swap out an attribute with a new object.
+
+ Usage:
+ with swap_attr(obj, "attr", 5):
+ ...
+
+ This will set obj.attr to 5 for the duration of the with: block,
+ restoring the old value at the end of the block. If `attr` doesn't
+ exist on `obj`, it will be created and then deleted at the end of the
+ block.
+ """
+ if hasattr(obj, attr):
+ real_val = getattr(obj, attr)
+ setattr(obj, attr, new_val)
+ try:
+ yield
+ finally:
+ setattr(obj, attr, real_val)
+ else:
+ setattr(obj, attr, new_val)
+ try:
+ yield
+ finally:
+ delattr(obj, attr)
+
+@contextlib.contextmanager
+def swap_item(obj, item, new_val):
+ """Temporary swap out an item with a new object.
+
+ Usage:
+ with swap_item(obj, "item", 5):
+ ...
+
+ This will set obj["item"] to 5 for the duration of the with: block,
+ restoring the old value at the end of the block. If `item` doesn't
+ exist on `obj`, it will be created and then deleted at the end of the
+ block.
+ """
+ if item in obj:
+ real_val = obj[item]
+ obj[item] = new_val
+ try:
+ yield
+ finally:
+ obj[item] = real_val
+ else:
+ obj[item] = new_val
+ try:
+ yield
+ finally:
+ del obj[item]
+
+def strip_python_stderr(stderr):
+ """Strip the stderr of a Python process from potential debug output
+ emitted by the interpreter.
+
+ This will typically be run on the result of the communicate() method
+ of a subprocess.Popen object.
+ """
+ stderr = re.sub(br"\[\d+ refs\]\r?\n?$", b"", stderr).strip()
+ return stderr
+
+def args_from_interpreter_flags():
+ """Return a list of command-line arguments reproducing the current
+ settings in sys.flags and sys.warnoptions."""
+ flag_opt_map = {
+ 'bytes_warning': 'b',
+ 'dont_write_bytecode': 'B',
+ 'ignore_environment': 'E',
+ 'no_user_site': 's',
+ 'no_site': 'S',
+ 'optimize': 'O',
+ 'verbose': 'v',
+ }
+ args = []
+ for flag, opt in flag_opt_map.items():
+ v = getattr(sys.flags, flag)
+ if v > 0:
+ args.append('-' + opt * v)
+ for opt in sys.warnoptions:
+ args.append('-W' + opt)
+ return args
+
+#============================================================
+# Support for assertions about logging.
+#============================================================
+
+class TestHandler(logging.handlers.BufferingHandler):
+ def __init__(self, matcher):
+ # BufferingHandler takes a "capacity" argument
+ # so as to know when to flush. As we're overriding
+ # shouldFlush anyway, we can set a capacity of zero.
+ # You can call flush() manually to clear out the
+ # buffer.
+ logging.handlers.BufferingHandler.__init__(self, 0)
+ self.matcher = matcher
+
+ def shouldFlush(self):
+ return False
+
+ def emit(self, record):
+ self.format(record)
+ self.buffer.append(record.__dict__)
+
+ def matches(self, **kwargs):
+ """
+ Look for a saved dict whose keys/values match the supplied arguments.
+ """
+ result = False
+ for d in self.buffer:
+ if self.matcher.matches(d, **kwargs):
+ result = True
+ break
+ return result
+
+class Matcher(object):
+
+ _partial_matches = ('msg', 'message')
+
+ def matches(self, d, **kwargs):
+ """
+ Try to match a single dict with the supplied arguments.
+
+ Keys whose values are strings and which are in self._partial_matches
+ will be checked for partial (i.e. substring) matches. You can extend
+ this scheme to (for example) do regular expression matching, etc.
+ """
+ result = True
+ for k in kwargs:
+ v = kwargs[k]
+ dv = d.get(k)
+ if not self.match_value(k, dv, v):
+ result = False
+ break
+ return result
+
+ def match_value(self, k, dv, v):
+ """
+ Try to match a single stored value (dv) with a supplied value (v).
+ """
+ if type(v) != type(dv):
+ result = False
+ elif type(dv) is not str or k not in self._partial_matches:
+ result = (v == dv)
+ else:
+ result = dv.find(v) >= 0
+ return result
+
+
+_can_symlink = None
+def can_symlink():
+ global _can_symlink
+ if _can_symlink is not None:
+ return _can_symlink
+ symlink_path = TESTFN + "can_symlink"
+ try:
+ os.symlink(TESTFN, symlink_path)
+ can = True
+ except (OSError, NotImplementedError, AttributeError):
+ can = False
+ else:
+ os.remove(symlink_path)
+ _can_symlink = can
+ return can
+
+def skip_unless_symlink(test):
+ """Skip decorator for tests that require functional symlink"""
+ ok = can_symlink()
+ msg = "Requires functional symlink implementation"
+ return test if ok else unittest.skip(msg)(test)
+
+def patch(test_instance, object_to_patch, attr_name, new_value):
+ """Override 'object_to_patch'.'attr_name' with 'new_value'.
+
+ Also, add a cleanup procedure to 'test_instance' to restore
+ 'object_to_patch' value for 'attr_name'.
+ The 'attr_name' should be a valid attribute for 'object_to_patch'.
+
+ """
+ # check that 'attr_name' is a real attribute for 'object_to_patch'
+ # will raise AttributeError if it does not exist
+ getattr(object_to_patch, attr_name)
+
+ # keep a copy of the old value
+ attr_is_local = False
+ try:
+ old_value = object_to_patch.__dict__[attr_name]
+ except (AttributeError, KeyError):
+ old_value = getattr(object_to_patch, attr_name, None)
+ else:
+ attr_is_local = True
+
+ # restore the value when the test is done
+ def cleanup():
+ if attr_is_local:
+ setattr(object_to_patch, attr_name, old_value)
+ else:
+ delattr(object_to_patch, attr_name)
+
+ test_instance.addCleanup(cleanup)
+
+ # actually override the attribute
+ setattr(object_to_patch, attr_name, new_value)
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
new file mode 100644
index 0000000..0f2b08a
--- /dev/null
+++ b/tests/test_argparse.py
@@ -0,0 +1,26 @@
+from argparse import Namespace
+import unittest
+
+from eip_client.utils import eip_argparse
+
+
+class EIPArgParseTest(unittest.TestCase):
+ """
+ Test argparse options for eip client
+ """
+
+ def setUp(self):
+ """
+ get the parser
+ """
+ self.parser = eip_argparse.build_parser()
+
+ def test_debug_mode(self):
+ """
+ test debug mode option
+ """
+ opts = self.parser.parse_args(
+ ['--debug'])
+ self.assertEqual(opts,
+ Namespace(config=None,
+ debug=True))
diff --git a/tests/test_conductor.py b/tests/test_conductor.py
new file mode 100644
index 0000000..c8e5d54
--- /dev/null
+++ b/tests/test_conductor.py
@@ -0,0 +1,8 @@
+
+# Ideas for testing conductor:
+# - test_process_spawning
+# - test_process_watching_pipe
+# - test_process_output_callback
+
+# - test_status_change
+# - test_status_change_callback
diff --git a/tests/test_mainwindow.py b/tests/test_mainwindow.py
new file mode 100644
index 0000000..323a257
--- /dev/null
+++ b/tests/test_mainwindow.py
@@ -0,0 +1,150 @@
+# vim: set fileencoding=utf-8 :
+from argparse import Namespace
+import logging
+logger = logging.getLogger(name=__name__)
+
+import sys
+import unittest
+
+# black magic XXX ??
+import sip
+sip.setapi('QVariant', 2)
+
+from PyQt4 import QtGui
+from PyQt4.QtTest import QTest
+from PyQt4.QtCore import Qt
+
+from eip_client import eipcmainwindow, conductor
+
+
+class MainWindowTest(unittest.TestCase):
+ """
+ Test our mainwindow GUI
+ """
+
+ ##################################################
+ # FIXME
+ # To be moved to BaseEIPTestCase
+
+ def setUp(self):
+ '''Create the GUI'''
+ self.app = QtGui.QApplication(sys.argv)
+ opts = Namespace(config=None,
+ debug=False)
+ self.win = eipcmainwindow.EIPCMainWindow(opts)
+
+ def tearDown(self):
+ """
+ cleanup
+ """
+ # we have to delete references, otherwise
+ # we get nice segfaults :)
+ del(self.win)
+ del(self.app)
+
+ ##################################################
+
+ def test_system_has_systray(self):
+ """
+ does this system has a systray?
+ not the application response to that.
+ """
+ self.assertEqual(
+ self.win.trayIcon.isSystemTrayAvailable(),
+ True)
+
+ def test_defaults(self):
+ """
+ test that the defaults are those expected
+ """
+ self.assertEqual(self.win.windowTitle(), "EIP")
+ #self.assertEqual(self.win.durationSpinBox.value(), 15)
+ #logger.debug('durationSpinBox: %s' % self.win.durationSpinBox.value())
+
+ def test_main_window_has_conductor_instance(self):
+ """
+ test main window instantiates conductor class
+ """
+ self.assertEqual(hasattr(self.win, 'conductor'), True)
+ self.assertEqual(isinstance(self.win.conductor,
+ conductor.EIPConductor), True)
+
+ # Let's roll... let's test serious things
+ # ... better to have a different TestCase for this?
+ # plan is:
+ # 1) we signal to the app that we are running from the
+ # testrunner -- so it knows, just in case :P
+ # 2) we init the conductor with the default-for-testrunner
+ # options -- like getting a fake-output client script
+ # that mocks openvpn output to stdout.
+ # 3) we check that the important things work as they
+ # expected for the output of the binaries.
+ # XXX TODO:
+ # get generic helper methods for the base testcase class.
+ # mock_good_output
+ # mock_bad_output
+ # check_status
+
+ def test_connected_status_good_output(self):
+ """
+ check we get 'connected' state after mocked \
+good output from the fake openvpn process.
+ """
+ self.mock_good_output()
+ # wait?
+ self.check_state('connected')
+
+ def test_unrecoverable_status_bad_output(self):
+ """
+ check we get 'unrecoverable' state after
+ mocked bad output from the fake openvpn process.
+ """
+ self.mock_bad_output()
+ self.check_state('unrecoverable')
+
+ def test_icon_reflects_state(self):
+ """
+ test that the icon changes after an injection
+ of a change-of-state event.
+ """
+ self.mock_status_change('connected')
+ # icon == connectedIcon
+ # examine: QSystemtrayIcon.MessageIcon ??
+ self.mock_status_change('disconnected')
+ # ico == disconnectedIcon
+ self.mock_status_change('connecting')
+ # icon == connectingIcon
+
+ def test_status_signals_are_working(self):
+ """
+ test that status-change signals are being triggered
+ """
+ #???
+ pass
+
+
+ # sample tests below... to be removed
+
+ #def test_show_message_button_does_show_message(self):
+ #"""
+ #test that clicking on main window button shows message
+ #"""
+ # FIXME
+ #ok_show = self.win.showMessageButton
+ #trayIcon = self.win.trayIcon
+ # fake left click
+ #QTest.mouseClick(ok_show, Qt.LeftButton)
+ # how to assert that message has been shown?
+ #import ipdb;ipdb.set_trace()
+
+
+ #def test_do_fallback_if_not_systray(self):
+ #"""
+ #test that we do whatever we decide to do
+ #when we detect no systray.
+ #what happens with unity??
+ #"""
+ #pass
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_mgminterface.py b/tests/test_mgminterface.py
new file mode 100644
index 0000000..398b7f3
--- /dev/null
+++ b/tests/test_mgminterface.py
@@ -0,0 +1,333 @@
+import socket
+import select
+import telnetlib
+import contextlib
+
+from unittest import TestCase
+
+import support
+
+from eip_client.vpnmanager import OpenVPNManager
+
+HOST = "localhost"
+
+
+class SocketStub(object):
+ ''' a socket proxy that re-defines sendall() '''
+ def __init__(self, reads=[]):
+ self.reads = reads
+ self.writes = []
+ self.block = False
+
+ def sendall(self, data):
+ self.writes.append(data)
+
+ def recv(self, size):
+ out = b''
+ while self.reads and len(out) < size:
+ out += self.reads.pop(0)
+ #print(out)
+ if len(out) > size:
+ self.reads.insert(0, out[size:])
+ out = out[:size]
+ return out
+
+
+class TelnetAlike(telnetlib.Telnet):
+ def fileno(self):
+ raise NotImplementedError()
+
+ def close(self):
+ pass
+
+ def sock_avail(self):
+ return (not self.sock.block)
+
+ def msg(self, msg, *args):
+ with support.captured_stdout() as out:
+ telnetlib.Telnet.msg(self, msg, *args)
+ self._messages += out.getvalue()
+ return
+
+ def read_very_lazy(self):
+ self.fill_rawq()
+ _all = self.read_all()
+ print 'faking lazy:', _all
+ return _all
+
+
+def new_select(*s_args):
+ block = False
+ for l in s_args:
+ for fob in l:
+ if isinstance(fob, TelnetAlike):
+ block = fob.sock.block
+ if block:
+ return [[], [], []]
+ else:
+ return s_args
+
+
+@contextlib.contextmanager
+def test_socket(reads):
+ def new_conn(*ignored):
+ return SocketStub(reads)
+ try:
+ old_conn = socket.create_connection
+ socket.create_connection = new_conn
+ yield None
+ finally:
+ socket.create_connection = old_conn
+ return
+
+
+#
+# VPN Commands Dict
+#
+
+vpn_commands = {
+ 'status': [
+ 'OpenVPN STATISTICS', 'Updated,Mon Jun 25 11:51:21 2012',
+ 'TUN/TAP read bytes,306170', 'TUN/TAP write bytes,872102',
+ 'TCP/UDP read bytes,986177', 'TCP/UDP write bytes,439329',
+ 'Auth read bytes,872102'],
+ 'state': ['1340616463,CONNECTED,SUCCESS,172.28.0.2,198.252.153.38'],
+ # XXX add more tests
+ }
+
+
+class VPNManagementStub(TelnetAlike):
+ epilogue = "\nEND\n"
+
+ def write(self, data):
+ #print('data written')
+ data = data[:-1]
+ if data not in vpn_commands:
+ print('not in commands')
+ telnetlib.Telnet.write(self, data)
+ else:
+ msg = '\n'.join(vpn_commands[data]) + self.epilogue
+ print 'writing...'
+ print msg
+ for line in vpn_commands[data]:
+ self.sock.reads.append(line)
+ #telnetlib.Telnet.write(self, line)
+ self.sock.reads.append(self.epilogue)
+ #telnetlib.Telnet.write(self, self.epilogue)
+
+
+def test_telnet(reads=[], cls=VPNManagementStub):
+ ''' return a telnetlib.Telnet object that uses a SocketStub with
+ reads queued up to be read, and write method mocking a vpn
+ management interface'''
+ for x in reads:
+ assert type(x) is bytes, x
+ with test_socket(reads):
+ telnet = cls('dummy', 0)
+ telnet._messages = '' # debuglevel output
+ return telnet
+
+
+class ReadTests(TestCase):
+ def setUp(self):
+ self.old_select = select.select
+ select.select = new_select
+
+ def tearDown(self):
+ select.select = self.old_select
+
+ def test_read_until(self):
+ """
+ read_until(expected, timeout=None)
+ test the blocking version of read_util
+ """
+ want = [b'xxxmatchyyy']
+ telnet = test_telnet(want)
+ data = telnet.read_until(b'match')
+ self.assertEqual(data, b'xxxmatch', msg=(telnet.cookedq,
+ telnet.rawq, telnet.sock.reads))
+
+ reads = [b'x' * 50, b'match', b'y' * 50]
+ expect = b''.join(reads[:-1])
+ telnet = test_telnet(reads)
+ data = telnet.read_until(b'match')
+ self.assertEqual(data, expect)
+
+ def test_read_all(self):
+ """
+ read_all()
+ Read all data until EOF; may block.
+ """
+ reads = [b'x' * 500, b'y' * 500, b'z' * 500]
+ expect = b''.join(reads)
+ telnet = test_telnet(reads)
+ data = telnet.read_all()
+ self.assertEqual(data, expect)
+ return
+
+ def test_read_some(self):
+ """
+ read_some()
+ Read at least one byte or EOF; may block.
+ """
+ # test 'at least one byte'
+ telnet = test_telnet([b'x' * 500])
+ data = telnet.read_some()
+ self.assertTrue(len(data) >= 1)
+ # test EOF
+ telnet = test_telnet()
+ data = telnet.read_some()
+ self.assertEqual(b'', data)
+
+ def _read_eager(self, func_name):
+ """
+ read_*_eager()
+ Read all data available already queued or on the socket,
+ without blocking.
+ """
+ want = b'x' * 100
+ telnet = test_telnet([want])
+ func = getattr(telnet, func_name)
+ telnet.sock.block = True
+ self.assertEqual(b'', func())
+ telnet.sock.block = False
+ data = b''
+ while True:
+ try:
+ data += func()
+ except EOFError:
+ break
+ self.assertEqual(data, want)
+
+ def test_read_eager(self):
+ # read_eager and read_very_eager make the same gaurantees
+ # (they behave differently but we only test the gaurantees)
+ self._read_eager('read_eager')
+ self._read_eager('read_very_eager')
+ #self._read_eager('read_very_lazy')
+ # NB -- we need to test the IAC block which is mentioned in the
+ # docstring but not in the module docs
+
+ def read_very_lazy(self):
+ want = b'x' * 100
+ telnet = test_telnet([want])
+ self.assertEqual(b'', telnet.read_very_lazy())
+ while telnet.sock.reads:
+ telnet.fill_rawq()
+ data = telnet.read_very_lazy()
+ self.assertEqual(want, data)
+ self.assertRaises(EOFError, telnet.read_very_lazy)
+
+ def test_read_lazy(self):
+ want = b'x' * 100
+ telnet = test_telnet([want])
+ self.assertEqual(b'', telnet.read_lazy())
+ data = b''
+ while True:
+ try:
+ read_data = telnet.read_lazy()
+ data += read_data
+ if not read_data:
+ telnet.fill_rawq()
+ except EOFError:
+ break
+ self.assertTrue(want.startswith(data))
+ self.assertEqual(data, want)
+
+
+def _seek_to_eof(self):
+ """
+ Read as much as available. Position seek pointer to end of stream
+ """
+ #import ipdb;ipdb.set_trace()
+ while self.tn.sock.reads:
+ print 'reading...'
+ print 'and filling rawq'
+ self.tn.fill_rawq()
+ self.tn.process_rawq()
+ try:
+ b = self.tn.read_eager()
+ while b:
+ b = self.tn.read_eager()
+ except EOFError:
+ pass
+
+
+def connect_to_stub(self):
+ """
+ stub to be added to manager
+ """
+ try:
+ self.close()
+ except:
+ pass
+ if self.connected():
+ return True
+ self.tn = test_telnet()
+
+ self._seek_to_eof()
+ return True
+
+
+
+class VPNManagerTests(TestCase):
+
+ def setUp(self):
+ self.old_select = select.select
+ select.select = new_select
+
+ patched_manager = OpenVPNManager
+ patched_manager._seek_to_eof = _seek_to_eof
+ patched_manager.connect = connect_to_stub
+ self.manager = patched_manager()
+
+ def tearDown(self):
+ select.select = self.old_select
+
+ # tests
+
+
+ #def test_read_very_lazy(self):
+ #want = b'x' * 100
+ #telnet = test_telnet()
+ #self.assertEqual(b'', telnet.read_very_lazy())
+ #print 'writing to telnet'
+ #telnet.write('status\n')
+ #import ipdb;ipdb.set_trace()
+ #while telnet.sock.reads:
+ #print 'reading...'
+ #print 'and filling rawq'
+ #telnet.fill_rawq()
+ #import ipdb;ipdb.set_trace()
+ #data = telnet.read_very_lazy()
+ #print 'data ->', data
+
+ #def test_manager_status(self):
+ #buf = self.manager._send_command('state')
+ #import ipdb;ipdb.set_trace()
+ #print 'buf-->'
+ #print buf
+#
+ def test_manager_state(self):
+ buf = self.manager.state()
+ print 'buf-->'
+ print buf
+ import ipdb;ipdb.set_trace()
+
+ def test_command(self):
+ commands = [b'status']
+ for com in commands:
+ telnet = test_telnet()
+ telnet.write(com)
+ buf = telnet.read_until(b'END')
+ print 'buf '
+ print buf
+
+
+def test_main(verbose=None):
+ support.run_unittest(
+ #ReadTests,
+ VPNManagerTests)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/tests/test_vpn_management.py b/tests/test_vpn_management.py
new file mode 100644
index 0000000..d8a0314
--- /dev/null
+++ b/tests/test_vpn_management.py
@@ -0,0 +1,42 @@
+import unittest
+import sys
+import time
+
+from eip_client.mocks.manager import get_openvpn_manager_mocks
+
+
+class VPNManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.manager = get_openvpn_manager_mocks()
+
+ #
+ # tests
+ #
+
+ def test_status_command(self):
+ ret = self.manager.status()
+ #print ret
+
+ def test_connection_state(self):
+ ts, status, ok, ip, remote = self.manager.get_connection_state()
+ self.assertTrue(status in ('CONNECTED', 'DISCONNECTED'))
+ self.assertTrue(isinstance(ts, time.struct_time))
+
+ def test_status_io(self):
+ when_ts, counters = self.manager.get_status_io()
+ self.assertTrue(isinstance(when_ts, time.struct_time))
+ self.assertEqual(len(counters), 5)
+ self.assertTrue(all(map(lambda x: x.isdigit(), counters)))
+
+
+def test():
+ suite = unittest.TestSuite()
+ for cls in (VPNManagerTests,):
+ suite.addTest(unittest.makeSuite(cls))
+ runner = unittest.TextTestRunner(sys.stdout, verbosity=2,
+ failfast=False)
+ result = runner.run(suite)
+
+if __name__ == "__main__":
+ test()