From 414389951e28d88e195aaf5e23316a39523b354a Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 6 Nov 2006 16:05:00 +0000 Subject: [PATCH] moved pythonrewrite branch to trunk --- {bin => bin-perl-old}/Makefile | 0 {bin => bin-perl-old}/cbox-manage.sh | 0 {bin => bin-perl-old}/cbox-root-actions.sh | 0 {bin => bin-perl-old}/cryptobox.pl | 2 +- {bin => bin-perl-old}/cryptobox_wrapper.c | 0 {bin => bin-perl-old}/ro-system.sh | 0 bin/CryptoBox.py | 276 + bin/CryptoBoxContainer.py | 607 + bin/CryptoBoxExceptions.py | 107 + bin/CryptoBoxPlugin.py | 165 + bin/CryptoBoxRootActions.py | 386 + bin/CryptoBoxSettings.py | 481 + bin/CryptoBoxTools.py | 186 + bin/CryptoBoxWebserver.py | 38 + bin/Plugins.py | 67 + bin/WebInterfaceDataset.py | 136 + bin/WebInterfaceSites.py | 427 + bin/WebInterfaceTestClass.py | 77 + bin/coding_guidelines.txt | 18 + bin/cryptobox.conf | 83 + bin/cryptoboxd | 39 + bin/cryptoboxwebserver.conf | 17 + bin/do_unittests.sh | 22 + bin/example-super.tab | 2 + bin/test.complete.CryptoBox.py | 116 + bin/uml-setup.sh | 23 + bin/unittests.CryptoBox.py | 138 + bin/unittests.CryptoBoxTools.py | 48 + bin/unittests.Plugins.py | 33 + bin/unittests.WebSites.py | 39 + debian/README.Debian | 3 +- debian/control | 3 +- debian/rules | 7 +- design/background_frame_corner.svg | 265 + design/icon_background_active.svg | 92 + design/icons/applications-system_tango.svg | 245 + design/icons/computer_tango.svg | 738 + design/icons/dialog-error_tango.svg | 316 + design/icons/dialog-information_tango.svg | 1145 + design/icons/dialog-warning_tango.svg | 290 + design/icons/drive-cdrom_tango.svg | 444 + design/icons/drive-harddisk_tango.svg | 469 + design/icons/drive-removable-media_tango.svg | 390 + design/icons/globe-lips.svg | 512 + .../icons/gnome-dev-removable-usb_nuvola.svg | 1004 + design/icons/gnome-globe_nuvola.svg | 1195 + design/icons/gtk-zoom-in_nuvola.svg | 433 + design/icons/help_contents.svg | 701 + .../inaccessible_tango_emblem-unreadable.svg | 357 + design/icons/language.png | Bin 0 -> 3520 bytes design/icons/locked_tango-emblem-readonly.svg | 298 + .../icons/multimedia-dell-dj-pocket_tango.svg | 4405 ++++ ...multimedia-player-ipod-mini-blue_tango.svg | 4126 +++ .../multimedia-player-motorola-rokr_tango.svg | 1025 + .../icons/network-transmit-receive_design.svg | 1041 + design/icons/pile_of_devices.png | Bin 0 -> 17218 bytes design/icons/pile_of_devices.svg | 22002 ++++++++++++++++ .../preferences-desktop-locale_tango.svg | 828 + design/icons/preferences-system_tango.svg | 396 + design/icons/redhat-config-users_wasp.svg | 1904 ++ design/icons/seahorse-preferences_gnome.svg | 1250 + design/icons/spherecrystal_help.svg | 60 + design/icons/system-log-out_tango.svg | 362 + design/icons/unlocked_clavdia.svg | 515 + design/icons/unlocked_lars.svg | 1198 + doc/html/fr | 1 + doc/html/si | 1 + known_problems | 4 + lang/README | 31 +- lang/TODO | 2 + lang/de.hdf | 437 +- lang/en.hdf | 360 +- lang/fr.hdf | 502 +- lang/language_specification.txt | 24 + lang/si.hdf | 400 +- plugins/date/date.py | 69 + plugins/date/form_date.cs | 44 + plugins/date/lang/en.hdf | 35 + plugins/date/plugin_icon.png | Bin 0 -> 3285 bytes plugins/date/root_action.py | 36 + plugins/date/unittests.py | 60 + plugins/disks/disks.cs | 17 + plugins/disks/disks.py | 17 + plugins/disks/lang/de.hdf | 6 + plugins/disks/lang/en.hdf | 6 + plugins/disks/plugin_icon.png | Bin 0 -> 6279 bytes plugins/disks/unittests.py | 9 + plugins/format_fs/format_fs.py | 95 + plugins/format_fs/lang/en.hdf | 49 + plugins/format_fs/plugin_icon.png | Bin 0 -> 6376 bytes plugins/format_fs/unittests.py | 10 + plugins/format_fs/volume_format.cs | 37 + plugins/format_fs/volume_format_luks.cs | 32 + plugins/help/doc.cs | 9 + plugins/help/help.py | 25 + plugins/help/lang/en.hdf | 5 + plugins/help/plugin_icon.png | Bin 0 -> 12693 bytes plugins/help/unittests.py | 29 + plugins/language_selection/lang/en.hdf | 5 + .../language_selection/language_selection.cs | 15 + .../language_selection/language_selection.py | 16 + plugins/language_selection/plugin_icon.png | Bin 0 -> 13094 bytes plugins/language_selection/unittests.py | 10 + plugins/logs/lang/en.hdf | 6 + plugins/logs/logs.css | 6 + plugins/logs/logs.py | 29 + plugins/logs/plugin_icon.png | Bin 0 -> 16601 bytes plugins/logs/show_log.cs | 19 + plugins/logs/unittests.py | 21 + plugins/network/form_network.cs | 30 + plugins/network/lang/en.hdf | 23 + plugins/network/network.py | 126 + plugins/network/plugin_icon.png | Bin 0 -> 13698 bytes plugins/network/root_action.py | 42 + plugins/network/unittests.py | 45 + plugins/partition/current_partition_info.cs | 11 + plugins/partition/lang/en.hdf | 83 + plugins/partition/partition.css | 4 + plugins/partition/partition.py | 416 + plugins/partition/plugin_icon.png | Bin 0 -> 2943 bytes plugins/partition/root_action.py | 96 + plugins/partition/select_device.cs | 45 + plugins/partition/set_partitions.cs | 78 + plugins/partition/show_format_progress.cs | 17 + plugins/partition/unittests.py | 10 + plugins/plugin-interface.txt | 63 + plugins/plugin_icon_unknown.png | Bin 0 -> 14269 bytes plugins/plugin_manager/lang/en.hdf | 15 + plugins/plugin_manager/plugin_icon.png | Bin 0 -> 631 bytes plugins/plugin_manager/plugin_list.cs | 65 + plugins/plugin_manager/plugin_manager.py | 52 + plugins/plugin_manager/unittests.py | 12 + plugins/shutdown/form_shutdown.cs | 15 + plugins/shutdown/gnome-reboot.png | Bin 0 -> 3752 bytes plugins/shutdown/gnome-shutdown.png | Bin 0 -> 4532 bytes plugins/shutdown/lang/en.hdf | 34 + plugins/shutdown/plugin_icon.png | Bin 0 -> 7588 bytes plugins/shutdown/progress_reboot.cs | 6 + plugins/shutdown/progress_shutdown.cs | 6 + plugins/shutdown/root_action.py | 48 + plugins/shutdown/shutdown.py | 51 + plugins/shutdown/unittests.py | 11 + plugins/system_preferences/lang/en.hdf | 5 + plugins/system_preferences/plugin_icon.png | Bin 0 -> 12762 bytes plugins/system_preferences/show_plugins.cs | 15 + .../system_preferences/system_preferences.py | 16 + plugins/system_preferences/unittests.py | 8 + plugins/user_manager/lang/en.hdf | 51 + plugins/user_manager/plugin_icon.png | Bin 0 -> 10613 bytes plugins/user_manager/unittests.py | 27 + plugins/user_manager/user_list.cs | 82 + plugins/user_manager/user_manager.py | 81 + plugins/volume_details/lang/en.hdf | 20 + plugins/volume_details/plugin_icon.png | Bin 0 -> 11235 bytes plugins/volume_details/unittests.py | 10 + plugins/volume_details/volume_details.cs | 21 + plugins/volume_details/volume_details.py | 18 + plugins/volume_mount/lang/en.hdf | 56 + plugins/volume_mount/plugin_icon.png | Bin 0 -> 4535 bytes plugins/volume_mount/unittests.py | 10 + plugins/volume_mount/volume_mount.cs | 18 + plugins/volume_mount/volume_mount.py | 103 + plugins/volume_mount/volume_status.cs | 9 + plugins/volume_mount/volume_umount.cs | 10 + plugins/volume_props/lang/en.hdf | 63 + plugins/volume_props/plugin_icon.png | Bin 0 -> 15249 bytes plugins/volume_props/unittests.py | 10 + plugins/volume_props/volume_properties.cs | 75 + plugins/volume_props/volume_props.py | 81 + scripts/check_languages.py | 106 + scripts/check_languages.sh | 26 - scripts/userdocexport.sh | 2 +- templates/access_denied.cs | 6 + templates/empty.cs | 3 + templates/error.cs | 3 - templates/footer.cs | 37 +- templates/form_config.cs | 34 - templates/form_init.cs | 27 - templates/form_init_partition.cs | 33 - templates/form_mount.cs | 40 - templates/form_system.cs | 29 - templates/form_umount.cs | 37 - templates/header.cs | 71 +- templates/macros.cs | 204 +- templates/main.cs | 10 +- templates/nav.cs | 26 - templates/show_doc.cs | 7 - templates/show_log.cs | 13 - templates/show_status.cs | 28 - templates/show_volume.cs | 73 +- templates/show_volume_footer.cs | 3 + templates/show_volume_header.cs | 21 + templates/show_volumes.cs | 15 - templates/volume_plugins.cs | 25 + www-data/background_frame_corner.png | Bin 0 -> 2355 bytes www-data/background_frame_top.png | Bin 0 -> 2109 bytes www-data/cryptobox.css | 518 +- www-data/dialog-error_tango.png | Bin 0 -> 8750 bytes www-data/dialog-information_tango.png | Bin 0 -> 14120 bytes www-data/dialog-warning_tango.png | Bin 0 -> 7611 bytes www-data/disc_gray.png | Bin 0 -> 4524 bytes www-data/disc_green.png | Bin 4752 -> 4936 bytes www-data/disc_red.png | Bin 4804 -> 4988 bytes www-data/evil_stick.png | Bin 0 -> 32626 bytes www-data/footer_line.png | Bin 0 -> 262 bytes www-data/icon_background_active.png | Bin 0 -> 14372 bytes www-data/icon_background_active_060.png | Bin 0 -> 1845 bytes www-data/icon_background_active_080.png | Bin 0 -> 2891 bytes www-data/icon_background_active_100.png | Bin 0 -> 4101 bytes www-data/icon_background_active_256.png | Bin 0 -> 14372 bytes www-data/icon_background_passive_060.png | Bin 0 -> 1570 bytes www-data/icon_background_passive_080.png | Bin 0 -> 2338 bytes www-data/icon_background_passive_100.png | Bin 0 -> 3292 bytes www-data/pane_bottom_left.png | Bin 0 -> 249 bytes www-data/pane_bottom_right.png | Bin 0 -> 249 bytes www-data/pane_side_bottom.png | Bin 0 -> 131 bytes www-data/pane_side_left.png | Bin 0 -> 131 bytes www-data/pane_side_right.png | Bin 0 -> 131 bytes www-data/pane_side_top.png | Bin 0 -> 133 bytes www-data/pane_top_left.png | Bin 0 -> 249 bytes www-data/pane_top_right.png | Bin 0 -> 273 bytes www-data/register_active.png | Bin 0 -> 560 bytes www-data/register_active2.png | Bin 0 -> 1078 bytes www-data/register_passive.png | Bin 0 -> 523 bytes www-data/register_passive2.png | Bin 0 -> 1087 bytes www-data/volume_active_crypto.png | Bin 0 -> 6590 bytes www-data/volume_active_plain.png | Bin 0 -> 5903 bytes www-data/volume_passive_crypto.png | Bin 0 -> 8140 bytes www-data/volume_passive_plain.png | Bin 0 -> 7661 bytes www-data/volume_property_frame.png | Bin 0 -> 6389 bytes 230 files changed, 56014 insertions(+), 1607 deletions(-) rename {bin => bin-perl-old}/Makefile (100%) rename {bin => bin-perl-old}/cbox-manage.sh (100%) rename {bin => bin-perl-old}/cbox-root-actions.sh (100%) rename {bin => bin-perl-old}/cryptobox.pl (99%) rename {bin => bin-perl-old}/cryptobox_wrapper.c (100%) rename {bin => bin-perl-old}/ro-system.sh (100%) create mode 100755 bin/CryptoBox.py create mode 100755 bin/CryptoBoxContainer.py create mode 100644 bin/CryptoBoxExceptions.py create mode 100644 bin/CryptoBoxPlugin.py create mode 100755 bin/CryptoBoxRootActions.py create mode 100644 bin/CryptoBoxSettings.py create mode 100644 bin/CryptoBoxTools.py create mode 100755 bin/CryptoBoxWebserver.py create mode 100644 bin/Plugins.py create mode 100644 bin/WebInterfaceDataset.py create mode 100755 bin/WebInterfaceSites.py create mode 100644 bin/WebInterfaceTestClass.py create mode 100644 bin/coding_guidelines.txt create mode 100644 bin/cryptobox.conf create mode 100755 bin/cryptoboxd create mode 100644 bin/cryptoboxwebserver.conf create mode 100755 bin/do_unittests.sh create mode 100644 bin/example-super.tab create mode 100755 bin/test.complete.CryptoBox.py create mode 100755 bin/uml-setup.sh create mode 100755 bin/unittests.CryptoBox.py create mode 100755 bin/unittests.CryptoBoxTools.py create mode 100755 bin/unittests.Plugins.py create mode 100755 bin/unittests.WebSites.py create mode 100644 design/background_frame_corner.svg create mode 100644 design/icon_background_active.svg create mode 100644 design/icons/applications-system_tango.svg create mode 100644 design/icons/computer_tango.svg create mode 100644 design/icons/dialog-error_tango.svg create mode 100644 design/icons/dialog-information_tango.svg create mode 100644 design/icons/dialog-warning_tango.svg create mode 100644 design/icons/drive-cdrom_tango.svg create mode 100644 design/icons/drive-harddisk_tango.svg create mode 100644 design/icons/drive-removable-media_tango.svg create mode 100644 design/icons/globe-lips.svg create mode 100644 design/icons/gnome-dev-removable-usb_nuvola.svg create mode 100644 design/icons/gnome-globe_nuvola.svg create mode 100644 design/icons/gtk-zoom-in_nuvola.svg create mode 100644 design/icons/help_contents.svg create mode 100644 design/icons/inaccessible_tango_emblem-unreadable.svg create mode 100644 design/icons/language.png create mode 100644 design/icons/locked_tango-emblem-readonly.svg create mode 100644 design/icons/multimedia-dell-dj-pocket_tango.svg create mode 100644 design/icons/multimedia-player-ipod-mini-blue_tango.svg create mode 100644 design/icons/multimedia-player-motorola-rokr_tango.svg create mode 100644 design/icons/network-transmit-receive_design.svg create mode 100644 design/icons/pile_of_devices.png create mode 100644 design/icons/pile_of_devices.svg create mode 100644 design/icons/preferences-desktop-locale_tango.svg create mode 100644 design/icons/preferences-system_tango.svg create mode 100644 design/icons/redhat-config-users_wasp.svg create mode 100644 design/icons/seahorse-preferences_gnome.svg create mode 100644 design/icons/spherecrystal_help.svg create mode 100644 design/icons/system-log-out_tango.svg create mode 100644 design/icons/unlocked_clavdia.svg create mode 100644 design/icons/unlocked_lars.svg create mode 120000 doc/html/fr create mode 120000 doc/html/si create mode 100644 known_problems create mode 100644 lang/language_specification.txt create mode 100644 plugins/date/date.py create mode 100644 plugins/date/form_date.cs create mode 100644 plugins/date/lang/en.hdf create mode 100644 plugins/date/plugin_icon.png create mode 100755 plugins/date/root_action.py create mode 100644 plugins/date/unittests.py create mode 100644 plugins/disks/disks.cs create mode 100644 plugins/disks/disks.py create mode 100644 plugins/disks/lang/de.hdf create mode 100644 plugins/disks/lang/en.hdf create mode 100644 plugins/disks/plugin_icon.png create mode 100644 plugins/disks/unittests.py create mode 100644 plugins/format_fs/format_fs.py create mode 100644 plugins/format_fs/lang/en.hdf create mode 100644 plugins/format_fs/plugin_icon.png create mode 100644 plugins/format_fs/unittests.py create mode 100644 plugins/format_fs/volume_format.cs create mode 100644 plugins/format_fs/volume_format_luks.cs create mode 100644 plugins/help/doc.cs create mode 100644 plugins/help/help.py create mode 100644 plugins/help/lang/en.hdf create mode 100644 plugins/help/plugin_icon.png create mode 100644 plugins/help/unittests.py create mode 100644 plugins/language_selection/lang/en.hdf create mode 100644 plugins/language_selection/language_selection.cs create mode 100644 plugins/language_selection/language_selection.py create mode 100644 plugins/language_selection/plugin_icon.png create mode 100644 plugins/language_selection/unittests.py create mode 100644 plugins/logs/lang/en.hdf create mode 100644 plugins/logs/logs.css create mode 100644 plugins/logs/logs.py create mode 100644 plugins/logs/plugin_icon.png create mode 100644 plugins/logs/show_log.cs create mode 100644 plugins/logs/unittests.py create mode 100644 plugins/network/form_network.cs create mode 100644 plugins/network/lang/en.hdf create mode 100644 plugins/network/network.py create mode 100644 plugins/network/plugin_icon.png create mode 100755 plugins/network/root_action.py create mode 100644 plugins/network/unittests.py create mode 100644 plugins/partition/current_partition_info.cs create mode 100644 plugins/partition/lang/en.hdf create mode 100644 plugins/partition/partition.css create mode 100644 plugins/partition/partition.py create mode 100644 plugins/partition/plugin_icon.png create mode 100755 plugins/partition/root_action.py create mode 100644 plugins/partition/select_device.cs create mode 100644 plugins/partition/set_partitions.cs create mode 100644 plugins/partition/show_format_progress.cs create mode 100644 plugins/partition/unittests.py create mode 100644 plugins/plugin-interface.txt create mode 100644 plugins/plugin_icon_unknown.png create mode 100644 plugins/plugin_manager/lang/en.hdf create mode 100644 plugins/plugin_manager/plugin_icon.png create mode 100644 plugins/plugin_manager/plugin_list.cs create mode 100644 plugins/plugin_manager/plugin_manager.py create mode 100644 plugins/plugin_manager/unittests.py create mode 100644 plugins/shutdown/form_shutdown.cs create mode 100644 plugins/shutdown/gnome-reboot.png create mode 100644 plugins/shutdown/gnome-shutdown.png create mode 100644 plugins/shutdown/lang/en.hdf create mode 100644 plugins/shutdown/plugin_icon.png create mode 100644 plugins/shutdown/progress_reboot.cs create mode 100644 plugins/shutdown/progress_shutdown.cs create mode 100755 plugins/shutdown/root_action.py create mode 100644 plugins/shutdown/shutdown.py create mode 100644 plugins/shutdown/unittests.py create mode 100644 plugins/system_preferences/lang/en.hdf create mode 100644 plugins/system_preferences/plugin_icon.png create mode 100644 plugins/system_preferences/show_plugins.cs create mode 100644 plugins/system_preferences/system_preferences.py create mode 100644 plugins/system_preferences/unittests.py create mode 100644 plugins/user_manager/lang/en.hdf create mode 100644 plugins/user_manager/plugin_icon.png create mode 100644 plugins/user_manager/unittests.py create mode 100644 plugins/user_manager/user_list.cs create mode 100644 plugins/user_manager/user_manager.py create mode 100644 plugins/volume_details/lang/en.hdf create mode 100644 plugins/volume_details/plugin_icon.png create mode 100644 plugins/volume_details/unittests.py create mode 100644 plugins/volume_details/volume_details.cs create mode 100644 plugins/volume_details/volume_details.py create mode 100644 plugins/volume_mount/lang/en.hdf create mode 100644 plugins/volume_mount/plugin_icon.png create mode 100644 plugins/volume_mount/unittests.py create mode 100644 plugins/volume_mount/volume_mount.cs create mode 100644 plugins/volume_mount/volume_mount.py create mode 100644 plugins/volume_mount/volume_status.cs create mode 100644 plugins/volume_mount/volume_umount.cs create mode 100644 plugins/volume_props/lang/en.hdf create mode 100644 plugins/volume_props/plugin_icon.png create mode 100644 plugins/volume_props/unittests.py create mode 100644 plugins/volume_props/volume_properties.cs create mode 100644 plugins/volume_props/volume_props.py create mode 100755 scripts/check_languages.py delete mode 100755 scripts/check_languages.sh create mode 100644 templates/access_denied.cs delete mode 100644 templates/error.cs delete mode 100644 templates/form_config.cs delete mode 100644 templates/form_init.cs delete mode 100644 templates/form_init_partition.cs delete mode 100644 templates/form_mount.cs delete mode 100644 templates/form_system.cs delete mode 100644 templates/form_umount.cs delete mode 100644 templates/nav.cs delete mode 100644 templates/show_doc.cs delete mode 100644 templates/show_log.cs delete mode 100644 templates/show_status.cs create mode 100644 templates/show_volume_footer.cs create mode 100644 templates/show_volume_header.cs delete mode 100644 templates/show_volumes.cs create mode 100644 templates/volume_plugins.cs create mode 100644 www-data/background_frame_corner.png create mode 100644 www-data/background_frame_top.png create mode 100644 www-data/dialog-error_tango.png create mode 100644 www-data/dialog-information_tango.png create mode 100644 www-data/dialog-warning_tango.png create mode 100644 www-data/disc_gray.png create mode 100644 www-data/evil_stick.png create mode 100644 www-data/footer_line.png create mode 100644 www-data/icon_background_active.png create mode 100644 www-data/icon_background_active_060.png create mode 100644 www-data/icon_background_active_080.png create mode 100644 www-data/icon_background_active_100.png create mode 100644 www-data/icon_background_active_256.png create mode 100644 www-data/icon_background_passive_060.png create mode 100644 www-data/icon_background_passive_080.png create mode 100644 www-data/icon_background_passive_100.png create mode 100644 www-data/pane_bottom_left.png create mode 100644 www-data/pane_bottom_right.png create mode 100644 www-data/pane_side_bottom.png create mode 100644 www-data/pane_side_left.png create mode 100644 www-data/pane_side_right.png create mode 100644 www-data/pane_side_top.png create mode 100644 www-data/pane_top_left.png create mode 100644 www-data/pane_top_right.png create mode 100644 www-data/register_active.png create mode 100644 www-data/register_active2.png create mode 100644 www-data/register_passive.png create mode 100644 www-data/register_passive2.png create mode 100644 www-data/volume_active_crypto.png create mode 100644 www-data/volume_active_plain.png create mode 100644 www-data/volume_passive_crypto.png create mode 100644 www-data/volume_passive_plain.png create mode 100644 www-data/volume_property_frame.png diff --git a/bin/Makefile b/bin-perl-old/Makefile similarity index 100% rename from bin/Makefile rename to bin-perl-old/Makefile diff --git a/bin/cbox-manage.sh b/bin-perl-old/cbox-manage.sh similarity index 100% rename from bin/cbox-manage.sh rename to bin-perl-old/cbox-manage.sh diff --git a/bin/cbox-root-actions.sh b/bin-perl-old/cbox-root-actions.sh similarity index 100% rename from bin/cbox-root-actions.sh rename to bin-perl-old/cbox-root-actions.sh diff --git a/bin/cryptobox.pl b/bin-perl-old/cryptobox.pl similarity index 99% rename from bin/cryptobox.pl rename to bin-perl-old/cryptobox.pl index 9c3a999..7f36806 100755 --- a/bin/cryptobox.pl +++ b/bin-perl-old/cryptobox.pl @@ -532,7 +532,7 @@ if ( ! &check_ssl()) { if ($device eq '') { &debug_msg(DEBUG_INFO, "invalid device: " . $query->param('device')); $pagedata->setValue('Data.Warning', 'InvalidDevice'); - $pagedata->setValue('Data.Action', 'empty'); + $pagedata->setValue('Data.Action', 'emptu'); } elsif ( ! &check_config()) { $pagedata->setValue('Data.Warning', 'NotInitialized'); $pagedata->setValue('Data.Action', 'form_init'); diff --git a/bin/cryptobox_wrapper.c b/bin-perl-old/cryptobox_wrapper.c similarity index 100% rename from bin/cryptobox_wrapper.c rename to bin-perl-old/cryptobox_wrapper.c diff --git a/bin/ro-system.sh b/bin-perl-old/ro-system.sh similarity index 100% rename from bin/ro-system.sh rename to bin-perl-old/ro-system.sh diff --git a/bin/CryptoBox.py b/bin/CryptoBox.py new file mode 100755 index 0000000..1472b53 --- /dev/null +++ b/bin/CryptoBox.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python2.4 +''' +This is the web interface for a fileserver managing encrypted filesystems. + +It was originally written in bash/perl. Now a complete rewrite is in +progress. So things might be confusing here. Hopefully not for long. +:) +''' + +# check python version +import sys +(ver_major, ver_minor, ver_sub, ver_desc, ver_subsub) = sys.version_info +if (ver_major < 2) or ((ver_major == 2) and (ver_minor < 4)): + sys.stderr.write("You need a python version >= 2.4\nCurrent version is:\n %s\n" % sys.version) + sys.exit(1) + +import CryptoBoxContainer +from CryptoBoxExceptions import * +import re +import os +import CryptoBoxTools +import subprocess + + + +class CryptoBox: + '''this class rules them all! + + put things like logging, conf and oter stuff in here, + that might be used by more classes, it will be passed on to them''' + + VERSION = "0.3~1" + + def __init__(self, config_file=None): + import CryptoBoxSettings + self.log = self.__getStartupLogger() + self.prefs = CryptoBoxSettings.CryptoBoxSettings(config_file) + self.__runTests() + + + def __getStartupLogger(self): + import logging + '''initialises the logging system + + use it with: 'self.log.[debug|info|warning|error|critical](logmessage)' + all classes should get the logging instance during __init__: + self.log = logging.getLogger("CryptoBox") + + first we output all warnings/errors to stderr + as soon as we opened the config file successfully, we redirect debug output + to the configured destination''' + ## basicConfig(...) needs python >= 2.4 + try: + log_handler = logging.getLogger("CryptoBox") + logging.basicConfig( + format='%(asctime)s CryptoBox %(levelname)s: %(message)s', + stderr=sys.stderr) + log_handler.setLevel(logging.ERROR) + log_handler.info("loggingsystem is up'n running") + ## from now on everything can be logged via self.log... + except: + raise CBEnvironmentError("couldn't initialise the loggingsystem. I give up.") + return log_handler + + + # do some initial checks + def __runTests(self): + self.__runTestUID() + self.__runTestRootPriv() + + + def __runTestUID(self): + if os.geteuid() == 0: + raise CBEnvironmentError("you may not run the cryptobox as root") + + + def __runTestRootPriv(self): + """try to run 'super' with 'CryptoBoxRootActions'""" + try: + devnull = open(os.devnull, "w") + except IOError: + raise CBEnvironmentError("could not open %s for writing!" % os.devnull) + try: + prog_super = self.prefs["Programs"]["super"] + except KeyError: + raise CBConfigUndefinedError("Programs", "super") + try: + prog_rootactions = self.prefs["Programs"]["CryptoBoxRootActions"] + except KeyError: + raise CBConfigUndefinedError("Programs", "CryptoBoxRootActions") + try: + proc = subprocess.Popen( + shell = False, + stdout = devnull, + stderr = devnull, + args = [prog_super, prog_rootactions, "check"]) + except OSError: + raise CBEnvironmentError("failed to execute 'super' (%s)" % self.prefs["Programs"]["super"]) + proc.wait() + if proc.returncode != 0: + raise CBEnvironmentError("failed to call CryptoBoxRootActions (%s) via 'super' - maybe you did not add the appropriate line to /etc/super.tab?" % prog_rootactions) + + + # this method just demonstrates inheritance effects - may be removed + def cbx_inheritance_test(self, string="you lucky widow"): + self.log.info(string) + + +# RFC: why should CryptoBoxProps inherit CryptoBox? [l] +# RFC: shouldn't we move all useful functions of CryptoBoxProps to CryptoBox? [l] +class CryptoBoxProps(CryptoBox): + '''Get and set the properties of a CryptoBox + + This class contains all available devices that may be accessed. + All properties of the cryptobox can be accessed by this class. + ''' + + def __init__(self, config_file=None): + '''read config and fill class variables''' + CryptoBox.__init__(self, config_file) + self.reReadContainerList() + + + def reReadContainerList(self): + self.log.debug("rereading container list") + self.containers = [] + for device in CryptoBoxTools.getAvailablePartitions(): + if self.isDeviceAllowed(device) and not self.isConfigPartition(device): + self.containers.append(CryptoBoxContainer.CryptoBoxContainer(device, self)) + ## sort by container name + self.containers.sort(cmp = lambda x,y: x.getName() < y.getName() and -1 or 1) + + + def isConfigPartition(self, device): + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + args = [ + self.prefs["Programs"]["blkid"], + "-c", os.path.devnull, + "-o", "value", + "-s", "LABEL", + device]) + (output, error) = proc.communicate() + return output.strip() == self.prefs["Main"]["ConfigVolumeLabel"] + + + def isDeviceAllowed(self, devicename): + "check if a device is white-listed for being used as cryptobox containers" + import types + allowed = self.prefs["Main"]["AllowedDevices"] + if type(allowed) == types.StringType: allowed = [allowed] + for a_dev in allowed: + "remove double dots and so on ..." + real_device = os.path.realpath(devicename) + if a_dev and re.search('^' + a_dev, real_device): return True + return False + + + def getLogData(self, lines=None, maxSize=None): + """get the most recent log entries of the cryptobox + + the maximum number and size of these entries can be limited by 'lines' and 'maxSize' + """ + # return nothing if the currently selected log output is not a file + try: + if self.prefs["Log"]["Destination"].upper() != "FILE": return [] + log_file = self.prefs["Log"]["Details"] + except KeyError: + self.log.error("could not evaluate one of the following config settings: [Log]->Destination or [Log]->Details") + return [] + try: + fd = open(log_file, "r") + if maxSize: fd.seek(-maxSize, 2) # seek relative to the end of the file + content = fd.readlines() + fd.close() + except IOError: + self.log.warn("failed to read the log file (%s)" % log_file) + return [] + if lines: content = content[-lines:] + content.reverse() + return content + + + def getContainerList(self, filterType=None, filterName=None): + "retrieve the list of all containers of this cryptobox" + try: + result = self.containers[:] + if filterType != None: + if filterType in range(len(CryptoBoxContainer.Types)): + return [e for e in self.containers if e.getType() == filterType] + else: + self.log.info("invalid filterType (%d)" % filterType) + result.clear() + if filterName != None: + result = [e for e in self.containers if e.getName() == filterName] + return result + except AttributeError: + return [] + + + def getContainer(self, device): + "retrieve the container element for this device" + all = [e for e in self.getContainerList() if e.device == device] + if all: + return all[0] + else: + return None + + + def setNameForUUID(self, uuid, name): + "assign a name to a uuid in the ContainerNameDatabase" + used_uuid = self.getUUIDForName(name) + "first remove potential conflicting uuid/name combination" + if used_uuid: + ## remember the container which name was overriden + for e in self.containers: + if e.getName() == name: + forcedRename = e + break + del self.prefs.nameDB[used_uuid] + self.prefs.nameDB[uuid] = name + self.prefs.nameDB.write() + ## rename the container that lost its name (necessary while we use cherrypy) + if used_uuid: + ## this is surely not the best way to regenerate the name + dev = e.getDevice() + old_index = self.containers.index(e) + self.containers.remove(e) + self.containers.insert(old_index, CryptoBoxContainer.CryptoBoxContainer(dev,self)) + ## there should be no reason for any failure + return True + + + def getNameForUUID(self, uuid): + "get the name belonging to a specified key (usually the UUID of a fs)" + try: + return self.prefs.nameDB[uuid] + except KeyError: + return None + + + def getUUIDForName(self, name): + """ get the key belonging to a value in the ContainerNameDatabase + this is the reverse action of 'getNameForUUID' """ + for key in self.prefs.nameDB.keys(): + if self.prefs.nameDB[key] == name: return key + "the uuid was not found" + return None + + + def removeUUID(self, uuid): + if uuid in self.prefs.nameDB.keys(): + del self.prefs.nameDB[uuid] + return True + else: + return False + + + def getAvailableLanguages(self): + '''reads all files in path LangDir and returns a list of + basenames from existing hdf files, that should are all available + languages''' + languages = [ f.rstrip(".hdf") + for f in os.listdir(self.prefs["Locations"]["LangDir"]) + if f.endswith(".hdf") ] + if len(languages) < 1: + self.log.error("No .hdf files found! The website won't render properly.") + return languages + + + + +if __name__ == "__main__": + cb = CryptoBoxProps() + diff --git a/bin/CryptoBoxContainer.py b/bin/CryptoBoxContainer.py new file mode 100755 index 0000000..e658c53 --- /dev/null +++ b/bin/CryptoBoxContainer.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python2.4 + +## check python version +import sys +(ver_major, ver_minor, ver_sub, ver_desc, ver_subsub) = sys.version_info +if (ver_major < 2) or ((ver_major == 2) and (ver_minor < 4)): + sys.stderr.write("You need a python version >= 2.4\nCurrent version is:\n %s\n" % sys.version) + sys.exit(1) + +import subprocess +import os +import re +import logging +from CryptoBoxExceptions import * + +"""exceptions: + VolumeIsActive + NameActivelyUsed + InvalidName + InvalidPassword + InvalidType + CreateError + MountError + ChangePasswordError + """ + +class CryptoBoxContainer: + + Types = { + "unused":0, + "plain":1, + "luks":2, + "swap":3} + + + __fsTypes = { + "plain":["ext3", "ext2", "vfat", "reiser"], + "swap":["swap"]} + # TODO: more filesystem types? / check 'reiser' + + __dmDir = "/dev/mapper" + + + def __init__(self, device, cbox): + self.device = device + self.cbox = cbox + self.log = logging.getLogger("CryptoBox") + self.resetObject() + + + def getName(self): + return self.name + + + def setName(self, new_name): + if new_name == self.name: return + if self.isMounted(): + raise CBVolumeIsActive("the container must be inactive during renaming") + if not re.search(r'^[a-zA-Z0-9_\.\- ]+$', new_name): + raise CBInvalidName("the supplied new name contains illegal characters") + "check for active partitions with the same name" + prev_name_owner = self.cbox.getContainerList(filterName=new_name) + if prev_name_owner: + for a in prev_name_owner: + if a.isMounted(): + raise CBNameActivelyUsed("the supplied new name is already in use for an active partition") + if not self.cbox.setNameForUUID(self.uuid, new_name): + raise CBContainerError("failed to change the volume name for unknown reasons") + self.name = new_name + + + def getDevice(self): + return self.device + + + def getType(self): + return self.type + + + def isMounted(self): + return os.path.ismount(self.__getMountPoint()) + + + def getCapacity(self): + """return the current capacity state of the volume + + the volume may not be mounted + the result is a tuple of values in megabyte: + (size, available, used) + """ + info = os.statvfs(self.__getMountPoint()) + return ( + int(info.f_bsize*info.f_blocks/1024/1024), + int(info.f_bsize*info.f_bavail/1024/1024), + int(info.f_bsize*(info.f_blocks-info.f_bavail)/1024/1024)) + + + def getSize(self): + """return the size of the block device (_not_ of the filesystem) + + the result is a value in megabyte + an error is indicated by "-1" + """ + import CryptoBoxTools + return CryptoBoxTools.getBlockDeviceSize(self.device) + + + def resetObject(self): + """ recheck the information about this container + this is especially useful after changing the type via 'create' """ + self.uuid = self.__getUUID() + self.type = self.__getTypeOfPartition() + self.name = self.__getNameOfContainer() + if self.type == self.Types["luks"]: + self.mount = self.__mountLuks + self.umount = self.__umountLuks + elif self.type == self.Types["plain"]: + self.mount = self.__mountPlain + self.umount = self.__umountPlain + + + def create(self, type, password=None): + old_name = self.getName() + if type == self.Types["luks"]: + self.__createLuks(password) + elif type == self.Types["plain"]: + self.__createPlain() + else: + raise CBInvalidType("invalid container type (%d) supplied" % (type, )) + ## no exception was raised during creation -> we can continue + ## reset the properties (encryption state, ...) of the device + self.resetObject() + ## restore the old name (must be after resetObject) + self.setName(old_name) + + + def changePassword(self, oldpw, newpw): + if self.type != self.Types["luks"]: + raise CBInvalidType("changing of password is possible only for luks containers") + if not oldpw: + raise CBInvalidPassword("no old password supplied for password change") + if not newpw: + raise CBInvalidPassword("no new password supplied for password change") + "return if new and old passwords are the same" + if oldpw == newpw: return + if self.isMounted(): + raise CBVolumeIsActive("this container is currently active") + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + "remove any potential open luks mapping" + self.__umountLuks() + "create the luks header" + proc = subprocess.Popen( + shell = False, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "cryptsetup", + "luksAddKey", + self.device, + "--batch-mode"]) + proc.stdin.write("%s\n%s" % (oldpw, newpw)) + (output, errout) = proc.communicate() + if proc.returncode != 0: + errorMsg = "Could not add a new luks key: %s - %s" % (output.strip(), errout.strip(), ) + self.log.error(errorMsg) + raise CBChangePasswordError(errorMsg) + ## retrieve the key slot we used for unlocking + keys_found = re.search(r'key slot (\d{1,3}) unlocked', output).groups() + if keys_found: + keyslot = int(keys_found[0]) + else: + raise CBChangePasswordError("could not get the old key slot") + "remove the old key" + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["cryptsetup"], + "--batch-mode", + "luksDelKey", + self.device, + "%d" % (keyslot, )]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not remove the old luks key: %s" % (proc.stderr.read().strip(), ) + self.log.error(errorMsg) + raise CBChangePasswordError(errorMsg) + + + + " ****************** internal stuff ********************* " + + def __getNameOfContainer(self): + "retrieve the name of the container by querying the database" + def_name = self.cbox.getNameForUUID(self.uuid) + if def_name: return def_name + "there is no name defined for this uuid - we will propose a good one" + prefix = self.cbox.prefs["Main"]["DefaultVolumePrefix"] + unused_found = False + counter = 1 + while not unused_found: + guess = prefix + str(counter) + if self.cbox.getUUIDForName(guess): + counter += 1 + else: + unused_found = True + self.cbox.setNameForUUID(self.uuid, guess) + return guess + + + def __getUUID(self): + if self.__getTypeOfPartition() == self.Types["luks"]: + guess = self.__getLuksUUID() + else: + guess = self.__getNonLuksUUID() + ## did we get a valid value? + if guess: + return guess + else: + ## emergency default value + return self.device.replace(os.path.sep, "_") + + + def __getLuksUUID(self): + """get uuid for luks devices""" + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [self.cbox.prefs["Programs"]["cryptsetup"], + "luksUUID", + self.device]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + self.cbox.log.info("could not retrieve luks uuid (%s): %s", (self.device, stderr.strip())) + return None + return stdout.strip() + + + def __getNonLuksUUID(self): + """return UUID for ext2/3 and vfat filesystems""" + try: + devnull = open(os.devnull, "w") + except IOError: + self.warn("Could not open %s" % (os.devnull, )) + proc = subprocess.Popen( + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + args=[self.cbox.prefs["Programs"]["blkid"], + "-s", "UUID", + "-o", "value", + "-c", os.devnull, + "-w", os.devnull, + self.device]) + (stdout, stderr) = proc.communicate() + devnull.close() + ## execution failed? + if proc.returncode != 0: + self.log.info("retrieving of partition type (%s) via 'blkid' failed: %s - maybe it is encrypted?" % (self.device, stderr.strip())) + return None + ## return output of blkid + return stdout.strip() + + + def __getTypeOfPartition(self): + "retrieve the type of the given partition (see CryptoBoxContainer.Types)" + if self.__isLuksPartition(): return self.Types["luks"] + typeOfPartition = self.__getTypeIdOfPartition() + if typeOfPartition in self.__fsTypes["plain"]: + return self.Types["plain"] + if typeOfPartition in self.__fsTypes["swap"]: + return self.Types["swap"] + return self.Types["unused"] + + + def __getTypeIdOfPartition(self): + "returns the type of the partition (see 'man blkid')" + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + proc = subprocess.Popen( + shell=False, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + args=[self.cbox.prefs["Programs"]["blkid"], + "-s", "TYPE", + "-o", "value", + "-c", os.devnull, + "-w", os.devnull, + self.device]) + proc.wait() + output = proc.stdout.read().strip() + if proc.returncode != 0: + self.log.warn("retrieving of partition type via 'blkid' failed: %s" % (proc.stderr.read().strip(), )) + return None + devnull.close() + return output + + + def __isLuksPartition(self): + "check if the given device is a luks partition" + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = devnull, + args = [ + self.cbox.prefs["Programs"]["cryptsetup"], + "--batch-mode", + "isLuks", + self.device]) + proc.wait() + devnull.close() + return proc.returncode == 0 + + + def __getMountPoint(self): + "return the name of the mountpoint of this volume" + return os.path.join(self.cbox.prefs["Locations"]["MountParentDir"], self.name) + + + def __mountLuks(self, password): + "mount a luks partition" + if not password: + raise CBInvalidPassword("no password supplied for luksOpen") + if self.isMounted(): raise CBVolumeIsActive("this container is already active") + self.__umountLuks() + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + self.__cleanMountDirs() + if not os.path.exists(self.__getMountPoint()): + os.mkdir(self.__getMountPoint()) + if not os.path.exists(self.__getMountPoint()): + errorMsg = "Could not create mountpoint (%s)" % (self.__getMountPoint(), ) + self.log.error(errorMsg) + raise CBMountError(errorMsg) + proc = subprocess.Popen( + shell = False, + stdin = subprocess.PIPE, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "cryptsetup", + "luksOpen", + self.device, + self.name, + "--batch-mode"]) + proc.stdin.write(password) + (output, errout) = proc.communicate() + if proc.returncode != 0: + errorMsg = "Could not open the luks mapping: %s" % (errout.strip(), ) + self.log.warn(errorMsg) + raise CBMountError(errorMsg) + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "mount", + os.path.join(self.__dmDir, self.name), + self.__getMountPoint()]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not mount the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.warn(errorMsg) + raise CBMountError(errorMsg) + devnull.close() + + + def __umountLuks(self): + "umount a luks partition" + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + if self.isMounted(): + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "umount", + self.__getMountPoint()]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not umount the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.warn(errorMsg) + raise CBUmountError(errorMsg) + if os.path.exists(os.path.join(self.__dmDir, self.name)): + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "cryptsetup", + "luksClose", + self.name, + "--batch-mode"]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not remove the luks mapping: %s" % (proc.stderr.read().strip(), ) + self.log.warn(errorMsg) + raise CBUmountError(errorMsg) + devnull.close() + + + def __mountPlain(self): + "mount a plaintext partition" + if self.isMounted(): raise CBVolumeIsActive("this container is already active") + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + self.__cleanMountDirs() + if not os.path.exists(self.__getMountPoint()): + os.mkdir(self.__getMountPoint()) + if not os.path.exists(self.__getMountPoint()): + errorMsg = "Could not create mountpoint (%s)" % (self.__getMountPoint(), ) + self.log.error(errorMsg) + raise CBMountError(errorMsg) + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "mount", + self.device, + self.__getMountPoint()]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not mount the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.warn(errorMsg) + raise CBMountError(errorMsg) + devnull.close() + + + def __umountPlain(self): + "umount a plaintext partition" + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + if self.isMounted(): + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "umount", + self.__getMountPoint()]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not umount the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.warn(errorMsg) + raise CBUmountError(errorMsg) + devnull.close() + + + def __createPlain(self): + "make a plaintext partition" + if self.isMounted(): + raise CBVolumeIsActive("deactivate the partition before filesystem initialization") + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["mkfs-data"], + self.device]) + proc.wait() + if proc.returncode != 0: + errorMsg = "Could not create the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.error(errorMsg) + raise CBCreateError(errorMsg) + devnull.close() + + + def __createLuks(self, password): + "make a luks partition" + if not password: + raise CBInvalidPassword("no password supplied for new luks mapping") + if self.isMounted(): + raise CBVolumeIsActive("deactivate the partition before filesystem initialization") + devnull = None + try: + devnull = open(os.devnull, "w") + except IOError: + self.log.warn("Could not open %s" % (os.devnull, )) + "remove any potential open luks mapping" + self.__umountLuks() + "create the luks header" + proc = subprocess.Popen( + shell = False, + stdin = subprocess.PIPE, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "cryptsetup", + "luksFormat", + self.device, + "--batch-mode", + "--cipher", self.cbox.prefs["Main"]["DefaultCipher"], + "--iter-time", "2000"]) + proc.stdin.write(password) + (output, errout) = proc.communicate() + if proc.returncode != 0: + errorMsg = "Could not create the luks header: %s" % (errout.strip(), ) + self.log.error(errorMsg) + raise CBCreateError(errorMsg) + "open the luks container for mkfs" + proc = subprocess.Popen( + shell = False, + stdin = subprocess.PIPE, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "cryptsetup", + "luksOpen", + self.device, + self.name, + "--batch-mode"]) + proc.stdin.write(password) + (output, errout) = proc.communicate() + if proc.returncode != 0: + errorMsg = "Could not open the new luks mapping: %s" % (errout.strip(), ) + self.log.error(errorMsg) + raise CBCreateError(errorMsg) + "make the filesystem" + proc = subprocess.Popen( + shell = False, + stdin = None, + stdout = devnull, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["mkfs-data"], + os.path.join(self.__dmDir, self.name)]) + proc.wait() + "remove the mapping - for every exit status" + self.__umountLuks() + if proc.returncode != 0: + errorMsg = "Could not create the filesystem: %s" % (proc.stderr.read().strip(), ) + self.log.error(errorMsg) + "remove the luks mapping" + raise CBCreateError(errorMsg) + devnull.close() + + + def __cleanMountDirs(self): + """ remove all unnecessary subdirs of the mount parent directory + this should be called for every (u)mount """ + subdirs = os.listdir(self.cbox.prefs["Locations"]["MountParentDir"]) + for dir in subdirs: + abs_dir = os.path.join(self.cbox.prefs["Locations"]["MountParentDir"], dir) + if (not os.path.islink(abs_dir)) and os.path.isdir(abs_dir) and (not os.path.ismount(abs_dir)): + os.rmdir(abs_dir) + + diff --git a/bin/CryptoBoxExceptions.py b/bin/CryptoBoxExceptions.py new file mode 100644 index 0000000..743bfcd --- /dev/null +++ b/bin/CryptoBoxExceptions.py @@ -0,0 +1,107 @@ +""" +exceptions of the cryptobox package +""" + + +class CryptoBoxError(Exception): + """base class for exceptions of the cryptobox""" + pass + + +class CBConfigError(CryptoBoxError): + """any kind of error related to the configuration of a cryptobox""" + pass + + +class CBConfigUnavailableError(CBConfigError): + """config file/input was not available at all""" + + def __init__(self, source=None): + self.source = source + + def __str__(self): + if self.source: + return "failed to access the configuration of the cryptobox: %s" % self.source + else: + return "failed to access the configuration of the cryptobox" + + +class CBConfigUndefinedError(CBConfigError): + """a specific configuration setting was not defined""" + + def __init__(self, section, name=None): + self.section = section + self.name = name + + def __str__(self): + # is it a settings or a section? + if self.name: + # setting + return "undefined configuration setting: [%s]->%s - please check your configuration file" % (self.section, self.name) + else: + # section + return "undefined configuration section: [%s] - please check your configuration file" % (self.section, ) + + + +class CBConfigInvalidValueError(CBConfigError): + """a configuration setting was invalid somehow""" + + def __init__(self, section, name, value, reason): + self.section = section + self.name = name + self.value = value + self.reason = reason + + def __str__(self): + return "invalid configuration setting [%s]->%s (%s): %s" % (self.section, self.name, self.value, self.reason) + + +class CBEnvironmentError(CryptoBoxError): + """some part of the environment of the cryptobox is broken + e.g. the wrong version of a required program + """ + + def __init__(self, desc): + self.desc = desc + + def __str__(self): + return "misconfiguration detected: %s" % self.desc + + +class CBContainerError(CryptoBoxError): + """any error raised while manipulating a cryptobox container""" + + def __init__(self, desc): + self.desc = desc + + def __str__(self): + return self.desc + +class CBCreateError(CBContainerError): + pass + +class CBVolumeIsActive(CBContainerError): + pass + +class CBInvalidName(CBContainerError): + pass + +class CBNameActivelyUsed(CBContainerError): + pass + +class CBInvalidType(CBContainerError): + pass + +class CBInvalidPassword(CBContainerError): + pass + +class CBChangePasswordError(CBContainerError): + pass + +class CBMountError(CBContainerError): + pass + +class CBUmountError(CBContainerError): + pass + diff --git a/bin/CryptoBoxPlugin.py b/bin/CryptoBoxPlugin.py new file mode 100644 index 0000000..abb3f0c --- /dev/null +++ b/bin/CryptoBoxPlugin.py @@ -0,0 +1,165 @@ +# $Id$ +# +# parent class for all plugins of the CryptoBox +# + +import os +import cherrypy + + +class CryptoBoxPlugin: + + ## default capability is "system" - the other supported capability is: "volume" + pluginCapabilities = [ "system" ] + + ## does this plugin require admin authentification? + requestAuth = False + + ## is this plugin enabled by default? + enabled = True + + ## default rank (0..100) of the plugin in listings (lower value means higher priority) + rank = 80 + + + ## default icon of this plugin (relative path) + defaultIconFileName = "plugin_icon.png" + + + def __init__(self, cbox, pluginDir): + self.cbox = cbox + self.hdf = {} + self.pluginDir = pluginDir + self.hdf_prefix = "Data.Plugins.%s." % self.getName() + + + def doAction(self, **args): + """override doAction with your plugin code""" + raise Exception, "undefined action handler ('doAction') in plugin '%'" % self.getName() + + + def getStatus(self): + """you should override this, to supply useful state information""" + raise Exception, "undefined state handler ('getStatus') in plugin '%'" % self.getName() + + + def getName(self): + """the name of the python file (module) should be the name of the plugin""" + return self.__module__ + + + @cherrypy.expose + def getIcon(self, image=None, **kargs): + """return the image data of the icon of the plugin + + the parameter 'image' may be used for alternative image locations (relative + to the directory of the plugin) + '**kargs' is necessary, as a 'weblang' attribute may be specified (and ignored)""" + import cherrypy, re + if (image is None): # or (re.search(u'[\w-\.]', image)): + plugin_icon_file = os.path.join(self.pluginDir, self.defaultIconFileName) + else: + plugin_icon_file = os.path.join(self.pluginDir, image) + if not os.access(plugin_icon_file, os.R_OK): + plugin_icon_file = os.path.join(self.cbox.prefs["Locations"]["PluginDir"], "plugin_icon_unknown.png") + return cherrypy.lib.cptools.serveFile(plugin_icon_file) + + + def getTemplateFileName(self, template_name): + """return the filename of the template, if it is part of this plugin + + use this function to check, if the plugin provides the specified template + """ + result_file = os.path.join(self.pluginDir, template_name + ".cs") + if os.access(result_file, os.R_OK) and os.path.isfile(result_file): + return result_file + else: + return None + + + def getLanguageData(self, lang="en"): + try: + import neo_cgi, neo_util + except: + raise CryptoBoxExceptions.CBEnvironmentError("couldn't import 'neo_*'! Try 'apt-get install python-clearsilver'.") + langdir = os.path.abspath(os.path.join(self.pluginDir, "lang")) + ## first: the default language file (english) + langFiles = [os.path.join(langdir, "en.hdf")] + ## maybe we have to load a translation afterwards + if lang != "en": + langFiles.append(os.path.join(langdir, lang + ".hdf")) + file_found = False + lang_hdf = neo_util.HDF() + for langFile in langFiles: + if os.access(langFile, os.R_OK): + lang_hdf.readFile(langFile) + file_found = True + if file_found: + return lang_hdf + else: + self.cbox.log.debug("Couldn't find a valid plugin language file (%s)" % str(langFiles)) + return None + + + def loadDataSet(self, hdf): + for (key, value) in self.hdf.items(): + hdf.setValue(key, str(value)) + + + def isAuthRequired(self): + """check if this plugin requires authentication + first step: check plugin configuration + second step: check default value of plugin""" + try: + if self.cbox.prefs.pluginConf[self.getName()]["requestAuth"] is None: + return self.requestAuth + if self.cbox.prefs.pluginConf[self.getName()]["requestAuth"]: + return True + else: + return False + except KeyError: + return self.requestAuth + + + def isEnabled(self): + """check if this plugin is enabled + first step: check plugin configuration + second step: check default value of plugin""" + import types + try: + if self.cbox.prefs.pluginConf[self.getName()]["enabled"] is None: + return self.enabled + if self.cbox.prefs.pluginConf[self.getName()]["enabled"]: + return True + else: + return False + except KeyError: + return self.enabled + + + def getRank(self): + """check the rank of this plugin + first step: check plugin configuration + second step: check default value of plugin""" + try: + if self.cbox.prefs.pluginConf[self.getName()]["rank"] is None: + return self.rank + return int(self.cbox.prefs.pluginConf[self.getName()]["rank"]) + except KeyError, TypeError: + return self.rank + + + def getTestClass(self): + import imp + pl_file = os.path.join(self.pluginDir, "unittests.py") + if os.access(pl_file, os.R_OK) and os.path.isfile(pl_file): + try: + return getattr(imp.load_source("unittests_%s" % self.getName(), pl_file), "unittests") + except AttributeError: + pass + try: + self.cbox.log.info("could not load unittests for plugin: %s" % self.getName()) + except AttributeError: + pass + return None + diff --git a/bin/CryptoBoxRootActions.py b/bin/CryptoBoxRootActions.py new file mode 100755 index 0000000..b92ae3c --- /dev/null +++ b/bin/CryptoBoxRootActions.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python2.4 + +"""module for executing the programs, that need root privileges + +Syntax: + - program + - device + - [action] + - [action args] + +this script will always return with an exitcode 0 (true), if "check" is the only argument +""" + +import os +import sys +import subprocess +import pwd +import grp +import types + +allowedProgs = { + "sfdisk": "/sbin/sfdisk", + "cryptsetup": "/sbin/cryptsetup", + "mount": "/bin/mount", + "umount": "/bin/umount", + "blkid": "/sbin/blkid", + } + + +DEV_TYPES = { "pipe":1, "char":2, "dir":4, "block":6, "file":8, "link":10, "socket":12} + + +def checkIfPluginIsSafe(plugin): + """check if the plugin and its parents are only writeable for root""" + #FIXME: for now we may skip this test - but users will not like it this way :) + return True + props = os.stat(plugin) + ## check if it is owned by non-root + if props.st_uid != 0: return False + ## check group-write permission if gid is not zero + if (props.st_gid != 0) and (props.st_mode % 32 / 16 > 0): return False + ## check if it is world-writeable + if props.st_mode % 4 / 2 > 0: return False + ## are we at root-level (directory-wise)? If yes, then we are ok ... + if plugin == os.path.sep: return True + ## check if the parent directory is ok - recursively :) + return checkIfPluginIsSafe(os.path.dirname(os.path.abspath(plugin))) + + +def checkIfPluginIsValid(plugin): + import imp + try: + x = imp.load_source("cbox_plugin",plugin) + except Exception: + return False + try: + if getattr(x, "PLUGIN_TYPE") == "cryptobox": + return True + else: + return False + except Exception: + return False + + +def call_plugin(args): + """check if the plugin may be called - and do it finally ...""" + plugin = os.path.abspath(args[0]) + del args[0] + ## check existence and excutability + if not os.access(plugin, os.X_OK): + raise Exception, "could not find executable plugin (%s)" % plugin + ## check if the plugin (and its parents) are only writeable for root + if not checkIfPluginIsSafe(plugin): + raise Exception, "the plugin (%s) was not safe - check its (and its parents') permissions" % plugin + ## check if the plugin is a python program, that is marked as a cryptobox plugin + if not checkIfPluginIsValid(plugin): + raise Exception, "the plugin (%s) is not a correctly marked python script" % plugin + args.insert(0,plugin) + proc = subprocess.Popen( + shell = False, + args = args) + proc.wait() + return proc.returncode == 0 + + +def isWriteable(device, force_dev_type=None): + """check if the calling user (not root!) has write access to the device/file + + the real (not the effictive) user id is used for the check + additionally the permissions of the default groups of the real uid are checked + this check works nicely together with "super", as it changes (by default) only + the effective uid (not the real uid) + """ + # first check, if the device/file exists + if not os.path.exists(device): + return False + # check the type of the device - if necessary + if not force_dev_type is None: + dev_type = os.stat(device).st_mode % 65536 / 4096 + if dev_type != force_dev_type: return False + # retrieve the information for the real user id + (trustUserName, trustUID, groupsOfTrustUser) = getUserInfo(os.getuid()) + # set the default groups of the caller for the check (restore them later) + savedGroups = os.getgroups() + os.setgroups(groupsOfTrustUser) + # check permissions + result = os.access(device, os.W_OK) and os.access(device, os.R_OK) + # reset the groups of this process + os.setgroups(savedGroups) + return result + + +def run_cryptsetup(args): + """execute cryptsetup as root + + @args: list of arguments - they will be treated accordingly to the first element + of this list (the action)""" + if not args: raise "WrongArguments", "no action for cryptsetup supplied" + if type(args) != types.ListType: raise "WrongArguments", "invalid arguments supplied: %s" % (args, ) + try: + action = args[0] + del args[0] + device = None + cmd_args = [] + if action == "luksFormat": + device = args[0]; del args[0] + cmd_args.append(action) + cmd_args.append(device) + elif action == "luksUUID": + device = args[0]; del args[0] + cmd_args.append(action) + cmd_args.append(device) + elif action == "luksOpen": + if len(args) < 2: raise "WrongArguments", "missing arguments" + device = args[0]; del args[0] + destination = args[0]; del args[0] + cmd_args.append(action) + cmd_args.append(device) + cmd_args.append(destination) + elif action == "luksClose": + if len(args) < 1: raise "WrongArguments", "missing arguments" + destination = args[0]; del args[0] + # maybe add a check for the mapped device's permissions? + # dmsetup deps self.device + cmd_args.append(action) + cmd_args.append(destination) + elif action == "luksAddKey": + device = args[0]; del args[0] + cmd_args.append(action) + cmd_args.append(device) + elif action == "luksDelKey": + if len(cs_args) < 2: raise "WrongArguments", "missing arguments" + device = args[0]; del args[0] + cmd_args.insert(-1, action) + cmd_args.insert(-1, device) + elif action == "isLuks": + device = args[0]; del args[0] + cmd_args.append(action) + cmd_args.append(device) + else: raise "WrongArguments", "invalid action supplied: %s" % (action, ) + # check if a device was defined - and check it + if (not device is None) and (not isWriteable(device, DEV_TYPES["block"])): + raise "WrongArguments", "%s is not a writeable block device" % (device, ) + cs_args = [allowedProgs["cryptsetup"]] + cs_args.extend(args) + cs_args.extend(cmd_args) + except (TypeError, IndexError): + raise "WrongArguments", "invalid arguments supplied: %s" % (args, ) + # execute cryptsetup with the given parameters + proc = subprocess.Popen( + shell = False, + args = cs_args) + proc.wait() + ## chown the devmapper block device to the cryptobox user + if (proc.returncode == 0) and (action == "luksOpen"): + os.chown(os.path.join(os.path.sep, "dev", "mapper", destination), os.getuid(), os.getgid()) + return proc.returncode == 0 + + +def run_sfdisk(args): + """execute sfdisk for partitioning + + not implemented yet""" + print "ok - you are free to call sfdisk ..." + print " not yet implemented ..." + return True + + +def getFSType(device): + """get the filesystem type of a device""" + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + args = [ allowedProgs["blkid"], + "-s", "TYPE", + "-o", "value", + "-c", os.devnull, + "-w", os.devnull, + device]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + return None + return stdout.strip() + + +def run_mount(args): + """execute mount + """ + if not args: raise "WrongArguments", "no destination for mount supplied" + if type(args) != types.ListType: raise "WrongArguments", "invalid arguments supplied: %s" % (args, ) + try: + device = args[0] + del args[0] + destination = args[0] + del args[0] + # check permissions for the device + if not isWriteable(device, DEV_TYPES["block"]): + raise "WrongArguments", "%s is not a writeable block device" % (device, ) + ## check permissions for the mountpoint + if not isWriteable(destination, DEV_TYPES["dir"]): + raise "WrongArguments", "the mountpoint (%s) is not writeable" % (destination, ) + # check for additional (not allowed) arguments + if len(args) != 0: + raise "WrongArguments", "too many arguments for 'mount': %s" % (args, ) + except TypeError: + raise "WrongArguments", "invalid arguments supplied: %s" % (args, ) + # execute mount with the given parameters + # first overwrite the real uid, as 'mount' wants this to be zero (root) + savedUID = os.getuid() + os.setuid(os.geteuid()) + ## we have to change the permissions of the mounted directory - otherwise it will + ## not be writeable for the cryptobox user + ## for 'vfat' we have to do this during mount + ## for ext2/3 we have to do it afterward + ## first: get the user/group of the target + (trustUserName, trustUID, groupsOfTrustUser) = getUserInfo(savedUID) + trustGID = groupsOfTrustUser[0] + fsType = getFSType(device) + ## define arguments + if fsType == "vfat": + ## add the "uid/gid" arguments to the mount call + mount_args = [allowedProgs["mount"], + "-o", "uid=%d,gid=%d" % (trustUID, trustGID), + device, + destination] + else: + ## all other filesystem types will be handled after mount + mount_args = [allowedProgs["mount"], device, destination] + # execute mount + proc = subprocess.Popen( + shell = False, + args = mount_args) + proc.wait() + ## return in case of an error + if proc.returncode != 0: + return False + ## for vfat: we are done + if fsType == "vfat": return True + ## for all other filesystem types: chown the mount directory + try: + os.chown(destination, trustUID, groupsOfTrustUser[0]) + except OSError, errMsg: + sys.stderr.write("could not chown the mount destination (%s) to the specified user (%d/%d): %s\n" % (destination, trustUID, groupsOfTrustUser[0], errMsg)) + sys.stderr.write("UID: %d\n" % (os.geteuid(),)) + return False + ## BEWARE: it would be nice, if we could restore the previous uid (not euid) but + ## this would also override the euid (see 'man 2 setuid') - any ideas? + return True + + +def run_umount(args): + """execute mount + """ + if not args: raise "WrongArguments", "no mountpoint for umount supplied" + if type(args) != types.ListType: raise "WrongArguments", "invalid arguments supplied" + try: + destination = args[0] + del args[0] + # check permissions for the destination + if not isWriteable(os.path.dirname(destination), DEV_TYPES["dir"]): + raise "WrongArguments", "the parent of the mountpoint (%s) is not writeable" % (destination, ) + if len(args) != 0: raise "WrongArguments", "umount does not allow arguments" + except TypeError: + raise "WrongArguments", "invalid arguments supplied" + # execute umount with the given parameters + # first overwrite the real uid, as 'umount' wants this to be zero (root) + savedUID = os.getuid() + os.setuid(os.geteuid()) + # execute umount (with the parameter '-l' - lazy umount) + proc = subprocess.Popen( + shell = False, + args = [allowedProgs["umount"], "-l", destination]) + proc.wait() + # restore previous real uid + os.setuid(savedUID) + return proc.returncode == 0 + + +def getUserInfo(user): + """return information about the specified user + + @user: (uid or name) + @return: tuple of (name, uid, (groups)) + """ + if user is None: raise "KeyError", "no user supplied" + # first check, if 'user' contains an id - then check for a name + try: + userinfo = pwd.getpwuid(user) + except TypeError: + # if a KeyError is raised again, then the supplied user was invalid + userinfo = pwd.getpwnam(user) + u_groups =[one_group.gr_gid + for one_group in grp.getgrall() + if userinfo.pw_name in one_group.gr_mem] + if not userinfo.pw_gid in u_groups: u_groups.append(userinfo.pw_gid) + return (userinfo.pw_name, userinfo.pw_uid, u_groups) + + +# **************** main ********************** + +# prevent import +if __name__ == "__main__": + + # do we have root privileges (effective uid is zero)? + if os.geteuid() != 0: + sys.stderr.write("the effective uid is not zero - you should use 'super' to call this script (%s)" % sys.argv[0]) + sys.exit(100) + + # remove program name + args = sys.argv[1:] + + # do not allow to use root permissions (real uid may not be zero) + if os.getuid() == 0: + sys.stderr.write("the uid of the caller is zero (root) - this is not allowed\n") + sys.exit(100) + + # check if there were arguments + if (len(args) == 0): + sys.stderr.write("No arguments supplied\n") + sys.exit(100) + + # did the user call the "check" action? + if (len(args) == 1) and (args[0].lower() == "check"): + # exit silently + sys.exit(0) + + if args[0].lower() == "plugin": + del args[0] + try: + isOK = call_plugin(args) + except Exception, errMsg: + sys.stderr.write("Execution of plugin failed: %s\n" % errMsg) + sys.exit(100) + if isOK: + sys.exit(0) + else: + sys.exit(1) + + # check parameters count + if len(args) < 2: + sys.stderr.write("Not enough arguments supplied (%s)!\n" % " ".join(args)) + sys.exit(100) + + progRequest = args[0] + del args[0] + + if not progRequest in allowedProgs.keys(): + sys.stderr.write("Invalid program requested: %s\n" % progRequest) + sys.exit(100) + + if progRequest == "cryptsetup": runner = run_cryptsetup + elif progRequest == "sfdisk": runner = run_sfdisk + elif progRequest == "mount": runner = run_mount + elif progRequest == "umount": runner = run_umount + else: + sys.stderr.write("The interface for this program (%s) is not yet implemented!\n" % progRequest) + sys.exit(100) + try: + if runner(args): + sys.exit(0) + else: + sys.exit(1) + except "WrongArguments", errstr: + sys.stderr.write("Execution failed: %s\n" % errstr) + sys.exit(100) + diff --git a/bin/CryptoBoxSettings.py b/bin/CryptoBoxSettings.py new file mode 100644 index 0000000..73ca9a6 --- /dev/null +++ b/bin/CryptoBoxSettings.py @@ -0,0 +1,481 @@ +import logging +try: + import validate +except: + raise CryptoBoxExceptions.CBEnvironmentError("couldn't import 'validate'! Try 'apt-get install python-formencode'.") +import os +import CryptoBoxExceptions +import subprocess +try: + import configobj ## needed for reading and writing of the config file +except: + raise CryptoBoxExceptions.CBEnvironmentError("couldn't import 'configobj'! Try 'apt-get install python-configobj'.") + + + +class CryptoBoxSettings: + + CONF_LOCATIONS = [ + "./cryptobox.conf", + "~/.cryptobox.conf", + "/etc/cryptobox/cryptobox.conf"] + + NAMEDB_FILE = "cryptobox_names.db" + PLUGINCONF_FILE = "cryptobox_plugins.conf" + USERDB_FILE = "cryptobox_users.db" + + + def __init__(self, config_file=None): + self.log = logging.getLogger("CryptoBox") + config_file = self.__getConfigFileName(config_file) + self.log.info("loading config file: %s" % config_file) + self.prefs = self.__getPreferences(config_file) + self.__validateConfig() + self.__configureLogHandler() + self.__checkUnknownPreferences() + self.preparePartition() + self.nameDB = self.__getNameDatabase() + self.pluginConf = self.__getPluginConfig() + self.userDB = self.__getUserDB() + self.misc_files = self.__getMiscFiles() + + + def write(self): + """ + write all local setting files including the content of the "misc" subdirectory + """ + ok = True + try: + self.nameDB.write() + except IOError: + self.log.warn("could not save the name database") + ok = False + try: + self.pluginConf.write() + except IOError: + self.log.warn("could not save the plugin configuration") + ok = False + try: + self.userDB.write() + except IOError: + self.log.warn("could not save the user database") + ok = False + for misc_file in self.misc_files: + if not misc_file.save(): + self.log.warn("could not save a misc setting file (%s)" % misc_file.filename) + ok = False + return ok + + + def requiresPartition(self): + return bool(self.prefs["Main"]["UseConfigPartition"]) + + + def getActivePartition(self): + settings_dir = self.prefs["Locations"]["SettingsDir"] + if not os.path.ismount(settings_dir): return None + for line in file("/proc/mounts"): + fields = line.split(" ") + mount_dir = fields[1] + try: + if os.path.samefile(mount_dir, settings_dir): return fields[0] + except OSError: + pass + ## no matching entry found + return None + + + def mountPartition(self): + self.log.debug("trying to mount configuration partition") + if not self.requiresPartition(): + self.log.warn("mountConfigPartition: configuration partition is not required - mounting anyway") + if self.getActivePartition(): + self.log.warn("mountConfigPartition: configuration partition already mounted - not mounting again") + return False + confPartitions = self.getAvailablePartitions() + if not confPartitions: + self.log.error("no configuration partitions found - you have to create it first") + return False + partition = confPartitions[0] + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [ + self.prefs["Programs"]["super"], + self.prefs["Programs"]["CryptoBoxRootActions"], + "mount", + partition, + self.prefs["Locations"]["SettingsDir"]]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + self.log.error("failed to mount the configuration partition: %s" % partition) + self.log.error("output of mount: %s" % (stderr,)) + return False + self.log.info("configuration partition mounted: %s" % partition) + return True + + + def umountPartition(self): + if not self.getActivePartition(): + self.log.warn("umountConfigPartition: no configuration partition mounted") + return False + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [ + self.prefs["Programs"]["super"], + self.prefs["Programs"]["CryptoBoxRootActions"], + "umount", + self.prefs["Locations"]["SettingsDir"]]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + self.log.error("failed to unmount the configuration partition") + self.log.error("output of mount: %s" % (stderr,)) + return False + self.log.info("configuration partition unmounted") + return True + + + def getAvailablePartitions(self): + """returns a sequence of found config partitions""" + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + args = [ + self.prefs["Programs"]["blkid"], + "-c", os.path.devnull, + "-t", "LABEL=%s" % self.prefs["Main"]["ConfigVolumeLabel"] ]) + (output, error) = proc.communicate() + if output: + return [e.strip().split(":",1)[0] for e in output.splitlines()] + else: + return [] + + + def preparePartition(self): + if self.requiresPartition() and not self.getActivePartition(): + self.mountPartition() + + + def __getitem__(self, key): + """redirect all requests to the 'prefs' attribute""" + return self.prefs[key] + + + def __getPreferences(self, config_file): + import StringIO + config_rules = StringIO.StringIO(self.validation_spec) + try: + prefs = configobj.ConfigObj(config_file, configspec=config_rules) + if prefs: + self.log.info("found config: %s" % prefs.items()) + else: + raise CryptoBoxExceptions.CBConfigUnavailableError("failed to load the config file: %s" % config_file) + except IOError: + raise CryptoBoxExceptions.CBConfigUnavailableError("unable to open the config file: %s" % config_file) + return prefs + + + def __validateConfig(self): + result = self.prefs.validate(CryptoBoxSettingsValidator(), preserve_errors=True) + error_list = configobj.flatten_errors(self.prefs, result) + if not error_list: return + errorMsgs = [] + for sections, key, text in error_list: + section_name = "->".join(sections) + if not text: + errorMsg = "undefined configuration value (%s) in section '%s'" % (key, section_name) + else: + errorMsg = "invalid configuration value (%s) in section '%s': %s" % (key, section_name, text) + errorMsgs.append(errorMsg) + raise CryptoBoxExceptions.CBConfigError, "\n".join(errorMsgs) + + + def __checkUnknownPreferences(self): + import StringIO + config_rules = configobj.ConfigObj(StringIO.StringIO(self.validation_spec), list_values=False) + self.__recursiveConfigSectionCheck("", self.prefs, config_rules) + + + def __recursiveConfigSectionCheck(self, section_path, section_config, section_rules): + """should be called by '__checkUnknownPreferences' for every section + sends a warning message to the logger for every undefined (see validation_spec) + configuration setting + """ + for e in section_config.keys(): + element_path = section_path + e + if e in section_rules.keys(): + if isinstance(section_config[e], configobj.Section): + if isinstance(section_rules[e], configobj.Section): + self.__recursiveConfigSectionCheck(element_path + "->", section_config[e], section_rules[e]) + else: + self.log.warn("configuration setting should be a value instead of a section name: %s" % element_path) + else: + if not isinstance(section_rules[e], configobj.Section): + pass # good - the setting is valid + else: + self.log.warn("configuration setting should be a section name instead of a value: %s" % element_path) + else: + self.log.warn("unknown configuration setting: %s" % element_path) + + + def __getNameDatabase(self): + try: + try: + nameDB_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.NAMEDB_FILE) + except KeyError: + raise CryptoBoxExceptions.CBConfigUndefinedError("Locations", "SettingsDir") + except SyntaxError: + raise CryptoBoxExceptions.CBConfigInvalidValueError("Locations", "SettingsDir", nameDB_file, "failed to interprete the filename of the name database correctly (%s)" % nameDB_file) + ## create nameDB if necessary + if os.path.exists(nameDB_file): + nameDB = configobj.ConfigObj(nameDB_file) + else: + nameDB = configobj.ConfigObj(nameDB_file, create_empty=True) + ## check if nameDB file was created successfully? + if not os.path.exists(nameDB_file): + raise CryptoBoxExceptions.CBEnvironmentError("failed to create name database (%s)" % nameDB_file) + return nameDB + + + def __getPluginConfig(self): + import StringIO + plugin_rules = StringIO.StringIO(self.pluginValidationSpec) + try: + try: + pluginConf_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.PLUGINCONF_FILE) + except KeyError: + raise CryptoBoxExceptions.CBConfigUndefinedError("Locations", "SettingsDir") + except SyntaxError: + raise CryptoBoxExceptions.CBConfigInvalidValueError("Locations", "SettingsDir", pluginConf_file, "failed to interprete the filename of the plugin config file correctly (%s)" % pluginConf_file) + ## create pluginConf_file if necessary + if os.path.exists(pluginConf_file): + pluginConf = configobj.ConfigObj(pluginConf_file, configspec=plugin_rules) + else: + pluginConf = configobj.ConfigObj(pluginConf_file, configspec=plugin_rules, create_empty=True) + ## validate and convert values according to the spec + pluginConf.validate(validate.Validator()) + ## check if pluginConf_file file was created successfully? + if not os.path.exists(pluginConf_file): + raise CryptoBoxExceptions.CBEnvironmentError("failed to create plugin configuration file (%s)" % pluginConf_file) + return pluginConf + + + def __getUserDB(self): + import StringIO, sha + userDB_rules = StringIO.StringIO(self.userDatabaseSpec) + try: + try: + userDB_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.USERDB_FILE) + except KeyError: + raise CryptoBoxExceptions.CBConfigUndefinedError("Locations", "SettingsDir") + except SyntaxError: + raise CryptoBoxExceptions.CBConfigInvalidValueError("Locations", "SettingsDir", userDB_file, "failed to interprete the filename of the users database file correctly (%s)" % userDB_file) + ## create userDB_file if necessary + if os.path.exists(userDB_file): + userDB = configobj.ConfigObj(userDB_file, configspec=userDB_rules) + else: + userDB = configobj.ConfigObj(userDB_file, configspec=userDB_rules, create_empty=True) + ## validate and set default value for "admin" user + userDB.validate(validate.Validator()) + ## check if userDB file was created successfully? + if not os.path.exists(userDB_file): + raise CryptoBoxExceptions.CBEnvironmentError("failed to create user database file (%s)" % userDB_file) + ## define password hash function - never use "sha" directly - SPOT + userDB.getDigest = lambda password: sha.new(password).hexdigest() + return userDB + + + def __getMiscFiles(self): + misc_dir = os.path.join(self.prefs["Locations"]["SettingsDir"], "misc") + if (not os.path.isdir(misc_dir)) or (not os.access(misc_dir, os.X_OK)): + return [] + return [MiscConfigFile(os.path.join(misc_dir, f), self.log) + for f in os.listdir(misc_dir) + if os.path.isfile(os.path.join(misc_dir, f))] + + + def __getConfigFileName(self, config_file): + # search for the configuration file + import types + if config_file is None: + # no config file was specified - we will look for it in the ususal locations + conf_file_list = [os.path.expanduser(f) + for f in self.CONF_LOCATIONS + if os.path.exists(os.path.expanduser(f))] + if not conf_file_list: + # no possible config file found in the usual locations + raise CryptoBoxExceptions.CBConfigUnavailableError() + config_file = conf_file_list[0] + else: + # a config file was specified (e.g. via command line) + if type(config_file) != types.StringType: + raise CryptoBoxExceptions.CBConfigUnavailableError("invalid config file specified: %s" % config_file) + if not os.path.exists(config_file): + raise CryptoBoxExceptions.CBConfigUnavailableError("could not find the specified configuration file (%s)" % config_file) + return config_file + + + def __configureLogHandler(self): + try: + log_level = self.prefs["Log"]["Level"].upper() + log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"] + if not log_level in log_level_avail: + raise TypeError + except KeyError: + raise CryptoBoxExceptions.CBConfigUndefinedError("Log", "Level") + except TypeError: + raise CryptoBoxExceptions.CBConfigInvalidValueError("Log", "Level", log_level, "invalid log level: only %s are allowed" % log_level_avail) + try: + try: + log_handler = logging.FileHandler(self.prefs["Log"]["Details"]) + except KeyError: + raise CryptoBoxExceptions.CBConfigUndefinedError("Log", "Details") + except IOError: + raise CryptoBoxExceptions.CBEnvironmentError("could not create the log file (%s)" % self.prefs["Log"]["Details"]) + log_handler.setFormatter(logging.Formatter('%(asctime)s CryptoBox %(levelname)s: %(message)s')) + cbox_log = logging.getLogger("CryptoBox") + ## remove previous handlers + cbox_log.handlers = [] + ## add new one + cbox_log.addHandler(log_handler) + ## do not call parent's handlers + cbox_log.propagate = False + ## 'log_level' is a string -> use 'getattr' + cbox_log.setLevel(getattr(logging,log_level)) + ## the logger named "CryptoBox" is configured now + + + validation_spec = """ +[Main] +AllowedDevices = list(min=1) +DefaultVolumePrefix = string(min=1) +DefaultCipher = string(default="aes-cbc-essiv:sha256") +ConfigVolumeLabel = string(min=1, default="cbox_config") +UseConfigPartition = integer(min=0, max=1, default=0) + +[Locations] +MountParentDir = directoryExists(default="/var/cache/cryptobox/mnt") +SettingsDir = directoryExists(default="/var/cache/cryptobox/settings") +TemplateDir = directoryExists(default="/usr/share/cryptobox/template") +LangDir = directoryExists(default="/usr/share/cryptobox/lang") +DocDir = directoryExists(default="/usr/share/doc/cryptobox/html") +PluginDir = directoryExists(default="/usr/share/cryptobox/plugins") + +[Log] +Level = option("debug", "info", "warn", "error", default="warn") +Destination = option("file", default="file") +Details = string(min=1) + +[WebSettings] +Stylesheet = string(min=1) +Language = string(min=1, default="en") + +[Programs] +cryptsetup = fileExecutable(default="/sbin/cryptsetup") +mkfs-data = fileExecutable(default="/sbin/mkfs.ext3") +blkid = fileExecutable(default="/sbin/blkid") +blockdev = fileExecutable(default="/sbin/blockdev") +mount = fileExecutable(default="/bin/mount") +umount = fileExecutable(default="/bin/umount") +super = fileExecutable(default="/usr/bin/super") +# this is the "program" name as defined in /etc/super.tab +CryptoBoxRootActions = string(min=1) + """ + + pluginValidationSpec = """ +[__many__] +enabled = boolean(default=None) +requestAuth = boolean(default=None) +rank = integer(default=None) + """ + + userDatabaseSpec = """ +[admins] +admin = string(default=d033e22ae348aeb5660fc2140aec35850c4da997) + """ + + +class CryptoBoxSettingsValidator(validate.Validator): + + def __init__(self): + validate.Validator.__init__(self) + self.functions["directoryExists"] = self.check_directoryExists + self.functions["fileExecutable"] = self.check_fileExecutable + self.functions["fileWriteable"] = self.check_fileWriteable + + + def check_directoryExists(self, value): + dir_path = os.path.abspath(value) + if not os.path.isdir(dir_path): + raise validate.VdtValueError("%s (not found)" % value) + if not os.access(dir_path, os.X_OK): + raise validate.VdtValueError("%s (access denied)" % value) + return dir_path + + + def check_fileExecutable(self, value): + file_path = os.path.abspath(value) + if not os.path.isfile(file_path): + raise validate.VdtValueError("%s (not found)" % value) + if not os.access(file_path, os.X_OK): + raise validate.VdtValueError("%s (access denied)" % value) + return file_path + + + def check_fileWriteable(self, value): + file_path = os.path.abspath(value) + if os.path.isfile(file_path): + if not os.access(file_path, os.W_OK): + raise validate.VdtValueError("%s (not found)" % value) + else: + parent_dir = os.path.dirname(file_path) + if os.path.isdir(parent_dir) and os.access(parent_dir, os.W_OK): + return file_path + raise validate.VdtValueError("%s (directory does not exist)" % value) + return file_path + + + +class MiscConfigFile: + + maxSize = 20480 + + def __init__(self, filename, logger): + self.filename = filename + self.log = logger + self.load() + + + def load(self): + fd = open(self.filename, "rb") + ## limit the maximum size + self.content = fd.read(self.maxSize) + if fd.tell() == self.maxSize: + self.log.warn("file in misc settings directory (%s) is bigger than allowed (%s)" % (self.filename, self.maxSize)) + fd.close() + + + def save(self): + save_dir = os.path.dirname(self.filename) + ## create the directory, if necessary + if not os.path.isdir(save_dir): + try: + os.mkdir(save_dir) + except IOError: + return False + ## save the content of the file + try: + fd = open(self.filename, "wb") + except IOError: + return False + try: + fd.write(self.content) + fd.close() + return True + except IOError: + fd.close() + return False + diff --git a/bin/CryptoBoxTools.py b/bin/CryptoBoxTools.py new file mode 100644 index 0000000..25ffa03 --- /dev/null +++ b/bin/CryptoBoxTools.py @@ -0,0 +1,186 @@ +import logging +import os +import re + +logger = logging.getLogger("CryptoBox") + + +def getAvailablePartitions(): + "retrieve a list of all available containers" + ret_list = [] + try: + "the following reads all lines of /proc/partitions and adds the mentioned devices" + fpart = open("/proc/partitions", "r") + try: + line = fpart.readline() + while line: + p_details = line.split() + if (len(p_details) == 4): + "the following code prevents double entries like /dev/hda and /dev/hda1" + (p_major, p_minor, p_size, p_device) = p_details + ## ignore lines with: invalid minor/major or extend partitions (size=1) + if re.search('^[0-9]*$', p_major) and re.search('^[0-9]*$', p_minor) and (p_size != "1"): + p_parent = re.sub('[1-9]?[0-9]$', '', p_device) + if p_parent == p_device: + if [e for e in ret_list if re.search('^' + p_parent + '[1-9]?[0-9]$', e)]: + "major partition - its children are already in the list" + pass + else: + "major partition - but there are no children for now" + ret_list.append(p_device) + else: + "minor partition - remove parent if necessary" + if p_parent in ret_list: ret_list.remove(p_parent) + ret_list.append(p_device) + line = fpart.readline() + finally: + fpart.close() + return map(getAbsoluteDeviceName, ret_list) + except IOError: + logger.warning("Could not read /proc/partitions") + return [] + + +def getAbsoluteDeviceName(shortname): + """ returns the absolute file name of a device (e.g.: "hda1" -> "/dev/hda1") + this does also work for device mapper devices + if the result is non-unique, one arbitrary value is returned""" + if re.search('^/', shortname): return shortname + default = os.path.join("/dev", shortname) + if os.path.exists(default): return default + result = findMajorMinorOfDevice(shortname) + "if no valid major/minor was found -> exit" + if not result: return default + (major, minor) = result + "for device-mapper devices (major == 254) ..." + if major == 254: + result = findMajorMinorDeviceName("/dev/mapper", major, minor) + if result: return result[0] + "now check all files in /dev" + result = findMajorMinorDeviceName("/dev", major, minor) + if result: return result[0] + return default + + +def findMajorMinorOfDevice(device): + "return the major/minor numbers of a block device" + if re.match("/", device) or not os.path.exists(os.path.join(os.path.sep,"sys","block",device)): + ## maybe it is an absolute device name + if not os.path.exists(device): return None + ## okay - it seems to to a device node + rdev = os.stat(device).st_rdev + return (os.major(rdev), os.minor(rdev)) + blockdev_info_file = os.path.join(os.path.join(os.path.sep,"sys","block", device), "dev") + try: + f_blockdev_info = open(blockdev_info_file, "r") + blockdev_info = f_blockdev_info.read() + f_blockdev_info.close() + (str_major, str_minor) = blockdev_info.split(":") + "numeric conversion" + try: + major = int(str_major) + minor = int(str_minor) + return (major, minor) + except ValueError: + "unknown device numbers -> stop guessing" + return None + except IOError: + pass + return None + + +def findMajorMinorDeviceName(dir, major, minor): + "returns the names of devices with the specified major and minor number" + collected = [] + try: + subdirs = [os.path.join(dir, e) for e in os.listdir(dir) if (not os.path.islink(os.path.join(dir, e))) and os.path.isdir(os.path.join(dir, e))] + "do a recursive call to parse the directory tree" + for dirs in subdirs: + collected.extend(findMajorMinorDeviceName(dirs, major, minor)) + "filter all device inodes in this directory" + collected.extend([os.path.realpath(os.path.join(dir, e)) for e in os.listdir(dir) if (os.major(os.stat(os.path.join(dir, e)).st_rdev) == major) and (os.minor(os.stat(os.path.join(dir, e)).st_rdev) == minor)]) + ## remove double entries + result = [] + for e in collected: + if e not in result: result.append(e) + return result + except OSError: + return [] + + +def getParentBlockDevices(): + devs = [] + for line in file("/proc/partitions"): + p_details = line.split() + ## we expect four values - otherwise continue with next iteration + if len(p_details) != 4: continue + (p_major, p_minor, p_size, p_device) = p_details + ## we expect numeric values in the first two columns + if re.search(u'\D',p_major) or re.search(u'\D',p_minor): continue + ## now let us check, if it is a (parent) block device or a partition + if not os.path.isdir(os.path.join(os.path.sep, "sys", "block", p_device)): continue + devs.append(p_device) + return map(getAbsoluteDeviceName, devs) + + +def isPartOfBlockDevice(parent, subdevice): + """check if the given block device is a parent of 'subdevice' + e.g. for checking if a partition belongs to a block device""" + try: + (par_major, par_minor) = findMajorMinorOfDevice(parent) + (sub_major, sub_minor) = findMajorMinorOfDevice(subdevice) + except TypeError: + ## at least one of these devices did not return a valid major/minor combination + return False + ## search the entry below '/sys/block' belonging to the parent + root = os.path.join(os.path.sep, 'sys', 'block') + for bldev in os.listdir(root): + blpath = os.path.join(root, bldev, 'dev') + if os.access(blpath, os.R_OK): + try: + if (str(par_major), str(par_minor)) == tuple([e for e in file(blpath)][0].strip().split(":",1)): + parent_path = os.path.join(root, bldev) + break + except IndexError, OSError: + pass + else: + ## no block device with this major/minor combination found below '/sys/block' + return False + for subbldev in os.listdir(parent_path): + subblpath = os.path.join(parent_path, subbldev, "dev") + if os.access(subblpath, os.R_OK): + try: + if (str(sub_major), str(sub_minor)) == tuple([e for e in file(subblpath)][0].strip().split(":",1)): + ## the name of the subdevice node is not important - we found it! + return True + except IndexError, OSError: + pass + return False + + +def getBlockDeviceSize(device): + if not device: return -1 + try: + rdev = os.stat(device).st_rdev + except OSError: + return -1 + minor = os.minor(rdev) + major = os.major(rdev) + for f in file("/proc/partitions"): + try: + elements = f.split() + if len(elements) != 4: continue + if (int(elements[0]) == major) and (int(elements[1]) == minor): + return int(elements[2])/1024 + except ValueError: + pass + return -1 + + +def getBlockDeviceSizeHumanly(device): + size = getBlockDeviceSize(device) + if size > 5120: + return "%sGB" % size/1024 + else: + return "%sMB" % size + diff --git a/bin/CryptoBoxWebserver.py b/bin/CryptoBoxWebserver.py new file mode 100755 index 0000000..b841262 --- /dev/null +++ b/bin/CryptoBoxWebserver.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python2.4 +import os +import WebInterfaceSites +import sys + +try: + import cherrypy +except: + print "Could not import the cherrypy module! Try 'apt-get install python-cherrypy'." + sys.exit(1) + +class CryptoBoxWebserver: + '''this class starts the cherryp webserver and serves the single sites''' + + def __init__(self): + cherrypy.root = WebInterfaceSites.WebInterfaceSites() + #expose static content: + #I currently have no idea how to cleanly extract the stylesheet path from + #the config object without an extra CryptoBox.CryptoBoxProps instance. + #perhaps put config handling into a seperate class in CryptoBox.py? + # + # the following manual mapping is necessary, as we may not use relative + # paths in the config file + cherrypy.config.configMap.update({ + "/cryptobox-misc": { + "staticFilter.on" : True, + "staticFilter.dir": os.path.abspath("../www-data" )} + }) + + def start(self): + # just use this config, when we're started directly + cherrypy.config.update(file = "cryptoboxwebserver.conf") + cherrypy.server.start() + +if __name__ == "__main__": + cbw = CryptoBoxWebserver() + cbw.start() + diff --git a/bin/Plugins.py b/bin/Plugins.py new file mode 100644 index 0000000..97a7e83 --- /dev/null +++ b/bin/Plugins.py @@ -0,0 +1,67 @@ +# $Id$ + +import imp +import os +import logging + + +class PluginManager: + """manage available plugins""" + + def __init__(self, cbox, plugin_dirs="."): + self.cbox = cbox + self.log = logging.getLogger("CryptoBox") + if hasattr(plugin_dirs, "__iter__"): + self.plugin_dirs = [os.path.abspath(dir) for dir in plugin_dirs] + else: + self.plugin_dirs = [os.path.abspath(plugin_dirs)] + self.pluginList = self.__getAllPlugins() + + + def getPlugins(self): + return self.pluginList[:] + + + def getPlugin(self, name): + for p in self.pluginList[:]: + if p.getName() == name: + return p + return None + + + def __getAllPlugins(self): + list = [] + for plfile in self.__getPluginFiles(): + list.append(self.__getPluginClass(os.path.basename(plfile)[:-3])) + return list + + + def __getPluginClass(self, name): + for plfile in self.__getPluginFiles(): + if name == os.path.basename(plfile)[:-3]: + try: + pl_class = getattr(imp.load_source(name, plfile), name) + except AttributeError: + return None + return pl_class(self.cbox, os.path.dirname(plfile)) + else: + return None + + + def __getPluginFiles(self): + result = [] + for dir in [os.path.abspath(e) for e in self.plugin_dirs if os.access(e, os.R_OK) and os.path.isdir(e)]: + for plname in [f for f in os.listdir(dir)]: + pldir = os.path.join(dir, plname) + plfile = os.path.join(pldir, plname + ".py") + if os.path.isfile(plfile) and os.access(plfile, os.R_OK): + result.append(plfile) + return result + + +if __name__ == "__main__": + x = PluginManager(None, "../plugins") + for a in x.getPlugins(): + if not a is None: + print "Plugin: %s" % a.getName() + diff --git a/bin/WebInterfaceDataset.py b/bin/WebInterfaceDataset.py new file mode 100644 index 0000000..7f2de6c --- /dev/null +++ b/bin/WebInterfaceDataset.py @@ -0,0 +1,136 @@ +import os +import CryptoBoxContainer +import CryptoBoxTools + +## useful constant for some functions +CONT_TYPES = CryptoBoxContainer.CryptoBoxContainer.Types + +class WebInterfaceDataset(dict): + """this class contains all data that should be available for the clearsilver + templates + """ + + def __init__(self, cbox, prefs, plugins): + self.prefs = prefs + self.cbox = cbox + self.__setConfigValues() + self.plugins = plugins + self.setCryptoBoxState() + self.setPluginData() + self.setContainersState() + + + def setCryptoBoxState(self): + import cherrypy + self["Data.Version"] = self.cbox.VERSION + langs = self.cbox.getAvailableLanguages() + langs.sort() + for (index, lang) in enumerate(langs): + self.cbox.log.info("language loaded: %s" % lang) + self["Data.Languages.%d.name" % index] = lang + self["Data.Languages.%d.link" % index] = self.__getLanguageName(lang) + try: + self["Data.ScriptURL.Prot"] = cherrypy.request.scheme + host = cherrypy.request.headers["Host"] + self["Data.ScriptURL.Host"] = host.split(":",1)[0] + complete_url = "%s://%s" % (self["Data.ScriptURL.Prot"], self["Data.ScriptURL.Host"]) + try: + port = int(host.split(":",1)[1]) + complete_url += ":%s" % port + except (IndexError, ValueError): + if cherrypy.request.scheme == "http": + port = 80 + elif cherrypy.request.scheme == "https": + port = 443 + else: + ## unknown scheme -> port 0 + self.cbox.log.info("unknown protocol scheme used: %s" % (cherrypy.request.scheme,)) + port = 0 + self["Data.ScriptURL.Port"] = port + ## retrieve the relative address of the CGI (or the cherrypy base address) + ## remove the last part of the url and add a slash + path = "/".join(cherrypy.request.path.split("/")[:-1]) + "/" + self["Data.ScriptURL.Path"] = path + complete_url += path + self["Data.ScriptURL"] = complete_url + except AttributeError: + self["Data.ScriptURL"] = "" + + + def setCurrentDiskState(self, device): + for container in self.cbox.getContainerList(): + if container.getDevice() == device: + isEncrypted = (container.getType() == CONT_TYPES["luks"]) and 1 or 0 + isPlain = (container.getType() == CONT_TYPES["plain"]) and 1 or 0 + isMounted = container.isMounted() and 1 or 0 + self["Data.CurrentDisk.device"] = container.getDevice() + self["Data.CurrentDisk.name"] = container.getName() + self["Data.CurrentDisk.encryption"] = isEncrypted + self["Data.CurrentDisk.plaintext"] = isPlain + self["Data.CurrentDisk.active"] = isMounted + self["Data.CurrentDisk.size"] = CryptoBoxTools.getBlockDeviceSizeHumanly(container.getDevice()) + if isMounted: + (size, avail, used) = container.getCapacity() + percent = used / size + self["Data.CurrentDisk.capacity.used"] = used + self["Data.CurrentDisk.capacity.free"] = avail + self["Data.CurrentDisk.capacity.size"] = size + self["Data.CurrentDisk.capacity.percent"] = percent + self["Settings.LinkAttrs.device"] = device + + + def setContainersState(self): + avail_counter = 0 + active_counter = 0 + for container in self.cbox.getContainerList(): + ## useful if the container was changed during an action + container.resetObject() + isEncrypted = (container.getType() == CONT_TYPES["luks"]) and 1 or 0 + isPlain = (container.getType() == CONT_TYPES["plain"]) and 1 or 0 + isMounted = container.isMounted() and 1 or 0 + self["Data.Disks.%d.device" % avail_counter] = container.getDevice() + self["Data.Disks.%d.name" % avail_counter] = container.getName() + self["Data.Disks.%d.encryption" % avail_counter] = isEncrypted + self["Data.Disks.%d.plaintext" % avail_counter] = isPlain + self["Data.Disks.%d.active" % avail_counter] = isMounted + self["Data.Disks.%d.size" % avail_counter] = CryptoBoxTools.getBlockDeviceSizeHumanly(container.getDevice()) + if isMounted: active_counter += 1 + avail_counter += 1 + self["Data.activeDisksCount"] = active_counter + + + def setPluginData(self): + for p in self.plugins: + lang_data = p.getLanguageData() + entryName = "Settings.PluginList." + p.getName() + self[entryName] = p.getName() + self[entryName + ".Link"] = lang_data.getValue("Link", p.getName()) + self[entryName + ".Rank"] = p.getRank() + self[entryName + ".RequestAuth"] = p.isAuthRequired() and "1" or "0" + self[entryName + ".Enabled"] = p.isEnabled() and "1" or "0" + for a in p.pluginCapabilities: + self[entryName + ".Types." + a] = "1" + + + def __setConfigValues(self): + self["Settings.TemplateDir"] = os.path.abspath(self.prefs["Locations"]["TemplateDir"]) + self["Settings.LanguageDir"] = os.path.abspath(self.prefs["Locations"]["LangDir"]) + self["Settings.DocDir"] = os.path.abspath(self.prefs["Locations"]["DocDir"]) + self["Settings.Stylesheet"] = self.prefs["WebSettings"]["Stylesheet"] + self["Settings.Language"] = self.prefs["WebSettings"]["Language"] + self["Settings.PluginDir"] = self.prefs["Locations"]["PluginDir"] + self["Settings.SettingsDir"] = self.prefs["Locations"]["SettingsDir"] + + + def __getLanguageName(self, lang): + try: + import neo_cgi, neo_util, neo_cs + except: + raise CryptoBoxExceptions.CBEnvironmentError("couldn't import 'neo_*'! Try 'apt-get install python-clearsilver'.") + hdf_path = os.path.join(self.prefs["Locations"]["LangDir"], lang + ".hdf") + hdf = neo_util.HDF() + hdf.readFile(hdf_path) + return hdf.getValue("Name",lang) + + + diff --git a/bin/WebInterfaceSites.py b/bin/WebInterfaceSites.py new file mode 100755 index 0000000..82405e9 --- /dev/null +++ b/bin/WebInterfaceSites.py @@ -0,0 +1,427 @@ +import CryptoBox +import WebInterfaceDataset +import re +import Plugins +from CryptoBoxExceptions import * +import cherrypy +import types +import os + +try: + import neo_cgi, neo_util, neo_cs +except ImportError: + errorMsg = "Could not import clearsilver module. Try 'apt-get install python-clearsilver'." + self.log.error(errorMsg) + sys.stderr.write(errorMsg) + raise ImportError, errorMsg + + + +class PluginIconHandler: + + def __init__(self, plugins): + for plugin in plugins.getPlugins(): + if not plugin: continue + plname = plugin.getName() + ## expose the getIcon function of this plugin + setattr(self, plname, plugin.getIcon) + + + +class WebInterfaceSites: + ''' + ''' + + ## this template is used under strange circumstances + defaultTemplate = "empty" + + + def __init__(self): + import logging + self.cbox = CryptoBox.CryptoBoxProps() + self.log = logging.getLogger("CryptoBox") + self.prefs = self.cbox.prefs + self.__resetDataset() + + + def __resetDataset(self): + """this method has to be called at the beginning of every "site" action + important: only at the beginning of an action (to not loose information) + important: for _every_ "site" action (cherrypy is stateful) + also take care for the plugins, as they also contain datasets + """ + self.__loadPlugins() + self.dataset = WebInterfaceDataset.WebInterfaceDataset(self.cbox, self.prefs, self.pluginList.getPlugins()) + ## publish plugin icons + self.icons = PluginIconHandler(self.pluginList) + self.icons.exposed = True + ## check, if a configuration partition has become available + self.cbox.prefs.preparePartition() + + + def __loadPlugins(self): + self.pluginList = Plugins.PluginManager(self.cbox, self.prefs["Locations"]["PluginDir"]) + for plugin in self.pluginList.getPlugins(): + if not plugin: continue + plname = plugin.getName() + if plugin.isEnabled(): + self.cbox.log.info("Plugin '%s' loaded" % plname) + ## this should be the "easiest" way to expose all plugins as URLs + setattr(self, plname, self.return_plugin_action(plugin)) + setattr(getattr(self, plname), "exposed", True) + # TODO: check, if this really works - for now the "stream_response" feature seems to be broken + #setattr(getattr(self, plname), "stream_respones", True) + else: + self.cbox.log.info("Plugin '%s' is disabled" % plname) + ## remove the plugin, if it was active before + setattr(self, plname, None) + + + ## this is a function decorator to check authentication + ## it has to be defined before any page definition requiring authentification + def __requestAuth(self=None): + def check_credentials(site): + def _inner_wrapper(self, *args, **kargs): + import base64 + ## define a "non-allowed" function + user, password = None, None + try: + resp = cherrypy.request.headers["Authorization"][6:] # ignore "Basic " + (user, password) = base64.b64decode(resp).split(":",1) + except KeyError: + ## no "authorization" header was sent + pass + except TypeError: + ## invalid base64 string + pass + except AttributeError: + ## no cherrypy response header defined + pass + authDict = self.cbox.prefs.userDB["admins"] + if user in authDict.keys(): + if self.cbox.prefs.userDB.getDigest(password) == authDict[user]: + ## ok: return the choosen page + self.cbox.log.info("access granted for: %s" % user) + return site(self, *args, **kargs) + else: + self.cbox.log.info("wrong password supplied for: %s" % user) + else: + self.cbox.log.info("unknown user: %s" % str(user)) + ## wrong credentials: return "access denied" + cherrypy.response.headers["WWW-Authenticate"] = '''Basic realm="CryptoBox"''' + cherrypy.response.status = 401 + return self.__render("access_denied") + return _inner_wrapper + return check_credentials + + + ###################################################################### + ## put real sites down here and don't forget to expose them at the end + + + @cherrypy.expose + def index(self, weblang=""): + self.__resetDataset() + self.__setWebLang(weblang) + self.__checkEnvironment() + ## do not forget the language! + param_dict = {"weblang":weblang} + ## render "disks" plugin by default + return self.return_plugin_action(self.pluginList.getPlugin("disks"))(**param_dict) + + + def return_plugin_action(self, plugin): + def handler(self, **args): + self.__resetDataset() + self.__checkEnvironment() + args_orig = dict(args) + ## set web interface language + try: + self.__setWebLang(args["weblang"]) + del args["weblang"] + except KeyError: + self.__setWebLang("") + ## we always read the "device" setting - otherwise volume-plugin links + ## would not work easily (see "volume_props" linking to "format_fs") + ## it will get ignored for non-volume plugins + try: + plugin.device = None + if self.__setDevice(args["device"]): + plugin.device = args["device"] + del args["device"] + except KeyError: + pass + ## check the device argument of volume plugins + if "volume" in plugin.pluginCapabilities: + ## initialize the dataset of the selected device if necessary + if plugin.device: + self.dataset.setCurrentDiskState(plugin.device) + else: + ## invalid (or missing) device setting + return self.__render(self.defaultTemplate) + ## check if there is a "redirect" setting - this will override the return + ## value of the doAction function (e.g. useful for umount-before-format) + try: + if args["redirect"]: + override_nextTemplate = { "plugin":args["redirect"] } + if "volume" in plugin.pluginCapabilities: + override_nextTemplate["values"] = {"device":plugin.device} + del args["redirect"] + except KeyError: + override_nextTemplate = None + ## call the plugin handler + nextTemplate = plugin.doAction(**args) + ## for 'volume' plugins: reread the dataset of the current disk + ## additionally: set the default template for plugins + if "volume" in plugin.pluginCapabilities: + ## maybe the state of the current volume was changed? + self.dataset.setCurrentDiskState(plugin.device) + if not nextTemplate: nextTemplate = { "plugin":"volume_mount", "values":{"device":plugin.device}} + else: + ## maybe a non-volume plugin changed some plugin settings (e.g. plugin_manager) + self.dataset.setPluginData() + ## update the container hdf-dataset (maybe a plugin changed the state of a container) + self.dataset.setContainersState() + ## default page for non-volume plugins is the disk selection + if not nextTemplate: nextTemplate = { "plugin":"disks", "values":{} } + ## was a redirect requested? + if override_nextTemplate: + nextTemplate = override_nextTemplate + ## if another plugins was choosen for 'nextTemplate', then do it! + if isinstance(nextTemplate, types.DictType) \ + and "plugin" in nextTemplate.keys() \ + and "values" in nextTemplate.keys() \ + and self.pluginList.getPlugin(nextTemplate["plugin"]): + valueDict = dict(nextTemplate["values"]) + ## force the current weblang attribute - otherwise it gets lost + valueDict["weblang"] = self.dataset["Settings.Language"] + new_plugin = self.pluginList.getPlugin(nextTemplate["plugin"]) + return self.return_plugin_action(new_plugin)(**valueDict) + ## save the currently active plugin name + self.dataset["Data.ActivePlugin"] = plugin.getName() + return self.__render(nextTemplate, plugin) + ## apply authentication? + if plugin.isAuthRequired(): + return lambda **args: self.__requestAuth()(handler)(self, **args) + else: + return lambda **args: handler(self, **args) + + + ## test authentication + @cherrypy.expose + @__requestAuth + def test(self, weblang=""): + self.__resetDataset() + self.__setWebLang(weblang) + self.__checkEnvironment() + return "test passed" + + + @cherrypy.expose + def test_stream(self): + """just for testing purposes - to check if the "stream_response" feature + actually works - for now (September 02006) it does not seem to be ok""" + import time + yield "neu

" + + + + ##################### input checker ########################## + + def __checkEnvironment(self): + """here we should place all interesting checks to inform the user of problems + + examples are: non-https, readonly-config, ... + """ + ## TODO: maybe add an option "mount"? + if self.cbox.prefs.requiresPartition() and not self.cbox.prefs.getActivePartition(): + self.dataset["Data.EnvironmentWarning"] = "ReadOnlyConfig" + # TODO: turn this on soon (add "not") - for now it is annoying + if self.__checkHTTPS(): + self.dataset["Data.EnvironmentWarning"] = "NoSSL" + + + def __checkHTTPS(self): + ## check the request scheme + if cherrypy.request.scheme == "https": return True + ## check an environment setting - this is quite common behind proxies + try: + if os.environ["HTTPS"]: return True + except KeyError: + pass + ## check http header TODO (check pound for the name) + try: + if cherrypy.request.headers["TODO"]: return True + except KeyError: + pass + ## the connection seems to be unencrypted + return False + + + def __setWebLang(self, value): + guess = value + availLangs = self.cbox.getAvailableLanguages() + ## no language specified: check browser language + if not guess: + guess = self.__getPreferredBrowserLanguage(availLangs) + ## no preferred language or invalid language? + if not guess \ + or not guess in availLangs \ + or re.search(u'\W', guess): + ## warn only for invalid languages + if not guess is None: + self.cbox.log.info("invalid language choosen: %s" % guess) + guess = self.prefs["WebSettings"]["Language"] + ## maybe the language is still not valid + if not guess in availLangs: + self.log.warn("the configured language is invalid: %s" % guess) + guess = "en" + ## maybe there is no english dataset??? + if not guess in availLangs: + self.log.warn("couldn't find the english dataset") + guess = availLangs[0] + self.dataset["Settings.Language"] = guess + ## we only have to save it, if it was specified correctly and explicitly + if value == guess: + self.dataset["Settings.LinkAttrs.weblang"] = guess + + + def __getPreferredBrowserLanguage(self, availLangs): + """guess the preferred language of the user (as sent by the browser) + take the first language, that is part of 'availLangs' + """ + try: + pref_lang_header = cherrypy.request.headers["Accept-Language"] + except KeyError: + ## no language header was specified + return None + ## this could be a typical 'Accept-Language' header: + ## de-de,de;q=0.8,en-us;q=0.5,en;q=0.3 + regex = re.compile(u"\w+(-\w+)?(;q=[\d\.]+)?$") + pref_langs = [e.split(";",1)[0] + for e in pref_lang_header.split(",") + if regex.match(e)] + ## is one of these preferred languages available? + for lang in pref_langs: + if lang in availLangs: return lang + ## we try to be nice: also look for "de" if "de-de" was specified ... + for lang in pref_langs: + ## use only the first part of the language + short_lang = lang.split("-",1)[0] + if short_lang in availLangs: return short_lang + ## we give up + return None + + + def __setDevice(self, device): + if device and re.match(u'[\w /\-]+$', device) and self.cbox.getContainer(device): + self.log.debug("select device: %s" % device) + return True + else: + self.log.warn("invalid device: %s" % device) + self.dataset["Data.Warning"] = "InvalidDevice" + return False + + + def __checkVolumeName(self, name): + if name and re.match(u'[\w \-]+$', name): + return True + else: + return False + + + def __getLanguageValue(self, value): + hdf = self.__getLanguageData(self.dataset["Settings.Language"]) + return hdf.getValue(value, "") + + + def __getLanguageData(self, web_lang="en"): + default_lang = "en" + conf_lang = self.prefs["WebSettings"]["Language"] + hdf = neo_util.HDF() + langDir = os.path.abspath(self.prefs["Locations"]["LangDir"]) + langFiles = [] + ## first: read default language (en) + if (default_lang != conf_lang) and (default_lang != web_lang): + langFiles.append(os.path.join(langDir, default_lang + ".hdf")) + ## second: read language as defined in the config file + if (conf_lang != web_lang): + langFiles.append(os.path.join(langDir, conf_lang + ".hdf")) + ## third: read language as configured via web interface + langFiles.append(os.path.join(langDir, web_lang + ".hdf")) + for langFile in langFiles: + if os.access(langFile, os.R_OK): + hdf.readFile(langFile) + else: + log.warn("Couldn't read language file: %s" % langFile) + return hdf + + + def __render(self, renderInfo, plugin=None): + '''renders from clearsilver templates and returns the resulting html + ''' + ## is renderInfo a string (filename of the template) or a dictionary? + if type(renderInfo) == types.DictType: + template = renderInfo["template"] + if renderInfo.has_key("generator"): + generator = renderInfo["generator"] + else: + generator = False + else: + (template, generator) = (renderInfo, None) + + ## load the language data + hdf = neo_util.HDF() + hdf.copy("Lang", self.__getLanguageData(self.dataset["Settings.Language"])) + + ## first: assume, that the template file is in the global template directory + self.dataset["Settings.TemplateFile"] = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], template + ".cs")) + + if plugin: + ## check, if the plugin provides the template file -> overriding + plugin_cs_file = plugin.getTemplateFileName(template) + if plugin_cs_file: + self.dataset["Settings.TemplateFile"] = plugin_cs_file + + ## add the current state of the plugins to the hdf dataset + self.dataset["Data.Status.Plugins.%s" % plugin.getName()] = plugin.getStatus() + ## load the language data + pl_lang = plugin.getLanguageData(self.dataset["Settings.Language"]) + if pl_lang: + hdf.copy("Lang.Plugins.%s" % plugin.getName(), pl_lang) + ## load the dataset of the plugin + plugin.loadDataSet(hdf) + + self.log.info("rendering site: " + template) + + cs_path = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], "main.cs")) + if not os.access(cs_path, os.R_OK): + log.error("Couldn't read clearsilver file: %s" % cs_path) + yield "Couldn't read clearsilver file: %s" % cs_path + return + + self.log.debug(self.dataset) + for key in self.dataset.keys(): + hdf.setValue(key,str(self.dataset[key])) + cs = neo_cs.CS(hdf) + cs.parseFile(cs_path) + + ## is there a generator containing additional information? + if generator is None: + ## all content in one flush + yield cs.render() + else: + content_generate = generator() + dummy_line = """""" + ## now we do it linewise - checking for the content marker + for line in cs.render().splitlines(): + if line.find(dummy_line) != -1: + yield line.replace(dummy_line, content_generate.next()) + else: + yield line + "\n" + + diff --git a/bin/WebInterfaceTestClass.py b/bin/WebInterfaceTestClass.py new file mode 100644 index 0000000..c210a0f --- /dev/null +++ b/bin/WebInterfaceTestClass.py @@ -0,0 +1,77 @@ +""" +super class of all web interface unittests for the cryptobox + +just inherit this class and add some test functions +""" + +import unittest +import twill +import cherrypy +import WebInterfaceSites + +## we do the following, for easy surfing +## e.g. use: cbx.go(your_url) +## commands api: http://twill.idyll.org/commands.html +CBXHOST="localhost" +CBXPORT=8081 +CBX_URL="http://%s:%d/" % (CBXHOST, CBXPORT) +LOG_FILE="/tmp/twill.log" + +class WebInterfaceTestClass(unittest.TestCase): + '''this class checks the webserver, using "twill" + + the tests in this class are from the browsers point of view, so not + really unittests. + fetch twill from: http://twill.idyll.org + one way to manually run twill code is through the python + interpreter commandline e.g.: + + import twill + twill.shell.main() + go http://localhost:8080 + find "my very special html content" + help + ''' + + def setUp(self): + '''configures the cherrypy server that it works nice with twill + ''' + cherrypy.config.update({ + 'server.logToScreen' : False, + 'autoreload.on': False, + 'server.threadPool': 1, + 'server.environment': 'production', + }) + cherrypy.root = WebInterfaceSites.WebInterfaceSites() + cherrypy.server.start(initOnly=True, serverClass=None) + + from cherrypy._cpwsgi import wsgiApp + twill.add_wsgi_intercept(CBXHOST, CBXPORT, lambda: wsgiApp) + + # grab the output of twill commands + self.output = open(LOG_FILE,"a") + twill.set_output(self.output) + self.cmd = twill.commands + self.URL = CBX_URL + self.cbox = cherrypy.root.cbox + self.globals, self.locals = twill.namespaces.get_twill_glocals() + + + def tearDown(self): + '''clean up the room when leaving''' + # remove intercept. + twill.remove_wsgi_intercept(CBXHOST, CBXPORT) + # shut down the cherrypy server. + cherrypy.server.stop() + self.output.close() + + + def __get_soup(): + browser = twill.commands.get_browser() + soup = BeautifulSoup(browser.get_html()) + return soup + + + def register_auth(self, url, user="admin", password="admin"): + self.cmd.add_auth("CryptoBox", url, user, password) + diff --git a/bin/coding_guidelines.txt b/bin/coding_guidelines.txt new file mode 100644 index 0000000..a6fb47c --- /dev/null +++ b/bin/coding_guidelines.txt @@ -0,0 +1,18 @@ +Maybe we can add some notes here to get a consistent coding experience :) + +------------------------------------------------------------------------------- + +comments: + - should be usable for pydoc + - ''' or """ at the beginning of every class/method + - ## for longterm comments, that are useful for understanding + - #blabla for codelines, that are out for experimenting and might be used later again + +error handling: + - unspecific error handling is evil (try: "grep -r except: .") + +unit testing: + - first write a unittest and then write the relating code until the unittest stops failing :) + - 'unittests.ClassName.py' should contain all tests for 'ClassName.py' + - commits with broken unit tests are evil (fix or disable the code (not the test ;) )) + diff --git a/bin/cryptobox.conf b/bin/cryptobox.conf new file mode 100644 index 0000000..02ef334 --- /dev/null +++ b/bin/cryptobox.conf @@ -0,0 +1,83 @@ +[Main] + +# comma separated list of possible prefixes for accesible devices +# beware: .e.g "/dev/hd" grants access to _all_ harddisks +AllowedDevices = /dev/loop, /dev/ubdb + +# use sepepate config partition? (1=yes / 0=no) +UseConfigPartition = 1 + +# the default name prefix of not unnamed containers +DefaultVolumePrefix = "Disk " + +# which cipher should cryptsetup-luks use? +#TODO: uml does not support this module - DefaultCipher = aes-cbc-essiv:sha256 +DefaultCipher = aes-plain + +# label of the configuration partition (you should never change this) +ConfigVolumeLabel = cbox_config + + +[Locations] +# where should we mount volumes? +# this directory must be writeable by the cryptobox user (see above) +MountParentDir = /var/cache/cryptobox/mnt + +# settings directory: contains name database and plugin configuration +SettingsDir = /var/cache/cryptobox/settings + +# where are the clearsilver templates? +#TemplateDir = /usr/share/cryptobox/templates +TemplateDir = ../templates + +# path to language files +#LangDir = /usr/share/cryptobox/lang +LangDir = ../lang + +# path to documentation files +#DocDir = /usr/share/doc/cryptobox/html +DocDir = ../doc/html + +# path to the plugin directory +#PluginDir = /usr/share/cryptobox/plugins +PluginDir = ../plugins + + + +[Log] +# possible values are "debug", "info", "warn" and "error" or numbers from +# 0 (debug) to 7 (error) +Level = debug + +# where to write the log messages to? +# possible values are: file +# syslog support will be added later +Destination = file + +# depending on the choosen destination (see above) you may select +# details. Possible values for the different destinations are: +# file: $FILENAME +# syslog: $LOG_FACILITY +#Details = /var/log/cryptobox.log +Details = ./cryptobox.log + + +[WebSettings] +# URL of default stylesheet +Stylesheet = /cryptobox-misc/cryptobox.css + +# default language +Language = de + + +[Programs] +cryptsetup = /sbin/cryptsetup +mkfs-data = /sbin/mkfs.ext3 +blkid = /sbin/blkid +blockdev = /sbin/blockdev +mount = /bin/mount +umount = /bin/umount +super = /usr/bin/super +# this is the "program" name as defined in /etc/super.tab +CryptoBoxRootActions = CryptoBoxRootActions + diff --git a/bin/cryptoboxd b/bin/cryptoboxd new file mode 100755 index 0000000..85258bd --- /dev/null +++ b/bin/cryptoboxd @@ -0,0 +1,39 @@ +#!/bin/sh + +#TODO: CBXPATH=/usr/lib/cryptobox +CBXPATH=$(pwd) +CBXSERVER=CryptoBoxWebserver.py +PIDFILE=/var/run/cryptobox.pid +DAEMON=/usr/bin/python2.4 +DAEMON_OPTS=${CBXPATH}/CryptoBoxWebserver.py +NAME=cryptoboxd +DESC="CryptoBox Daemon (webinterface)" +#TODO: RUNAS=cryptobox +RUNAS=$USERNAME + +#test -x $DAEMON -a -f /etc/exports || exit 0 + +set -e + +case "$1" in + start) + echo -n "Starting $DESC: " + start-stop-daemon --background --chdir "$CBXPATH" --chuid "$RUNAS" --start --quiet --oknodo --user "$RUNAS" --make-pidfile --pidfile "$PIDFILE" --exec "$DAEMON" \ + -- $DAEMON_OPTS + echo "$NAME." + ;; + + stop) + echo -n "Stopping $DESC: " + #FIXME: this is the same as "killall python2.4" + # using a pid file instead prevents problems, but does not kill children??? + start-stop-daemon --stop --oknodo --exec "$DAEMON" + echo "$NAME." + ;; + *) + echo "Usage: $(basename $0) {start|stop}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/bin/cryptoboxwebserver.conf b/bin/cryptoboxwebserver.conf new file mode 100644 index 0000000..b8e1e6a --- /dev/null +++ b/bin/cryptoboxwebserver.conf @@ -0,0 +1,17 @@ +[global] +server.socketPort = 8080 +#server.environment = "production" +server.environment = "development" +server.logToScreen = True +server.log_tracebacks = True +server.threadPool = 1 +server.reverseDNS = False +server.logFile = "cryptoboxwebserver.log" + +[/favicon.ico] +static_filter.on = True +# TODO: use live-cd/live-cd-tree.d/var/www/favicon.ico +static_filter.file = "/usr/share/doc/python-cherrypy/cherrypy/favicon.ico" + +[/test_stream] +stream_response = True diff --git a/bin/do_unittests.sh b/bin/do_unittests.sh new file mode 100755 index 0000000..6af0d02 --- /dev/null +++ b/bin/do_unittests.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# +# run this script _before_ you do a commit and fix errors before uploading +# + +# check if /dev/loop1 is available - otherwise some tests will fail! +if /sbin/losetup /dev/loop1 &>/dev/null + then true + else echo "misconfiguration detected: sorry - you need /dev/loop1 for the tests" >&2 + echo "just do the following:" >&2 + echo " dd if=/dev/zero of=test.img bs=1M count=1 seek=100" >&2 + echo " sudo /sbin/losetup /dev/loop1 test.img" >&2 + echo "then you can run the tests again ..." >&2 + echo >&2 + exit 1 + fi + +# do the tests +for a in unittests.*.py + do testoob -v "$a" + done + diff --git a/bin/example-super.tab b/bin/example-super.tab new file mode 100644 index 0000000..03d21c0 --- /dev/null +++ b/bin/example-super.tab @@ -0,0 +1,2 @@ +# adapt the following line to your local setup and add it to /etc/super.tab +CryptoBoxRootActions /your/local/path/to/CryptoBoxRootActions.py yourUserName diff --git a/bin/test.complete.CryptoBox.py b/bin/test.complete.CryptoBox.py new file mode 100755 index 0000000..db5300d --- /dev/null +++ b/bin/test.complete.CryptoBox.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python2.4 + +""" +BEWARE: this script may overwrite the data of one of your loop devices. You +should restrict the AllowedDevices directive in cryptobox.conf to exclude +your precious black devices from being used by this script. + +the following script runs a number of tests for different parts +""" + +from CryptoBox import CryptoBoxProps +from CryptoBoxContainer import CryptoBoxContainer +import sys + + +def main(): + cb = CryptoBoxProps() + + print "Confguration:" + print "\tConfig file:\t\t%s" % (cb.prefs.prefs.filename, ) + print "\tAllowed devices:\t%s" % (cb.prefs["Main"]["AllowedDevices"], ) + + """for e in cb.getContainerList(filterType=CryptoBoxContainer.Types["luks"]):""" + for e in cb.getContainerList(): + print "\t\t%d\t\t%s - %s - %d" % (cb.getContainerList().index(e), e.getDevice(), e.getName(), e.getType()) + + if not cb.getContainerList() or len(cb.getContainerList()) < 1: + print "no loop devices found for testing" + sys.exit(1) + + if len(cb.getContainerList()) > 1: + print "I found more than one available loop device - I will stop now to avoid risking data loss." + print "Please change the 'AllowedDevices' setting in 'cryptobox.conf' to reduce the number of allowed devices to only one." + sys.exit(1) + + testElement = cb.getContainerList()[0] + print "\nRunning some tests now ..." + if not plain_tests(testElement): + print "some previous tests failed - we should stop now" + sys.exit(1) + luks_tests(testElement) + + +" ***************** some functions ******************** " + +def luks_tests(e): + # umount if necessary + try: + e.umount() + except "MountError": + pass + + e.create(e.Types["luks"], "alt") + print "\tluks create:\tok" + + e.changePassword("alt","neu") + print "\tluks changepw:\tok" + + e.setName("lalla") + print "\tluks setName:\tok" + + try: + e.mount("neu") + except "MountError": + pass + if e.isMounted(): print "\tluks mount:\tok" + else: print "\tluks mount:\tfailed" + + print "\tCapacity (size, free, used) [MB]:\t%s" % (e.getCapacity(), ) + + try: + e.umount() + except "MountError": + pass + if e.isMounted(): print "\tluks umount:\tfailed" + else: print "\tluks umount:\tok" + + if e.isMounted(): return False + else: return True + + +def plain_tests(e): + # umount if necessary + try: + e.umount() + except "MountError": + pass + + e.create(e.Types["plain"]) + print "\tplain create:\tok" + + e.setName("plain-lili") + print "\tplain setName:\tok" + + try: + e.mount() + except "MountError": + pass + if e.isMounted(): print "\tplain mount:\tok" + else: print "\tplain mount:\tfailed" + + print "\tCapacity (size, free, used) [MB]:\t%s" % (e.getCapacity(), ) + + try: + e.umount() + except "MountError": + pass + if e.isMounted(): print "\tplain umount:\tfailed" + else: print "\tplain umount:\tok" + + if e.isMounted(): return False + else: return True + +# ************ main **************** + +main() diff --git a/bin/uml-setup.sh b/bin/uml-setup.sh new file mode 100755 index 0000000..8826f3d --- /dev/null +++ b/bin/uml-setup.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +ROOT_IMG=/home/lars/devel-stuff/devel-chroots/cryptobox.img +TEST_IMG=test.img +TEST_SIZE=256 +MEM_SIZE=128M + +# Preparations: +# echo "tun" >>/etc/modules +# follow the instructions in /usr/share/doc/uml-utilities/README.Debian +# add your user to the group 'uml-net' +# + +/sbin/ifconfig tap0 &>/dev/null || { echo "tap0 is not configured - read /usr/share/doc/uml-utilities/README.Debian for hints"; exit 1; } + + +if [ ! -e "$TEST_IMG" ] + then echo "Creating testing image file ..." + dd if=/dev/zero of="$TEST_IMG" bs=1M count=$TEST_SIZE + fi + +linux ubd0="$ROOT_IMG" ubd1="$TEST_IMG" con=xterm hostfs=../ fakehd eth0=daemon mem=$MEM_SIZE + diff --git a/bin/unittests.CryptoBox.py b/bin/unittests.CryptoBox.py new file mode 100755 index 0000000..baaad3c --- /dev/null +++ b/bin/unittests.CryptoBox.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python2.4 + +import unittest +import sys +from CryptoBox import * +from CryptoBoxExceptions import * +import CryptoBoxSettings + +class CryptoBoxPropsDeviceTests(unittest.TestCase): + import CryptoBox + cb = CryptoBox.CryptoBoxProps() + + def testAllowedDevices(self): + '''isDeviceAllowed should accept permitted devices''' + self.assertTrue(self.cb.isDeviceAllowed("/dev/loop")) + self.assertTrue(self.cb.isDeviceAllowed("/dev/loop1")) + self.assertTrue(self.cb.isDeviceAllowed("/dev/loop/urgd")) + self.assertTrue(self.cb.isDeviceAllowed("/dev/usb/../loop1")) + + def testDeniedDevices(self): + '''isDeviceAllowed should fail with not explicitly allowed devices''' + self.assertFalse(self.cb.isDeviceAllowed("/dev/hda")) + self.assertFalse(self.cb.isDeviceAllowed("/dev/loopa/../hda")) + self.assertFalse(self.cb.isDeviceAllowed("/")) + + +class CryptoBoxPropsConfigTests(unittest.TestCase): + '''test here if everything with the config turns right''' + import os + import CryptoBox + + files = { + "configFileOK" : "cbox-test_ok.conf", + "configFileBroken" : "cbox-test_broken.conf", + "nameDBFile" : "cryptobox_names.db", + "pluginConf" : "cryptobox_plugins.conf", + "userDB" : "cryptobox_users.db", + "logFile" : "cryptobox.log", + "tmpdir" : "cryptobox-mnt" } + tmpdirname = "" + filenames = {} + configContentOK = """ +[Main] +AllowedDevices = /dev/loop +DefaultVolumePrefix = "Data " +DefaultCipher = aes-cbc-essiv:sha256 +[Locations] +SettingsDir = %s +MountParentDir = %s +TemplateDir = ../templates +LangDir = ../lang +DocDir = ../doc/html +PluginDir = ../plugins +[Log] +Level = debug +Destination = file +Details = %s/cryptobox.log +[WebSettings] +Stylesheet = /cryptobox-misc/cryptobox.css +[Programs] +blkid = /sbin/blkid +cryptsetup = /sbin/cryptsetup +super = /usr/bin/super +CryptoBoxRootActions = CryptoBoxRootActions +""" + + + def setUp(self): + '''generate all files in tmp and remember the names''' + import tempfile + os = self.os + self.tmpdirname = tempfile.mkdtemp(prefix="cbox-") + for file in self.files.keys(): + self.filenames[file] = os.path.join(self.tmpdirname, self.files[file]) + self.writeConfig() + + + def tearDown(self): + '''remove the created tmpfiles''' + os = self.os + # remove temp files + for file in self.filenames.values(): + compl_name = os.path.join(self.tmpdirname, file) + if os.path.exists(compl_name): + os.remove(compl_name) + # remove temp dir + os.rmdir(self.tmpdirname) + + + def testConfigInit(self): + '''Check various branches of config file loading''' + import os + self.assertRaises(CBConfigUnavailableError, self.CryptoBox.CryptoBoxProps,"/invalid/path/to/config/file") + self.assertRaises(CBConfigUnavailableError, self.CryptoBox.CryptoBoxProps,"/etc/shadow") + """ check one of the following things: + 1) are we successfully using an existing config file? + 2) do we break, if no config file is there? + depending on the existence of a config file, only one of these conditions + can be checked - hints for more comprehensive tests are appreciated :) """ + for a in CryptoBoxSettings.CryptoBoxSettings.CONF_LOCATIONS: + if os.path.exists(a): + self.CryptoBox.CryptoBoxProps() + break # this skips the 'else' clause + else: self.assertRaises(CBConfigUnavailableError, self.CryptoBox.CryptoBoxProps) + self.assertRaises(CBConfigUnavailableError, self.CryptoBox.CryptoBoxProps,[]) + + + def testBrokenConfigs(self): + """Check various broken configurations""" + self.writeConfig("SettingsDir", "SettingsDir=/foo/bar", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + self.writeConfig("Level", "Level = ho", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + self.writeConfig("Details", "#out", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + self.writeConfig("super", "super=/bin/invalid/no", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + self.writeConfig("CryptoBoxRootActions", "#not here", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + self.writeConfig("CryptoBoxRootActions", "CryptoBoxRootActions = /bin/false", filename=self.filenames["configFileBroken"]) + self.assertRaises(CBEnvironmentError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) + + + def writeConfig(self, replace=None, newline=None, filename=None): + """write a config file and (optional) replace a line in it""" + import re + if not filename: filename = self.filenames["configFileOK"] + content = self.configContentOK % (self.tmpdirname, self.tmpdirname, self.tmpdirname) + if replace: + pattern = re.compile('^' + replace + '\\s*=.*$', flags=re.M) + content = re.sub(pattern, newline, content) + cf = open(filename, "w") + cf.write(content) + cf.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/bin/unittests.CryptoBoxTools.py b/bin/unittests.CryptoBoxTools.py new file mode 100755 index 0000000..10daf4e --- /dev/null +++ b/bin/unittests.CryptoBoxTools.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python2.4 + +import unittest +import CryptoBoxTools +import os + + +class CryptoBoxToolsTests(unittest.TestCase): + + def testGetAbsoluteDeviceName(self): + func = CryptoBoxTools.getAbsoluteDeviceName + self.assertTrue(func("hda") == "/dev/hda") + self.assertTrue(func("loop0") == "/dev/loop0") + self.assertTrue(func(os.path.devnull) == os.path.devnull) + + + def testFindMajorMinorOfDevice(self): + func = CryptoBoxTools.findMajorMinorOfDevice + self.assertTrue(func("/dev/hda") == (3,0)) + self.assertTrue(func("/dev/hda1") == (3,1)) + self.assertTrue(func(os.path.devnull) == (1,3)) + self.assertTrue(func("/dev/nothere") is None) + + + def testFindMajorMinorDeviceName(self): + func = CryptoBoxTools.findMajorMinorDeviceName + dir = os.path.join(os.path.sep, "dev") + self.assertTrue(os.path.join(dir,"hda") in func(dir,3,0)) + self.assertTrue(os.path.devnull in func(dir,1,3)) + self.assertFalse(os.path.devnull in func(dir,2,3)) + + + def testIsPartOfBlockDevice(self): + func = CryptoBoxTools.isPartOfBlockDevice + self.assertTrue(func("/dev/hda", "/dev/hda1")) + self.assertFalse(func("/dev/hda", "/dev/hda")) + self.assertFalse(func("/dev/hda1", "/dev/hda")) + self.assertFalse(func("/dev/hda1", "/dev/hda1")) + self.assertFalse(func("/dev/hda", "/dev/hdb1")) + self.assertFalse(func(None, "/dev/hdb1")) + self.assertFalse(func("/dev/hda", None)) + self.assertFalse(func(None, "")) + self.assertFalse(func("loop0", "loop1")) + + +if __name__ == "__main__": + unittest.main() + diff --git a/bin/unittests.Plugins.py b/bin/unittests.Plugins.py new file mode 100755 index 0000000..929c9de --- /dev/null +++ b/bin/unittests.Plugins.py @@ -0,0 +1,33 @@ +#!/usr/bin/python2.4 + +import unittest +import Plugins + +class CheckForUndefinedTestCases(unittest.TestCase): + """here we will add failing test functions for every non-existing testcase""" + + +def create_testcases(): + + plugins = Plugins.PluginManager(None, "../plugins").getPlugins() + glob_dict = globals() + loc_dict = locals() + for pl in plugins: + test_class = pl.getTestClass() + if test_class: + ## add the testclass to the global dictionary + glob_dict["unittest" + pl.getName()] = test_class + else: + subname = "test_existence_%s" % pl.getName() + def test_existence(self): + """check if the plugin (%s) contains tests""" % pl.getName() + self.fail("no tests defined for plugin: %s" % pl.getName()) + ## add this function to the class above + setattr(CheckForUndefinedTestCases, subname, test_existence) + #FIXME: the failure output always contains the same name for all plugins + + +create_testcases() + +if __name__ == "__main__": + unittest.main() diff --git a/bin/unittests.WebSites.py b/bin/unittests.WebSites.py new file mode 100755 index 0000000..89e514d --- /dev/null +++ b/bin/unittests.WebSites.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python2.4 + +import unittest + +## this makes assertRaises shorter +from twill.errors import * +from mechanize import BrowserStateError, LinkNotFoundError + +## import the module of the common super class of all web interface test classes +import WebInterfaceTestClass + + + +class WebServer(WebInterfaceTestClass.WebInterfaceTestClass): + + def test_is_server_running(self): + '''the server should run under given name and port''' + self.cmd.go(self.URL) + ## other URLs must not be checked, as we do not know, if they are valid + + +class BuiltinPages(WebInterfaceTestClass.WebInterfaceTestClass): + + + def test_goto_index(self): + '''display all devices''' + self.cmd.go(self.URL + "?weblang=en") + self.cmd.find("The CryptoBox") + self.cmd.go(self.URL + "?weblang=de") + self.cmd.find("Die CryptoBox") + self.cmd.go(self.URL + "?weblang=si") + self.cmd.find("Privatnost v vsako vas") + self.cmd.go(self.URL + "?weblang=fr") + self.cmd.find("La CryptoBox") + + +if __name__ == "__main__": + unittest.main() + diff --git a/debian/README.Debian b/debian/README.Debian index 8a503fc..f06e815 100644 --- a/debian/README.Debian +++ b/debian/README.Debian @@ -1,6 +1,5 @@ CryptoBox for Debian - installation notes -be aware of two things: +be aware of one thing: 1) you need cryptsetup with luks support (for now only in unstable) -2) the debian perl-clearsilver package is broken (at least until April 02006) diff --git a/debian/control b/debian/control index b2dba57..26058dc 100644 --- a/debian/control +++ b/debian/control @@ -7,8 +7,7 @@ Standards-Version: 3.6.2 Package: cryptobox Architecture: any -Depends: bash (>=2.0), sed (>=4.0), coreutils, grep (>=2.0), perl, httpd-cgi, hashalot, libconfigfile-perl, cryptsetup (>=20050111), dmsetup, pmount, initscripts, e2fsprogs (>= 1.27), adduser -Recommends: perl-clearsilver +Depends: bash (>=2.0), sed (>=4.0), coreutils, grep (>=2.0), httpd-cgi, hashalot, cryptsetup (>=20050111), dmsetup, initscripts, e2fsprogs (>= 1.27), adduser, python (>=2.4), python-clearsilver Suggests: cron, samba Description: Web interface for an encrypting fileserver This bundle of scripts and cgis allow you to manage an encrypted harddisk diff --git a/debian/rules b/debian/rules index 33284e8..5d6d90b 100755 --- a/debian/rules +++ b/debian/rules @@ -74,19 +74,14 @@ binary-arch: build install # dh_installmenu # dh_installdebconf # dh_installlogrotate -# dh_installemacsen -# dh_installpam -# dh_installmime dh_installinit # dh_installcron -# dh_installinfo dh_installman dh_link dh_strip dh_compress dh_fixperms - dh_perl -# dh_python + dh_python # dh_makeshlibs dh_installdeb dh_shlibdeps diff --git a/design/background_frame_corner.svg b/design/background_frame_corner.svg new file mode 100644 index 0000000..deb4ae3 --- /dev/null +++ b/design/background_frame_corner.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/design/icon_background_active.svg b/design/icon_background_active.svg new file mode 100644 index 0000000..c6ee31a --- /dev/null +++ b/design/icon_background_active.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/design/icons/applications-system_tango.svg b/design/icons/applications-system_tango.svg new file mode 100644 index 0000000..35e2ffa --- /dev/null +++ b/design/icons/applications-system_tango.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Applications + + + Jakub Steiner + + + http://jimmac.musichall.cz/ + + + system + applications + group + category + admin + root + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/computer_tango.svg b/design/icons/computer_tango.svg new file mode 100644 index 0000000..d6e0f6b --- /dev/null +++ b/design/icons/computer_tango.svg @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Computer + 2005-03-08 + + + Jakub Steiner + + + + + workstation + computer + node + client + + + + http://jimmac.musichall.cz/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/dialog-error_tango.svg b/design/icons/dialog-error_tango.svg new file mode 100644 index 0000000..602fa79 --- /dev/null +++ b/design/icons/dialog-error_tango.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Rodney Dawes + + + + + Jakub Steiner, Garrett LeSage + + + + Dialog Error + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/dialog-information_tango.svg b/design/icons/dialog-information_tango.svg new file mode 100644 index 0000000..1e957cc --- /dev/null +++ b/design/icons/dialog-information_tango.svg @@ -0,0 +1,1145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Info + + + Jakub Steiner + + + + + dialog + info + + + http://jimmac.musichall.cz + + + + Garrett LeSage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/dialog-warning_tango.svg b/design/icons/dialog-warning_tango.svg new file mode 100644 index 0000000..3870db2 --- /dev/null +++ b/design/icons/dialog-warning_tango.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Dialog Warning + 2005-10-14 + + + Andreas Nilsson + + + + + Jakub Steiner, Garrett LeSage + + + + + dialog + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/drive-cdrom_tango.svg b/design/icons/drive-cdrom_tango.svg new file mode 100644 index 0000000..6588a65 --- /dev/null +++ b/design/icons/drive-cdrom_tango.svg @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Drive - CD-ROM + + + Jakub Steiner + + + + + cdrom + cd-rom + optical + drive + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/drive-harddisk_tango.svg b/design/icons/drive-harddisk_tango.svg new file mode 100644 index 0000000..406c4ac --- /dev/null +++ b/design/icons/drive-harddisk_tango.svg @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Drive - Hard Disk + + + Jakub Steiner + + + + + hdd + hard drive + fixed + media + solid + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/drive-removable-media_tango.svg b/design/icons/drive-removable-media_tango.svg new file mode 100644 index 0000000..e448605 --- /dev/null +++ b/design/icons/drive-removable-media_tango.svg @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Drive - Removable + + + Jakub Steiner + + + + + media + removable + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/globe-lips.svg b/design/icons/globe-lips.svg new file mode 100644 index 0000000..8b700f9 --- /dev/null +++ b/design/icons/globe-lips.svg @@ -0,0 +1,512 @@ + + + + + + + mouth - body part + + + + bodypart + mouth + + + + + Open Clip Art Library + + + + + Nicu Buculei + + + + + Nicu Buculei + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/gnome-dev-removable-usb_nuvola.svg b/design/icons/gnome-dev-removable-usb_nuvola.svg new file mode 100644 index 0000000..a2c624a --- /dev/null +++ b/design/icons/gnome-dev-removable-usb_nuvola.svg @@ -0,0 +1,1004 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Adobe PDF library 6.66 + + + + + + + + + + 2004-10-01T14:58:15+02:00 + + 2005-02-15T09:49:25Z + + Illustrator + + 2004-10-01T14:58:15+02:00 + + + + + JPEG + + 256 + + 256 + + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq +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 +rF+j9Kff61cAxoy+KJ9uT/iPuM1mo1/Qn4BmIvavJn5QeUfLJS4WH9Iamm/125AYq3jHH9lPnu3v +mqyaiUvIMwGcZQl2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvnX82fJH+Hda+u +2kdNI1Fi0QA2il6vF7D9pfbbtmywz8SNHmGB2Zn+UfnE32m/oS8k5XdktbV26yQDYD5x9PlmHkhR +ZBl19rSopFsnrsNjJWkYPf4v2iPBfppkEvMPO3mlUJS9ufVmG6WybAf7Cvw/Nt/nlmPFKfLkgli/ +l/yV5r86XHOyt/q+nBqPeTVWIeIBpWRvZR86Zk8OPFz3kx3KX+d/J2oeUNabTrl/XgkUSWt0F4rK +h6mlWoVOxFf4Zm6XUA7sSEsLX99YQ2d7f3EumQ1EGncykC/EWPJVpzPIk1b5ZsYaWBPEWFrTd2dp +GIoEWg6IgAUfdkp6iEBQWmS+WPy087+a+EqQ/o/S33+t3AKIy+KL9uT/AIj75rNRr+hPwDMRey+U +Pyb8oeXeE8kP6U1FaH61dAFVbxji3RfatSPHNVk1MpeQbAGd5jpdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirsVdirsVdirsVdiqV+ZvL1j5g0W50q8H7udfgkH2o5Bujr7qcnjmYmwpD568uxQ+ +VvPcdp5jpDDYSSC5JUupUxtSigEsrggjbeubDLHjjcWA2ZDq/nrzH5vv20ryfYSpFsDPQepx6cmb +7EK/M/TlcdPGAuZTbJvJ/wCR+m2brfeZZBqd8TyNsCfQU/5RPxSn50HscryaonaOwUReoRRRxRrF +EixxoAqIoAVQNgAB0GYjJiX5peUo/MnlO5iSPnqFmpubBgPi5oKsg/4yL8NPGnhl2DJwy8kEPFvK +/wCT3nTXkje4T9FacaH1roEOVPdIRRj/ALLiPfNll1oAq7YCL2Lyn+UPk/y9xm+r/pG/Xf61dgPx +P+RHTgvsaV9812TUSl5BmAzbKEuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K +uxV2KuxV2KsT84flroPmm/s72+aSKW2BSUw0Bmj6hGJBpQ9/n9F2LPKAICCE/wBI0XStHs1s9MtY +7S2X9iMUqfFid2PuTXK5TMjZSjcirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd +irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi +rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir +sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs +VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd +irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi +rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir//Z + + + + + + + + + + + + uuid:74d5a603-9ab8-427f-9735-c474bf2487a1 + + + + + + image/svg+xml + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/gnome-globe_nuvola.svg b/design/icons/gnome-globe_nuvola.svg new file mode 100644 index 0000000..3a5a620 --- /dev/null +++ b/design/icons/gnome-globe_nuvola.svg @@ -0,0 +1,1195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2004-03-28T20:03:13Z + +2004-03-28T20:03:13Z + +Illustrator + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +XH/ZRirv+hmfPf8AywaX/wAirj/soxV3/QzPnv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/so +xV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hm +fPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5FXH/ZRirv+hmfPf8AywaX/wAirj/soxV3/QzPnv8A +5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8sGl/ +8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxVFad/zk75oS4B1HSbG4t+ +6W/qwP8A8E7zj/hcVeyeRfzJ8tec7Vn0yUx3kQBuLCaizJ25AAkMlf2l+mmKsqxV2KuxV2KuxV2K +vm/XDqf5ufmk+j287Q+XtJLqJF3VIY2CSzAHYvM9AvtTwOKvePLfk/y35bs0tdHsYrZVFGlCgyuf +GSQ/Ex+ZxVOK4q6oxVrkMVdyGKu5jFWvUGKu9RffFWvVX3xV3rL74q71l8DirXrp4HFXfWE8DirX +1hPA4q76yngcVd9Zj8D+GKtfWo/A/hirvrcfgfw/rirvrcfgfw/rirX1yLwb8P64q765F4N+H9cV +d9di8G/D+uKtfXovBvw/riqVa/5X8r+abR7TV7GO55CiyMoWZP8AKjkHxKR7HFXzB5n0XXfys8/R +NZXBJgIudOujsJYGJUpIB8ijj+oxV9VeWtfs/MGhWWsWf9xexLKErUoxHxI3up2OKplirsVdirsV +Q+oMy2Fyy/aWJyvzCnFXhP8AziwqvL5nmYcpQLIBz1oxuC2/uVGKvficVaxVrFWicVaJxVrFWsVa +JxVonFWsVaxVrFWicVaxVrFWicVaJxVrFWsVaJxVonFWsVaxVdCSJkp/MP14q8V/5ypRBJ5ZkCjm +wvVZu5CmAgfRyOKsn/5x3vJX8lwWzElQZmSvbjMR/wAbYq9XxV2KuxV2KofUv+Oddf8AGGT/AIic +VeE/84pn/lKP+jD/ALGcVe+nFWsVaJxVonFWsVaxVonFWicVaxVrFWsVaJxVrFWsVaJxVonFWsVa +xVonFWicVaxVrFWicVXQ/wB9H/rD9eKvFv8AnKw/8ov/ANH/AP2LYqn/APzjn/yisHyuP+T4xV6/ +irsVdirsVQ+pf8c66/4wyf8AETirwf8A5xRNf8U/9GH/AGM4q9+PXFWicVeZfmbd3UOuwLFM8am1 +QlUYqK+pJ4HN12bEHGbHX9TrdZIiY9zDzqOoV/3ql/5GN/XNj4ce4OJxnvWHUdQ/5apv+Rjf1x8O +PcF4z3rDqWo/8tU3/Ixv64fDj3BHGe9YdS1H/lqm/wCRjf1x8OPcF4z3rDqWo/8ALVN/yMb+uHw4 +9wXjPesOpaj/AMtU3/Ixv64fDj3BeM96w6nqX/LXN/yMb+uPhx7gvGe9YdT1L/lrm/5GN/XD4ce4 +LxnvWHU9S/5a5v8AkY/9cPhx7gjjPeptqmpf8tc3/Ix/64+HHuC8Z71p1TU/+Wub/kY/9cPhR7gv +Ge9YdU1P/lrm/wCRj/1x8KPcF4z3qZ1TU/8Alsn/AORj/wBcPhR7gvGe9YdV1P8A5bJ/+Rj/ANcP +hR7gjjPesOq6p/y2T/8AIx/64fCj3BeM96w6rqn/AC2T/wDIx/64+FHuC8Z71jatqn/LZP8A8jH/ +AK4fCj3BeOXesOrar/y2T/8AI1/64+FHuC8cu9nP5OX17P5nuknuJJUFlIQruzCvqxb0JzWdrQiM +QofxfoLmaGRMzfc9jJznXaLof76P/WH68VeK/wDOVxp/hb/o/wD+xbFWQf8AOOX/ACilv8rj/k+M +VewYq7FXYq7FUPqX/HOuv+MMn/ETirwb/nE81/xT/wBGH/Yzir349cVaJxV5d+aP/Hft/wDmET/k +5Jm87M/uz7/0B1mt+se5INB8v3OtXE0UE0UAgjM0skxKqEBAJqA3j3zLz5xjAJBNuPixGZ2Xaz5V +1DTLOO+9WC7sZW4rdWr+old9jsPDBi1MZnh3Eu4pyYTEXzCSvDMIhKY2ETGiyEHiT7HpmRYumqlo +gmeNpEjZo0+24BIHzPbGwtKZhmJChGqRUChqR44bC0sEEziqRswoTUAkUXqfow2EUpvFKqLIyMI3 +qFcg0NOtDhsLS14J1iWZo2ETGiyEEKT7HpjYulpaba44M/pPwQAu3E0APQk9q48QWirvoeqLpC6u +0BGnvL6KTVG70J+zXlT4etKZEZo8fBfqZeGeHi6JactYLDhVYcULDhVTOFVhxVY2FVhwqzz8lv8A +lKrr/mBk/wCT0Wartj+6H9b9BczQ/Wfc9pzm3bLoP7+P/WH68VeK/wDOWBp/hb/o/wD+xbFWQf8A +OOH/ACidv8rj/k+MVew4q7FXYq7FUPqX/HOuv+MMn/ETirwT/nEw/wDKVf8ARh/2M4q9/Y7nFWsV +eXfmh/x37f8A5hU/5OSZvOzP7s+/9AdZrfrHuUPIBiNxqkDyxxPcWUkURkYICzEACpyeuuomuUmO +l5keSvcvp2ieVG0ae7hvrq8ukmmitm9RY41KE/F4kJ+OQiJZMvGAQAOrIkQhwk2SU/1bUrT6vq08 +9/bTeX57JY9Ps0ZC3q8dgqAVBDfdt4Zi4sZuIAPiCW5b5zFEkjhrZJb+5uJ/JliuiajbW1nBZumq +WhdUleTh+8HEgklvip41zIhEDMeOJJMti0yJOMcJFVunekSadLfaJrX6RtY7eHThavFJKqyerTda +Hw3rmPlEhGcOE2ZW3QIJjKxySnyv5hXTvLmgQRXMMZlv2jvUcpUQuz15VNVHQ1y/U4OPJMkH6dve +14cvDCIvqs8xa3Z32gazBdTwzR2mpRrYwoYwwt1KD92F6jiW3xwYTGcSAd47+9GXIDGQPSWyYecN +Z05/L98tq1tcadPbIlsoukHEr9n07fhUMhO+/wDTKtLikMguxIHfb9LZmmOE1VV3/oV9P8y2r32m +6XPe250ubR1+tB3jp63wqVZyevGvwnIz054ZSAPFx7e5lHKLAvbhYpFqs93+Vb2kOoxR3NpKVuLZ +2USNbUNI1UipqWBB9uuZpxiOpsx2I+1xhMnDV8vuedHNq4Sw4VWHFCw4VUzhVYcVWNhVYcKs8/Jb +/lKrr/mBk/5PRZqu2P7of1v0FzND9Z9z2jObdsugP7+P/WX9eKvFP+csz/yiv/R//wBi2Ksh/wCc +b/8AlErf5XH/ACfxV7FirsVdirsVQ+pf8c66/wCMMn/ETirwL/nEo/8AKVf9GH/Yzir6AbqcVaxV +5f8Amh/x37f/AJhU/wCTkmbzsz+7Pv8A0B1mt+se5hp65sXDWHCqmcKFhxVY2FVhwqsbFVM4VWHC +hY2FVhxVYcKrDihYcKqZwqsOKrGwqsOFWefkv/ylN1/zAyf8nos1XbH90P636C5mh+s+57OTnNu2 +Xwf38f8ArL+vFXiX/OWp/wCUV/6P/wDsWxVkX/ONv/KI23yuf+T+KvY8VdirsVdiqH1L/jnXX/GG +T/iJxV4D/wA4kGv+K/8At3/9jOKvoFvtH54qtJxViXm3yZc63qMd1FcpCqQrEVYEmoZmrt/rZsNJ +rBijRF7uJn05nK7SI/lZf/8ALdF/wLZlfypHuLR+RPetP5V6h/y3Rf8AAtj/ACpHuK/kT3rT+VOo +f8t0X/Atj/Kkf5pX8ie9Yfyo1D/lvh/4FsP8qx/mlfyJ71p/KfUf+W+H/gWx/lWP80r+RPetP5S6 +j/y3w/8AAtj/ACrH+aV/InvWn8pNR/5b4f8AgWw/ytH+aV/InvWn8otR/wCrhD/wDY/ytH+aV/In +vWn8odS/6uEP/ANh/laP80r+RPesP5P6kf8ApYQ/8A2P8rR/mlH5E960/k9qX/Vwh/4B8f5Xj/NK +/kT3rT+Tupf9XGH/AIB8P8rx/mlfyJ71p/JvU/8Aq4w/8A+P8rx/mlfyB71h/JrUz/0sYf8AgHx/ +liP80r+QPetP5Man/wBXGD/gHw/yxH+aV/IHvWn8l9U/6uUH/APj/LEf5pX8ge9afyV1T/q5Qf8A +APh/liP80r+Ql3rD+Smqf9XKD/gHx/lmP80r+Ql3sh8i/l5e+W9Xmvp7uO4SS3aAIisCCzo1d/8A +UzD1vaEc0BECt7b9PpjjlZLOic1bmL4P7+P/AF1/XirxL/nLc0/wp/28P+xbFWR/841/8ofbfK5/ +5P4q9jxV2KuxV2KofUv+Oddf8YZP+InFXz//AM4jGv8Aiv8A7d//AGM4q+gm+0fniq0nFWsVaxVr +FWicVaxVrFWicVaJxVrFWsVaJxVonFWsVaxVonFWicVaxVrFWicVaJxVrFV9v/vRF/rr+vFXiP8A +zlyaf4U/7eH/AGLYqyT/AJxq/wCUOtvlc/8AURir2TFXYq7FXYqh9S/4511/xhk/4icVfPv/ADiG +f+Us/wC3f/2M4q+g3PxH54qtxVrFWsVaJxVrFWsVaJxVonFWsVaxVonFWicVaxVrFWicVaJxVrFW +sVaJxVonFWsVaxVfbn/SIv8AXX9eKvEP+cvD/wAon/28P+xbFWS/840f8oba/K5/6iMVey4q7FXY +q7FUPqX/ABzrr/jDJ/xE4q+fP+cQT/yln/bv/wCxnFX0G/2j8ziq3FWsVaJxVrFWsVaJxVonFWsV +axVonFWicVaxVrFWicVaJxVrFWsVaJxVonFWsVaxVonFV9v/AL0Rf66/rxV4h/zl8f8AlE/+3h/2 +LYqyX/nGf/lDLX5XP/URir2bFXYq7FXYqh9S/wCOddf8YZP+InFXz1/zh+a/4t/7d/8A2NYq+hH+ +23zOKrcVSLXPME2nXSQJEsgaMPViR1JHb5Zm6fSjJGyeri59QYGqSw+drof8eyfecyP5Pj3tP509 +ym3nm7H/AB7R/wDBHH+To96/nT3LG8+XY/49Y/8Agmw/ydHvR+dPcpnz9eD/AI9Y/wDgmw/ybHvK +/nT3LD+YN4P+PSP/AIJsf5Nj3lfzp7lNvzEvR/x6R/8ABNh/kyPeV/OnuWN+Y98P+POL/gmw/wAm +R7yv509ymfzKvh/x5xf8E2P8mR7yv549yw/mbfj/AI8ov+CbD/Jce8o/PHuU2/NC/H/HlF/wTYf5 +Kj/OK/nj3LD+aeoD/jxi/wCCbH+So/ziv549ymfzW1Af8eMX/BNh/kmP84r+ePcsP5saiP8Ajwh/ +4Jsf5Jj/ADiv549ym35t6iP+PCH/AIJsP8kR/nFfzx7ljfm9qX/Vvh/4NsP8kR/nFH549ymfzh1I +f9K+H/g3x/keP84r+fPcsP5yamP+ldD/AMG+H+R4/wA4r+fPcpt+c+qf9W6D/g3w/wAjR/nFfz57 +k/8AI/5hXvmPVpbGe0jgSO3acOjMSSrotN/9fMPW9nxwwEgb3pv0+pOSVEM4zVuYvtv96Iv9df14 +q8P/AOcwDT/CX/bw/wCxXFWTf84y/wDKF2nyuf8AqIxV7PirsVdirsVQ+pf8c66/4wyf8ROKvnj/ +AJw9Nf8AFv8A27v+xrFX0K/22+ZxVaTirDvOP/HSi/4wr/xNs22g+g+91us+se5Ire1uLudbe3Qy +TPXigoK0FT19hmbKYiLPJxoxMjQRMnlrWllSN7Yo0nLhyZd+A5Hv4ZWNTjq7Z+BO+STNmQ1KTYUL +49Ou5rG4vY1Bt7UoJmqAR6h4rt1O+ROQCQj1LIQJBPQIFstYq1jpN/qHrG1jDrboZJnLKqqo8SxG +QyZYwq+rKGMy5Jc2WsFFsKoq20LVbqaeGOBllt4GupUk/dkRLQ8qNSv2hTK5ZoxAN8zTOOORStsu +a1FsKqTYUKLZJVJsKq+paRfafHaSXSBEvoVubchg1Y2JAJp06d8hjyxlddDTKUCKvqlrZawUWwqz +j8m/+Uouv+YGT/k7Fmr7Y/uh/W/QXM0P1n3PY85t2y+2/wB6Yv8AXX9eKvDv+cwjT/CX/bx/7FcV +ZR/zjH/yhVp8rn/qIxV7PirsVdirsVQ+pf8AHOuv+MMn/ETir53/AOcOj/yl3/bu/wCxrFX0LIfj +b5nFVuKsO84/8dKL/jCv/EmzbaD6D73W6z6x7mPNme4jJBqlgvmwXfrIbcQ8fUP2eXo8afftmB4U +vB4a3v8AS5niR8W72r9C621DSLjULG6nlhglFownoqhTKGFFbkrAVFe2CWOYiQAT6vsTGcTIE0Nl +l3qmjQXWrXVp9Wdmit2t0ZQUaQN8fFSBv3wwxTIiDfMrKcQZEV0V7bWPLMF/q/qPEbOea1aJFHws +VClmC+Cv8TZCWHKYx7wCyjkgDLu2S2G50saTrEN1d2xmkeVormMKZZapRFCFDRT24nb2y6UZccSA +a227msGPDKyEm0bWFtPLWt23rIk83ofV42VSWq5WWlQf2Pu7ZkZsXFkga2F/saseSoSCdeYNT8st +oM6WzWrRtFCNOgiSlxHKpPMyGnhT57/TjYMeXxBd9b7m/LOHDtXl3sd87avbXWuH9HiH6nbFWt5I +UADMyqzM23xfEKfRmVo8RjD1XZcfUZAZbcgyWXzNp661NqX1+3kgm0qRbeIqvJJ/3Z9OQAb8iuwJ +8cwxp5cAjRvj+zdyTmHFdj6VGz1nym/mRL2V7aOWfTY/3vFViS75HmN1YK1KbkHJTxZRjoXtL7ER +yQ472+n7WM3Fzok/5hRTNbxNpzzxh4bas0TMVAqoCKWBfcjjvmXGMxp6v1V12aCYnL5Mq1r9B2k+ +h3OrC3ktGvrlpJFt/RBQIRFzTiCwRuFTSmYWLjkJiF3wjrfvcmfCDEy5WeiDutX8mHzFoz38ljPN +Cbn6xc20f+jgMf8ARvU7Gg+fE/fk44s3hy4eIcufPzYmePiF11/YtXWfJ3+JtMe8lsXvYrKZLi+h +j/0P6yWX0WI2Bogeu/cYTiy+HKuKuIbda6rxw4xdXXwYj+ZOqWOoTaS1rdQXTQWSw3D2ylIxKrty +4oQtBvttmboMcoiVgi5dXH1MxKqN7MKbNg4qi2FWcfk5/wApRdf8wUn/ACdizV9sf3Q/rfoLmaH6 +z7nsWc27ZfbH/SYv9df14q8O/wCcxT/yiP8A28f+xXFWUf8AOMX/AChNp8rr/qIxV7RirsVdirsV +Q+pf8c66/wCMMn/ETir51/5w4P8Ayl//AG7v+xrFX0NJ9tvmcVW4qw/zh/x0ov8AjCv/ABJs22g+ +g+91us+se5j4R3YIilnY0VQKkk9gBmddOIA57O5EU0pQqsDBJg1FZWNRQqd+2ImLA72XCfkg2ybF +SbChRbCq+40+6isoL11At7kusTVFSUNG264I5AZGPUMjAgA9CgWyxgpNhVRbCqk2FDUltcrbpcNE +4t3YqkxUhGYdQG6EjESF11TRq0OsssUiyxO0ciHkjqSGBHQgjpkiARRQDTr/AFC/vpBJe3MtzIBQ +PM7SEDwqxOCEIx2ApMpE8ygmyxivsdPutQu0tLVQ88leCllQfCCx+JiB0GRnMRFnkmMTI0EC2WMV +JsKqLYVZv+Tv/KT3X/MFJ/ydizV9sf3Q/rfoLmaH6z7nsROc27ZUtv8AemL/AF1/Xirwz/nMc/8A +KIf9vH/sVxVlP/OMH/KD2fyuv+onFXtOKuxV2KuxVD6l/wAc66/4wyf8ROKvnP8A5w2Nf8X/APbu +/wCxrFX0RJ/eN8ziqwnFWIeb/wDjpRf8YV/4k2bbQfQfe63WfWPclekXa2mq2tw54okg5sRWinZj +9xzKzQ4oENGKXDIFOb6909hqPqXEM0kt1DJAygf3XIGmwH2V2bMWEJemgR6T83IlOO+45hE/pzRR +cA0teIvTHy9Nf95mj5Fun+/O+V+BOv4vp+2/1Nniwvpz+xQ02/8ALcGlXEQ+rtJ6s3qxykIZIyzc +OJ4OT8NKUyWTHlMwd+Q/HNjCcBE8uqS+TbiwhuLo3kkESMgUSykc16msYZXVjtuDmTrIyIHDbTpi +ATdJjpOt6NBbWFhPLBLaPLdrdGVN1jNWjPT4eZplGXDMmUhd+mm3HkiAAeW6hZ6j5dTyaYALdrkw +yrcQSkJIZdyrr8DFmH7PxAZKePJ4171aIzh4dbWlPlHUtHt7a8j1MIfRZLu1DAVeSIGsYND9rbL9 +VjmSDH3FqwTiAeL3ppPrPliPW4EgMD6fHbXUrM6Di01wxcRttvx2AygYcpgbvisfINpyQ4tuVH7V +vlrXPL7ab6141lBcvcySapHPEKyRMrcRCKHoSPhH+2dRhycVDiIr0/tXDkjW9Xe6Ci1zRp/LenW0 +9xEtraX/APpdlIo9R7Uy8k4gAk8RTlQ1O/XJnDMZJEA2Y7HzpiMkTAA8gfsQn5l6jot2LNdPNrKU +Z6T27gv6ZA4o6qiBafs7nLOz8c43xX8f7WOqlE1VIseZtBttJSOJLGW4t9JtHhMkKu/15Kq6kkbk +Llf5eZlvxUZnr/Cz8WIj0+kfNGabrPkmLzLr80clojTG3axkk4xwsgiX1lVikgXk9eXw1OV5MWY4 +4Dfrffz2Zxnj45cmKeXNR0CH8xZby5EFvpRkuOI+3AAVYLwqq/CT9n4RmbnhM6ehZlt73HxSiMtn +kmOka75bh0LTLC6+pMs2n3qXzuitKsvKsKlqclJqaZTlw5DOUhxfVGv0tkMkREA1yLy9s3DgKLYV +Zv8Ak7/yk91/zBSf8nYs1fbH90P636C5mh+s+57DnNu2VLX/AHpi/wBdf14q8L/5zJNP8If9vH/s +VxVlX/OL/wDyg1n8rr/qJxV7VirsVdirsVQ+pf8AHOuv+MMn/ETir5y/5w0Nf8Yf9u7/ALGsVfRE +n943zP68VWE4qxDzf/x0Y/8AjCP+JNm20H0H3ut1n1j3JJb2s11cJbwjlLIeKAkDf5nM2UhEWXGj +Ek0EPcRPFK8TijxsVYdd1NDkomxaCKNIdskhUs9Our6V4rZQzxo0rAkD4V6nfIzyCAspjAy5IBss +YttY3vq+l9Xk9UrzEfBuXCleVKVpTvg441dp4T3INssYqTYVUWwqpNhQpNhVV1LS7zT/AKt9aQJ9 +bgS6goQ1YpK8Tt06dMjjyCV10NMpQMavqhrOyuL68hs7ZQ1xcOI4lJABZjQbnbJTmIgk8giMSTQX +32g6laWTXs8YW3W5ezLBlJ9aMVZaA1+nBDNGRoc6v4JljIF+dKFnoeq3txZwQW78r9mSzdwUSRl+ +1xdqKad98M80Ygkn6eaxxkkeaAvLaW2uZraYcZoHaOQA1oyEqdx7jLYyBFjqwIo0hWySFFsKs2/J +7/lJ7n/mCk/5OxZq+2P7of1v0FzND9Z9z2HObdsqWv8AvVD/AK6/rxV4X/zmWaf4P/7eP/YrirKv ++cXf+UFs/ldf9ROKva8VdirsVdiqH1L/AI511/xhk/4icVfOH/OGJr/jD/t2/wDY1ir6JlP7xvmf +14qsxViPm7/jox/8YR/xJs22g+g+91us+se5LtEnig1e1mmYJEj1dz0ApmTniTAgNGEgTBKdzaro +lxdafPd+ifTuJxLxXb0zy9JmHcV4nMSOLJESAvkP2uUckCQT3l15qejLq7TEWzqtpKvMEOkj1HBX +AVBX/OuMMU+Ct/qCynDivbkkXl7UoI9Vvbq7kWL14J96UXnIQaADMrUYyYADoQ0YZjiJPcUyn1HQ +R5RSGAW7TiBQ8TELKs4pV1HAlvi3+0BTKBjyeNZur+FNpnDw6FftVh5gtG120uTfwC2uLJoi3Bax +SlRX1Ph2HIbf0yHgHwyOE2JfNl4o4wb2pD2OpaBH5anhka1lvj9YF2rkJ6jsW4vGRGSdqcaUpk54 +8hyAi+HavxaIzhwVte6W6gLPVbCxsrSSCGCz05LrUbhYwZPVhThwZhQk1bYeJy3HxQkZG7MqHxa5 +VIACto2WNeXjaDXLI3kwgthIDJKQrAUFRUMGXrtuMzNRfAaFloxVxC2aahqflVtetbmKS0dVs7mO +ckKY2cAemGHFFJbftmthjy8BBv6g5kpw4gduRQtlrXlqbWbS8na0hupNL4GRox6Ed5yr8ajYGn4b +ZZPDkECBZHH8aYxyQMgTV8P2sd8+ahp2o6tphguo5YY7KGC4mgQrGrK78+KEAgAGoXMrRQlCErH8 +R5tOokJSFHoynW9Y8orPor213bSzWeowvJcIEDC34EuW9NI1A5U2GYWHFlqVg0Yn5uRknC40RsWH ++Y9V0648tTW0M6vO2s3NyIx19F1IV/kczsGOQyAkbcADjZJgwr+kU70zzPp76T5RabULeIaZcNHq +Fu6qJQBUROKLXiFX4iDvUVqcx8mnlxZKB9Q2/S3Qyjhhvy5uuvMnlC/1PRL3Ujat9Xv7xbkRxino +Ev8AV3kUD4hy4N+OCODLGMhG94x+fVTlgTEmuZ/Yxv8ANPUNIvdUtW0/6tIUiZZbm1kDiQcvg58Y +4lDAeFfntmX2dCcYniv4/wBpadVKJIqmCNmxcVm35Pf8pPc/8wT/APJ2LNX2x/dD+t+guZofrPue +wZzbtlS1P+lQ/wCuv6xirwr/AJzONP8AB/8A28v+xXFWV/8AOLn/ACgll8rr/qJxV7ZirsVdirsV +Q+pf8c66/wCMMn/ETir5v/5wvNf8Y/8Abt/7GsVfRMv94/zP68VWYqxHzd/x0Y/+MI/4k2bbQfQf +e63WfWPcktvazXVwlvCOUsh4oCQN/mczZSERZcaMSTQQ9xE8UrxOKPGxVh13U0OSibFoIo0h2ySF +17Y3NqsDzKFW5jEsVCDVD0O2RhMSuuiZQIq+qmNNv3keP0WV44jOyv8AAfTUVLfFTth8SNXfkvAU +C2WsVFsKFJsKtiyuJLSa7RQYIGRZW5AEGSvHYmp+z2wGYBA6lIiatBtk2Kk2FVFsKqTYUKLZJVJs +KqLYUKTYVUWwqzb8n/8AlJrn/mCf/k7Fmr7Y/uh/W/QXM0P1n3PXyc5t2ypa/wC9UP8Arr+sYq8K +/wCc0DT/AAd/28v+xXFWWf8AOLX/ACgdl8rr/qJOKvbMVdirsVdiqH1L/jnXX/GGT/iJxV82/wDO +Fpr/AIx/7dv/AGNYq+ipf71/9Y/rxVYTirEvNv8Ax0Y/+MI/4k2bbQfQfe63WfWPcl+iTxQavazT +MEiR6u56AUzJzxJgQGjCQJglPr290Wa5sRLcQBxcySG4hQALDRiokDLSpanUe+YcIZAJUDy5Hvcq +U4EiyObcuoeXDrtvNzt6fV5E9UqDGJgw4l9kHSu+AY8vhkb8/sSZw4wduSQebNQt57iwe2mjmeCB +VkeJeKeorEmikdMy9LjIErFWWjUTBIruTa+1jTLi+luZbqCSCXTJEhQgcknPD4W26ntmPDDIRoA3 +x/Y3SyRJux9KG/TukQacqRraPNDp1u8ZeJWb62tVZSSNyBkvAmZb8VGR69GPixA6fSPmiLHU/K0e +u6xKj2ymUwm0d6JEyiMeooYo4WrV5bb5CePKccRv1v8AQzjPHxS5Jcl7oR0bW4zJa2pklne3ERV5 +GqOKooeMfuyd1KkEe2XGGTjhzOw/HPm1iUeGXIc0i0O602LRL2K5eNbh7m0aIOByKLJWShPanXMn +NGRmCOVFpxEcJvvCaecdV0G80vUILc2vrQ3iGxMCKrNEYxzPJRv8RauUaTFOMok3vHe23POJBArn +s8/bNo4Si2FVJsKFFskqk2FVFsKFJsKqLYVZr+UH/KTXP/ME/wDydizV9sf3Q/rfoLmaH6z7nr+c +27ZUtf8AeqH/AF1/WMVeE/8AOaRp/g7/ALeX/YrirLf+cWP+UCsfldf9RJxV7birsVdirsVQ+pf8 +c66/4wyf8ROKvmv/AJwrP/KZf9u3/sbxV9Fyn96/+sf14qsJxVifmz/joR/8YR/xJs22g+g+91us ++se5Jbe1murhLeEcpZDxQEgb/M5mykIiy40Ykmgh7iJ4pXicUeNirDrupoclE2LQRRpSSCad/Thj +aV+vBAWNB12GEyA5oAJ5IdskhRbCqk2SVRbChSbCqi2FVJsKFJsKqLYVUmwoUWySqTYVUWwoUmwq +othVmv5Qf8pNc/8AME//ACdizV9sf3Q/rfoLmaH6z7nr2c27ZUtP964f+Mi/rGKvCP8AnNQ/8ob/ +ANvL/sUxVl3/ADiv/wAoDY/K6/6ijir27FXYq7FXYqh9S/4511/xhk/4icVfNP8AzhQa/wCMv+3b +/wBjeKvoyY/vX/1j+vFVmKsT82f8dCP/AIwj/iTZttB9B97rdZ9Y9yA0SeKDV7WaZgkSPV3PQCmZ +OeJMCA0YSBMEp9eahoLXNi1y8E1LmSRnhSgEJDcBIKdeRWvyzDhjyVKrG32+TlSnCxdc1761pUOv +27JJAkTxSxyXMZDChoY+fwLxpTIjBM4zz5jb8Fl4sRMckFpepaILdl1N4ZLmGaaDkqji6XEi8pRQ +D4V+Km3TLcuPJfouiAfl0YY5wr1c7+9U0zVvLy6pfVaBRFHDDYySgKjJGPj+Iq9OTddq0yOXFk4I +8+tsoZIcR5eTWkan5ZjivxxtI5nuXZopWAjeIigCOY2JTuAFGOXHlJjz5fb81xzhvy5oXTtW8vx6 +dp9tN9WEU0F0t2HVWkQ8gYlLUqK1OTyYshlIi9iK/SxhkhwgGuRYLd20MUVu6XCTNMheSNA1YzyI +4tUDfbNnGRJO1U4RFVugWyxipNhQpNhVRbCqk2FCi2SVSbCqi2FCk2FVFsKs1/KH/lJrn/mCf/k7 +Fmr7Y/uh/W/QXM0P1n3PXc5t2ypaH/S4f+Mi/rGKvB/+c1zT/Bv/AG8v+xTFWX/84q/+S/sfld/9 +RRxV7firsVdirsVQ+pf8c66/4wyf8ROKvmj/AJwmNf8AGf8A27f+xvFX0bN/ev8A6x/XiqnirFPN +f/HQj/4wj/iTZttB9B97rdZ9Y9ySRwSzzJDEvOWQhUXxJ+eZxkALLigEmg3Dp13cXjWcSVuF5ckJ +ApwBLb9O2CWSMY8R5JjAk11QDZYxUmwoUWwqpNklRMmhaqquzQcVSJJ2JZRSOU8Ubr3OVjPDv8mf +hSW3nl/VLVbx5owq2DIlzRlPFpfs0od/oxhnjKq/i5LLFIXfRJ2y9rUmwoUmwqothVdZ2VxfXkNn +bKGuLhxHEpIALMaDc7YJzEQSeQTGJJoKF9aT2d3PaTjjPbyNFKoIIDoxVhUe4yUJCQBHIokKNFCN +k0KLYUKTYVUWwqzT8ov+Uluf+YJ/+TsWavtj+6H9b9BczQ/Wfc9dJzm3bKtp/vXB/wAZF/4kMVeD +f85smn+DP+3l/wBimKsv/wCcVP8AyX1h8rv/AKijir3DFXYq7FXYqh9S/wCOddf8YZP+InFXzN/z +hIf+Uz/7dn/Y3ir6Om/vn/1j+vFVMnFWK+av+OhH/wAYh/xJs22g+g+91us+se5LdImih1W0llYJ +Gkqs7HoAD1zKzAmBA7mjEakCUZpN7Zw+ZpbmaQLbM055noQwan31yrNCRxADns2Y5gZLPLdGPqGl +rf3dxFJbtatp7fVIHVaJKvHjGVI3av35UMc+EA3fFv7u9t448RO1cOzceo6G+pz3CtbRXU1lH6Mk +i/uUuKHmCOx6YDjyCIG9CXxpROHETtdfasGreWv0pqAb0hbqIbiFlSiPPDu6oKft7fPD4WXgjzvc +fAr4kOI9ySecb3SpWtoNM4tCPUnldQB8c7luB/1BsMydJCYsy58vk06iUTQio+YNWjkurNbW45QN +Z2sV0F6ExHlxb/VIw4MVA2N+I0jLk3FHoEZr+r6bPB5jWG4Rzdz2rWwH7YQDkR8srwYpAwscgWzL +kiRLfnTvLGsaLZ6ZpkN0LUmS4nW9MyKzpEUJXqKgFqYNTinKUiL5CveuHJERANc1XR9U8rQ+U5YC +LVrn9+LqCdgjvyLcCh9OQseNONKUyOXHlOW962r8WyxzgIVsgLrXfLzeWoZysZ1W5jgs7yFVAZY4 +XYySClN5F2rlscOTxCP4RZHx/UwOSHBf8XJNNf1rym93o7WzWbWsV5E/qRkB44eJDo8QjWifNjvl +GHDlqV8V0fn77bcmSFiqq2HprVpL59tr9zDb2FvdqI2iXhGsMb/C1B/k7k5nnERgMdzIj7XGGQeI +D0tkut655PnutOe9ls7sHVJLgyW0RHGzYMQLgcRVvUZS23xUrmHiw5QJVxD0Vv3+XwcieSBIuj6v +s82P/mlqOjXtzZfo76rIYxKGntXDFkJX01kCxxhSu9NzmT2djnEHiv4/2tOqlEkVTAWzZuGpNhVR +bCrNPyi/5SW5/wCYJ/8Ak7Fmr7Y/uh/W/QXM0P1n3PXM5t2yrZ/71wf8ZF/4kMVeC/8AObZ/5Qz/ +ALef/YpirMP+cUv/ACXth8rv/qKOKvccVdirsVdiqH1L/jnXX/GGT/iJxV8y/wDOER/5TT/t2f8A +Y3ir6OnP75/9Y/rxVTJxVjHmeKV76MojMPSG4BP7TZtdDICBvvddq4kyHuSNra4/30//AAJzO449 +7i8B7lJra5/30/8AwJw8ce9eA9yk1rc/75f/AIE4eOPejgPcpNaXX++X/wCBP9MPHHvXgPcotaXf +++ZP+BP9MPiR7wvAe5Sazu/98Sf8C39MPiR7wjgPcpNZ3n++JP8AgG/ph8SPeF4D3KTWV5/viT/g +G/ph8SPeF4D3KTWV7/yzyf8AAN/TD4ke8LwHuUmsb3/lnk/4Bv6YfEj3hHAe5Sawvv8Alnl/4Bv6 +YfEj3heA9yi1hff8s0v/AADf0w+JHvC8B7lJtPv/APlml/4Bv6YfEj3heA9yk2n3/wDyzS/8A39M +l4ke8I4D3KTadqH/ACyy/wDAN/THxY94XgPcpNp2of8ALLN/yLb+mS8WPeEcB7lJtN1H/llm/wCR +bf0w+LHvC8B7lFtM1L/lkm/5Ft/TD4se8LwS7mYflTZ3cHmO4eaCSNTZuAzqyivqx7VIzWdrTicQ +o/xfoLmaGJEzfc9WznXaKtn/AL2Qf8ZF/wCJDFXgv/Obp/5Qv/t5/wDYpirMf+cUP/JeWHyu/wDq +KOKvccVdirsVdiqH1L/jnXX/ABhk/wCInFXzH/zg+a/40/7dn/Y3ir6PnP76T/WP68VU8VaxVonF +WicVaxVrFWicVaxVrFWsVaJxVrFWsVaxVonFWsVaxVrFWicVaxVrFWsVVbM/6ZB/xkT/AIkMVeCf +85wGn+C/+3n/ANimKsy/5xP/APJd6f8AK7/6ijir3LFXYq7FXYqh9S/4511/xhk/4icVfMH/ADhB +IqT+dLdzxnI05hGdmohug23sWFcVfSNxUTSV/mP68VU8VaJxVonFWsVaxVonFWsVaxVrFWicVaxV +rFWsVaJxVrFWsVaxVonFWsVaxVrFWicVVrEE3kAAqean7jXFXgH/ADnDNGZfJkQYGRF1J2TuFY2o +U/TxOKs2/wCcTww/LrT6gg8bs7+BuiRir3LFXYq7FXYqtkjWSNo2+y4Kt8iKYq+MdZvdb/Ij88bj +WorZp/L2rNIZYFoqy20zh5Y0rsHhkoye1OxOKvqnyn548k+d9Pjv/L+qQ3gdavCjhbiM91lhPxoR +7j5bYqnn6Oi/mb8MVd+jYv5m/DFWv0ZF/O34Yq79GRfzt+GKu/RcP87fhirX6Kh/nb8MVd+iof52 +/D+mKtfomH+dvwxV36Jh/nb8MVd+iIP52/D+mKtfoeD+dvw/pirv0PB/O34f0xV36Gg/nb8P6Yq1 ++hYP52/D+mKu/QsH+/H/AA/pirX6Et/9+P8Ah/TFXfoO3/34/wCH9MVa/Qdv/vx/w/pirv0Fb/78 +f8P6Yq79BW/+/H/D+mKtfoG3/wB+P+H9MVd+gLb/AH4/4f0xVJvM/nDyJ5EsJdQ17U4bTipKRSOG +uJPBYoV+NyfYfPbFXxF+YvnbXvzg/Mhbi1t2jjl42mk2RNfRtkJblIRtU1Z3P0dAMVfY/wCT3lyP +QtCtrGAfubS3SIMRQsTT4j7twqfnir0LFXYq7FXYq7FWJ/mN+W/l/wA9aHJpmqxKXpWCelWRx0I7 +/dir5H83/wDOLfm3Sb10sJllgJPpmYMV4+0kYav0oMVY7/0L55+8bX/gp/8Aqlirv+hfPP3ja/8A +BT/9UsVd/wBC+efvG1/4Kf8A6pYq7/oXzz942v8AwU//AFSxV3/Qvnn7xtf+Cn/6pYq7/oXzz942 +v/BT/wDVLFXf9C+efvG1/wCCn/6pYq7/AKF88/eNr/wU/wD1SxV3/Qvnn7xtf+Cn/wCqWKu/6F88 +/eNr/wAFP/1SxV3/AEL55+8bX/gp/wDqlirv+hfPP3ja/wDBT/8AVLFXf9C+efvG1/4Kf/qlirv+ +hfPP3ja/8FP/ANUsVd/0L55+8bX/AIKf/qlirv8AoXzz942v/BT/APVLFXf9C+efvG1/4Kf/AKpY +q7/oXzz942v/AAU//VLFXf8AQvnn7xtf+Cn/AOqWKu/6F88/eNr/AMFP/wBUsVd/0L55+8bX/gp/ ++qWKu/6F88/eNr/wU/8A1SxVF6Z/zjn5yuLgJdzwQRd3jEsjf8Cyx/rxV71+Vn5J6Z5cH+iwme9k +A9e6loXI60YgURP8kde+Kvd9NsI7G1WFNz1dvFj3xVFYq7FXYq7FXYq7FVskUcqlJEDoeqsAR+OK +oU6NpZP+8yfdirv0Npf/ACzJ+OKu/Q2l/wDLMn44q79DaX/yzJ+OKu/Q2l/8syfjirv0Npf/ACzJ ++OKu/Q2l/wDLMn44q79DaX/yzJ+OKu/Q2l/8syfjirv0Npf/ACzJ+OKu/Q2l/wDLMn44q79DaX/y +zJ+OKu/Q2l/8syfjirv0Npf/ACzJ+OKu/Q2l/wDLMn44q79DaX/yzJ+OKu/Q2l/8syfjirv0Npf/ +ACzJ+OKu/Q2l/wDLMn44q79DaX/yzJ+OKu/Q2l/8syfjirv0Npf/ACzJ+OKuGjaWD/vMn3Yqio4o +4lCRoEQdFUAD8MVXYq7FXYq//9k= + + + + + + +image/svg+xml + + + + + + + + + +2004-03-28T20:07:21Z + +2004-03-28T20:07:21Z + +Illustrator + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 + + + + + + +image/svg+xml + + + + +begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + + + +2004-03-28T20:10:24Z + +2004-03-28T20:13:14Z + +Illustrator + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 +/9k= + + + + + + +image/svg+xml + + + + +end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/gtk-zoom-in_nuvola.svg b/design/icons/gtk-zoom-in_nuvola.svg new file mode 100644 index 0000000..1737355 --- /dev/null +++ b/design/icons/gtk-zoom-in_nuvola.svg @@ -0,0 +1,433 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/icons/help_contents.svg b/design/icons/help_contents.svg new file mode 100644 index 0000000..c3beee8 --- /dev/null +++ b/design/icons/help_contents.svg @@ -0,0 +1,701 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Help Contents + + + help + index + contents + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/inaccessible_tango_emblem-unreadable.svg b/design/icons/inaccessible_tango_emblem-unreadable.svg new file mode 100644 index 0000000..82a4a4f --- /dev/null +++ b/design/icons/inaccessible_tango_emblem-unreadable.svg @@ -0,0 +1,357 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Unreadable + + + emblem + access + denied + unreadable + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/language.png b/design/icons/language.png new file mode 100644 index 0000000000000000000000000000000000000000..3334b5d6db6c37e0f2bb49331307d4e076172507 GIT binary patch literal 3520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Read Only Emblem + + + emblem + read-only + nowrite + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/multimedia-dell-dj-pocket_tango.svg b/design/icons/multimedia-dell-dj-pocket_tango.svg new file mode 100644 index 0000000..0e25751 --- /dev/null +++ b/design/icons/multimedia-dell-dj-pocket_tango.svg @@ -0,0 +1,4405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Portable Media - iPod Mini Silver + October 2005 + + + Ryan Collier (pseudo) + + + + + http://www.tango-project.org + + + http://www.pseudocode.org + + + media + device + ipod + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R + + + + + + + + + + diff --git a/design/icons/multimedia-player-ipod-mini-blue_tango.svg b/design/icons/multimedia-player-ipod-mini-blue_tango.svg new file mode 100644 index 0000000..0c35cf8 --- /dev/null +++ b/design/icons/multimedia-player-ipod-mini-blue_tango.svg @@ -0,0 +1,4126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Portable Media - iPod Mini Blue + October 2005 + + + Ryan Collier (pseudo) + + + http://www.pseudocode.org + + + media + device + ipod + + + + + + http://www.tango-project.org + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MENU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R + + + + + + + + + + + diff --git a/design/icons/multimedia-player-motorola-rokr_tango.svg b/design/icons/multimedia-player-motorola-rokr_tango.svg new file mode 100644 index 0000000..207e27f --- /dev/null +++ b/design/icons/multimedia-player-motorola-rokr_tango.svg @@ -0,0 +1,1025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Portable Media - Motorola ROKR + October 2005 + + + Ryan Collier (pseudo) + + + + + http://www.tango-project.org + + + http://www.pseudocode.org + + + media + device + ipod + phone + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R + + + + + + + + + + diff --git a/design/icons/network-transmit-receive_design.svg b/design/icons/network-transmit-receive_design.svg new file mode 100644 index 0000000..dd48cc4 --- /dev/null +++ b/design/icons/network-transmit-receive_design.svg @@ -0,0 +1,1041 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Computer + 2005-03-08 + + + Jakub Steiner + + + + + workstation + computer + node + client + + + + http://jimmac.musichall.cz/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/icons/pile_of_devices.png b/design/icons/pile_of_devices.png new file mode 100644 index 0000000000000000000000000000000000000000..3591de98d51b9fbeb4f39a0fcb4f63aaa9acf692 GIT binary patch literal 17218 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +