diff options
82 files changed, 6729 insertions, 0 deletions
| diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3a0afe70 --- /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 00000000..e92537c0 --- /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 00000000..3ce64e45 --- /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 00000000..8f50f561 --- /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 00000000..14ac253f --- /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 00000000..580227ac --- /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.pngBinary files differ new file mode 100644 index 00000000..6a5bcba9 --- /dev/null +++ b/data/images/conn_connected.png diff --git a/data/images/conn_connecting.png b/data/images/conn_connecting.pngBinary files differ new file mode 100644 index 00000000..35cb0f6a --- /dev/null +++ b/data/images/conn_connecting.png diff --git a/data/images/conn_error.png b/data/images/conn_error.pngBinary files differ new file mode 100644 index 00000000..ac1391df --- /dev/null +++ b/data/images/conn_error.png diff --git a/data/images/leapfrog.jpg b/data/images/leapfrog.jpgBinary files differ new file mode 100644 index 00000000..a1ddf4bb --- /dev/null +++ b/data/images/leapfrog.jpg diff --git a/data/resources/mainwindow.qrc b/data/resources/mainwindow.qrc new file mode 100644 index 00000000..9a2531c9 --- /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 00000000..045d9700 --- /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 00000000..eae9eabd --- /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 00000000..41dd76f2 --- /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 00000000..45a4fb75 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..d4b38f25 --- /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 00000000..a5907f48 --- /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 00000000..e953f2c6 --- /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 00000000..1aed53a8 --- /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 00000000..3eec795a --- /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 00000000..0ac5a68c --- /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 00000000..e279eb11 --- /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 00000000..15e5320a --- /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 00000000..888928ca --- /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 00000000..5048c8e2 --- /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 00000000..8aeafcfe --- /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 00000000..19cc6ca1 --- /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 00000000..693a391e --- /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 00000000..c9704cfa --- /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 00000000..131c9f87 --- /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 00000000..e70c2917 --- /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 00000000..11edacf0 --- /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 00000000..8b32391c --- /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 00000000..5ebc7ff1 --- /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 00000000..80ba5c85 --- /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 00000000..ec47a76b --- /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 00000000..163aaf8d --- /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 00000000..791fbd7d --- /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 00000000..b7b5f53d --- /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 00000000..16aa258b --- /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 00000000..862a2f1f --- /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 00000000..fa42e6fd --- /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 00000000..aaa614bb --- /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 00000000..b241ea34 --- /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 00000000..b74adc06 --- /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 00000000..01bb9544 --- /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 00000000..2bda6b66 --- /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 00000000..7a6d39d9 --- /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 00000000..70a22b65 --- /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 00000000..e69de29b --- /dev/null +++ b/setup/requirements.pip diff --git a/setup/scripts/leap b/setup/scripts/leap new file mode 100755 index 00000000..6e62b597 --- /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 00000000..e4bc754e --- /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 00000000..05688ff1 --- /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 00000000..8b137891 --- /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 00000000..a184cd05 --- /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 00000000..8b137891 --- /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 00000000..2905ed7d --- /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 00000000..e69de29b --- /dev/null +++ b/src/leap/__init__.py diff --git a/src/leap/app.py b/src/leap/app.py new file mode 100644 index 00000000..0a61fd4f --- /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 00000000..e69de29b --- /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 00000000..efdb4726 --- /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 00000000..68b6de8f --- /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 00000000..e69de29b --- /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 00000000..e3adadc4 --- /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 00000000..78777cfb --- /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 00000000..09bd5811 --- /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 00000000..e69de29b --- /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 00000000..e5a671f3 --- /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 00000000..45de2cd6 --- /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 00000000..06f96870 --- /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 00000000..564631cd --- /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 00000000..e69de29b --- /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 00000000..5e25eb63 --- /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 00000000..9c355134 --- /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 00000000..ef0df1ca --- /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 00000000..8ac49669 --- /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 00000000..2c56e12d --- /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 00000000..0f2b08a5 --- /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 00000000..c8e5d54a --- /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 00000000..323a257f --- /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 00000000..398b7f36 --- /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 00000000..d8a0314b --- /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() | 
