tor-commits
Threads by month
- ----- 2025 -----
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
October 2012
- 20 participants
- 1288 discussions

[translation/tails-greeter] Update translations for tails-greeter
by translation@torproject.org 06 Oct '12
by translation@torproject.org 06 Oct '12
06 Oct '12
commit 661222db579c38695d69ffcaf1563d51649bf13a
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 6 14:45:38 2012 +0000
Update translations for tails-greeter
---
de/de.po | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 137 insertions(+), 0 deletions(-)
diff --git a/de/de.po b/de/de.po
new file mode 100644
index 0000000..18ab528
--- /dev/null
+++ b/de/de.po
@@ -0,0 +1,137 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# <ppt23(a)lkj.hopto.org>, 2012.
+msgid ""
+msgstr ""
+"Project-Id-Version: The Tor Project\n"
+"Report-Msgid-Bugs-To: https://trac.torproject.org/projects/tor\n"
+"POT-Creation-Date: 2012-06-01 15:10+0200\n"
+"PO-Revision-Date: 2012-10-06 14:29+0000\n"
+"Last-Translator: Cooligan <ppt23(a)lkj.hopto.org>\n"
+"Language-Team: LANGUAGE <LL(a)li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../glade/persistencewindow.glade.h:1 ../glade/optionswindow.glade.h:1
+#: ../GdmGreeter/persistencewindow.py:128
+msgid "Login"
+msgstr "Login"
+
+#: ../glade/persistencewindow.glade.h:2 ../glade/optionswindow.glade.h:2
+msgid "Welcome to Tails"
+msgstr ""
+
+#: ../glade/persistencewindow.glade.h:3
+msgid "Use persistence?"
+msgstr ""
+
+#: ../glade/persistencewindow.glade.h:4
+msgid "Yes"
+msgstr "Ja"
+
+#: ../glade/persistencewindow.glade.h:5
+msgid "No"
+msgstr "Nein"
+
+#: ../glade/persistencewindow.glade.h:6
+msgid "Passphrase:"
+msgstr "Passphrase:"
+
+#: ../glade/persistencewindow.glade.h:7
+msgid "Read-Only?"
+msgstr "Schreibgeschützt?"
+
+#: ../glade/persistencewindow.glade.h:8 ../glade/optionswindow.glade.h:8
+#: ../glade/langpanel.glade.h:1
+msgid " "
+msgstr " "
+
+#: ../glade/persistencewindow.glade.h:9
+msgid "More options?"
+msgstr "Weitere Optionen?"
+
+#: ../glade/optionswindow.glade.h:3
+msgid "Administration password"
+msgstr "Administrator-Passwort"
+
+#: ../glade/optionswindow.glade.h:4
+msgid ""
+"Enter an administration password in case you need to perform administration tasks.\n"
+"Otherwise it will be disabled for better security."
+msgstr ""
+
+#: ../glade/optionswindow.glade.h:6
+msgid "Password:"
+msgstr "Passwort:"
+
+#: ../glade/optionswindow.glade.h:7
+msgid "Verify Password:"
+msgstr "Passwort erneut eingeben:"
+
+#: ../glade/optionswindow.glade.h:9
+msgid "Windows Camouflage"
+msgstr ""
+
+#: ../glade/optionswindow.glade.h:10
+msgid ""
+"This option makes Tails look more like Microsoft Windows XP. This may be "
+"useful in public places in order to avoid attracting suspicion."
+msgstr ""
+
+#: ../glade/optionswindow.glade.h:11
+msgid "Activate Microsoft Windows XP Camouflage"
+msgstr ""
+
+#: ../glade/langpanel.glade.h:2 ../GdmGreeter/langpanel.py:42
+msgid "Language"
+msgstr "Sprache"
+
+#: ../glade/langpanel.glade.h:3
+msgid "Locale"
+msgstr ""
+
+#: ../glade/langpanel.glade.h:4
+msgid "Layout"
+msgstr "Layout"
+
+#: ../GdmGreeter/optionswindow.py:68
+msgid "<i>Passwords do not match</i>"
+msgstr "<i>Passwörter stimmen nicht überein</i>"
+
+#: ../GdmGreeter/persistence.py:59
+#, python-format
+msgid ""
+"live-persist failed with return code %(returncode)s:\n"
+"%(stderr)s"
+msgstr ""
+
+#: ../GdmGreeter/persistence.py:99
+#, python-format
+msgid ""
+"cryptsetup failed with return code %(returncode)s:\n"
+"%(stdout)s\n"
+"%(stderr)s"
+msgstr ""
+
+#: ../GdmGreeter/persistence.py:124
+#, python-format
+msgid ""
+"live-persist failed with return code %(returncode)s:\n"
+"%(stdout)s\n"
+"%(stderr)s"
+msgstr ""
+
+#: ../GdmGreeter/persistencewindow.py:94
+msgid "<i>Wrong passphrase. Please try again.</i>"
+msgstr "<i>Flasche Passphrase. Bitte erneut versuchen.</i>"
+
+#: ../GdmGreeter/langpanel.py:124 ../GdmGreeter/langpanel.py:148
+#: ../GdmGreeter/langpanel.py:181
+msgid "Other..."
+msgstr "Andere..."
1
0

[translation/tails-persistence-setup] Update translations for tails-persistence-setup
by translation@torproject.org 06 Oct '12
by translation@torproject.org 06 Oct '12
06 Oct '12
commit e7247e7980dd132712ade3c33a5b7b865e6458ea
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 6 14:45:36 2012 +0000
Update translations for tails-persistence-setup
---
de/de.po | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 319 insertions(+), 0 deletions(-)
diff --git a/de/de.po b/de/de.po
new file mode 100644
index 0000000..7662856
--- /dev/null
+++ b/de/de.po
@@ -0,0 +1,319 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Tails developers
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# <ppt23(a)lkj.hopto.org>, 2012.
+msgid ""
+msgstr ""
+"Project-Id-Version: The Tor Project\n"
+"Report-Msgid-Bugs-To: https://trac.torproject.org/projects/tor\n"
+"POT-Creation-Date: 2012-10-03 16:16+0200\n"
+"PO-Revision-Date: 2012-10-06 14:23+0000\n"
+"Last-Translator: Cooligan <ppt23(a)lkj.hopto.org>\n"
+"Language-Team: LANGUAGE <LL(a)li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:48
+msgid "Personal Data"
+msgstr "persönliche Daten"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:50
+msgid "Keep files stored in the `Persistent' directory"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:58
+msgid "GnuPG"
+msgstr "GnuPG"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:60
+msgid "GnuPG keyrings and configuration"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:68
+msgid "SSH Client"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:70
+msgid "SSH keys, configuration and known hosts"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:78
+msgid "Pidgin"
+msgstr "Pidgin"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:80
+msgid "Pidgin profiles and OTR keyring"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:88
+msgid "Claws Mail"
+msgstr "Claws Mail"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:90
+msgid "Claws Mail profiles and locally stored email"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:98
+msgid "GNOME Keyring"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:100
+msgid "Secrets stored by GNOME Keyring"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:108
+msgid "Network Connections"
+msgstr "Netzwerkverbindungen"
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:110
+msgid "Configuration of network devices and connections"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:118
+msgid "APT Packages"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:120
+msgid "Packages downloaded by APT"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:128
+msgid "APT Lists"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:130
+msgid "Lists downloaded by APT"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:138
+msgid "Dotfiles"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Configuration/Presets.pm:140
+msgid ""
+"Symlink into $HOME every file or directory found in the `dotfiles' directory"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:259
+msgid ""
+"The device Tails is running from cannot be found. Maybe you used the `toram'"
+" option?"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:284
+msgid "'Unparseable partition path.'"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:298
+msgid "Setup Tails persistent volume"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:442
+#, perl-format
+msgid "Device %s already has a persistent volume."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:450
+#, perl-format
+msgid "Device %s has not enough unallocated space."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:458 ../lib/Tails/Persistence/Setup.pm:472
+#, perl-format
+msgid "Device %s has no persistent volume."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:464
+msgid ""
+"Cannot delete the persistent volume while in use. You should restart Tails "
+"without persistence."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:483
+msgid "Persistence volume is not unlocked."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:488
+msgid "Persistence volume is not mounted."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:493
+msgid "Persistence volume is not readable. Permissions or ownership problems?"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:498
+msgid "Persistence volume is not writable. Maybe it was mounted read-only?"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:507
+#, perl-format
+msgid "Tails is running from non-USB device %s."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:513
+#, perl-format
+msgid "Device %s is optical."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:520
+#, perl-format
+msgid "Device %s was not created using Tails USB installer."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:554
+msgid "Error"
+msgstr "Fehler"
+
+#: ../lib/Tails/Persistence/Setup.pm:861
+msgid "Persistence wizard - Finished"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Setup.pm:864
+msgid ""
+"Any changes you have made will only take effect after restarting Tails.\n"
+"\n"
+"You may now close this application."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:54
+msgid "Persistence wizard - Persistent volume creation"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:57
+msgid "Choose a passphrase to protect the persistent volume"
+msgstr ""
+
+#. TRANSLATORS: size, device vendor, device model
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:61
+#, perl-format
+msgid ""
+"A %s persistent volume will be created on the <b>%s %s</b> device. Data on "
+"this device will be stored in an encrypted form protected by a passphrase."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:66
+msgid "Create"
+msgstr "Erstelle"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:110
+msgid ""
+"<b>Beware!</b> Using persistence has consequences that must be well "
+"understood. Tails can't help you if you use it wrong! See <a "
+"href='file:///usr/share/doc/tails/website/doc/first_steps/persistence.en.html'>Tails"
+" documentation about persistence</a> to learn more."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:144
+msgid "Passphrase:"
+msgstr "Passphrase:"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:154
+msgid "Verify Passphrase:"
+msgstr "Passphrase erneut eingeben:"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:167
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:231
+msgid "Passphrase can't be empty"
+msgstr "Passphrase kann nicht leer sein"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:222
+msgid "Passphrases do not match"
+msgstr "Passphrasen stimmen nicht überein"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:262
+#: ../lib/Tails/Persistence/Step/Configure.pm:189
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:269
+#: ../lib/Tails/Persistence/Step/Delete.pm:84
+msgid "Correcting attributes on Tails system partition."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:272
+#: ../lib/Tails/Persistence/Step/Delete.pm:87
+msgid "The attributes of the Tails system partition will be corrected."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:280
+msgid "Mounting Tails persistence partition."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:283
+msgid "The Tails persistence partition will be mounted."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:304
+msgid "Creating..."
+msgstr "Erstelle..."
+
+#: ../lib/Tails/Persistence/Step/Bootstrap.pm:307
+msgid "Creating the persistent volume..."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:60
+msgid "Persistence wizard - Persistent volume configuration"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:63
+msgid "Specify the files that will be saved in the persistent volume"
+msgstr ""
+
+#. TRANSLATORS: partition, size, device vendor, device model
+#: ../lib/Tails/Persistence/Step/Configure.pm:67
+#, perl-format
+msgid ""
+"The selected files will be stored in the encrypted partition %s (%s), on the"
+" <b>%s %s</b> device."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:73
+msgid "Save"
+msgstr "Speichern"
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:135
+msgid "Make custom directory persistent (absolute path):"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:141
+msgid "Destination:"
+msgstr "Ziel:"
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:204
+msgid "Saving..."
+msgstr "Speichere..."
+
+#: ../lib/Tails/Persistence/Step/Configure.pm:207
+msgid "Saving persistence configuration..."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:41
+msgid "Persistence wizard - Persistent volume deletion"
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:44
+msgid "Your persistent data will be deleted."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:48
+#, perl-format
+msgid ""
+"The persistent volume %s (%s), on the <b>%s %s</b> device, will be deleted."
+msgstr ""
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:54
+msgid "Delete"
+msgstr "Löschen"
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:99
+msgid "Deleting..."
+msgstr "Lösche..."
+
+#: ../lib/Tails/Persistence/Step/Delete.pm:102
+msgid "Deleting the persistent volume..."
+msgstr ""
1
0

06 Oct '12
commit 59cb26459e55e4774ad1974569a648beb2e4ae9b
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 6 14:45:05 2012 +0000
Update translations for orbot
---
values-de/strings.xml | 4 ++++
1 files changed, 4 insertions(+), 0 deletions(-)
diff --git a/values-de/strings.xml b/values-de/strings.xml
index ae5b054..6edeb6e 100644
--- a/values-de/strings.xml
+++ b/values-de/strings.xml
@@ -25,6 +25,8 @@
<string name="menu_stop">Stop</string>
<string name="menu_about">Über</string>
<string name="menu_wizard">Wizard</string>
+ <string name="main_layout_download">Download (Geschwindigkeit/Gesamt)</string>
+ <string name="main_layout_upload">Upload (Geschwindigkeit/Gesamtl)</string>
<string name="button_help">Hilfe</string>
<string name="button_close">Schließen</string>
<string name="button_about">Über</string>
@@ -72,6 +74,8 @@
<string name="wizard_tips_msg">Die unten aufgeführten Apps wurden für die Zusammenarbeit mit Orbot entwickelt. Klicken Sie, um das Programm jetzt zu installieren oder verwenden Sie den Android Market.</string>
<string name="wizard_tips_otrchat">Gibberbot - Ein sicheres Instant-Messaging Programm für Android</string>
<string name="wizard_tips_proxy">Proxy-Einstellungen - Lernen Sie Anwendungen so zu konfigurieren, dass sie mit Orbot zusammenarbeiten.</string>
+ <string name="wizard_tips_duckgo">Duckduckgo Suchmaschinen-App</string>
+ <string name="wizard_tips_firefox">Firefox mir Proxy Mobile-Add-On (extra Installation im Anschluss)</string>
<string name="wizard_proxy_help_info">Proxy-Einstellungen</string>
<string name="wizard_proxy_help_msg">Wenn das Android-App, das Sie benutzen, die Verwendung von HTTP- oder SOCKS-Proxys unterstützt, können Sie es so konfigurieren, dass es sich mit Orbot verbindet und Tor verwendet.\n\n\nDie Host-Einstellungen sind 127.0.0.1 oder \"localhost\". Die Port-Einstellungen sind 8118 für HTTP und 9050 für SOCKS. Sie sollten versuchen, SOCKS4A oder SOCKS5 zu verwenden.\n\n\n\nErfahren Sie mehr über die Verwendung von Proxys unter Android in den FAQ unter http://tinyurl.com/proxyandroid\n </string>
<string name="wizard_final">Orbot ist bereit!</string>
1
0

[translation/liveusb-creator] Update translations for liveusb-creator
by translation@torproject.org 06 Oct '12
by translation@torproject.org 06 Oct '12
06 Oct '12
commit d45cccfa0b1df4856b1ef3ed9e409b76f9e212b7
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 6 14:15:34 2012 +0000
Update translations for liveusb-creator
---
de/de.po | 27 ++++++++++++++-------------
1 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/de/de.po b/de/de.po
index b010b38..1f2fb30 100644
--- a/de/de.po
+++ b/de/de.po
@@ -5,13 +5,14 @@
# Translators:
# Cornelius Neckenig <tbull(a)fedoraproject.org>, 2009.
# Marcus Nitzschke <marcusni(a)gmx.de>, 2008.
+# <ppt23(a)lkj.hopto.org>, 2012.
msgid ""
msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: https://trac.torproject.org/projects/tor\n"
"POT-Creation-Date: 2012-04-13 14:16+0200\n"
-"PO-Revision-Date: 2012-10-03 18:09+0000\n"
-"Last-Translator: Tor Project <tor-assistants(a)torproject.org>\n"
+"PO-Revision-Date: 2012-10-06 14:13+0000\n"
+"Last-Translator: Cooligan <ppt23(a)lkj.hopto.org>\n"
"Language-Team: LANGUAGE <LL(a)li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -43,7 +44,7 @@ msgstr "Fehler: Kann die Beschriftung nicht setzen oder die UUID Ihres Gerätes
#: ../liveusb/gui.py:276
#, python-format
msgid "Installation complete! (%s)"
-msgstr ""
+msgstr "Installation abgeschlossen! (%s)"
#: ../liveusb/gui.py:281
msgid "LiveUSB creation failed!"
@@ -81,11 +82,11 @@ msgstr "Persistenter Speicher"
#: ../liveusb/gui.py:586
msgid "Installation complete!"
-msgstr ""
+msgstr "Installation abgeschlossen!"
#: ../liveusb/gui.py:587
msgid "Installation was completed. Press OK to close this program."
-msgstr ""
+msgstr "Installation abgeschlossen. OK drücken um das Programm zu schließen."
#: ../liveusb/gui.py:610
msgid ""
@@ -158,7 +159,7 @@ msgstr ""
#: ../liveusb/gui.py:743
#, python-format
msgid "%(filename)s selected"
-msgstr ""
+msgstr "%(filename)s ausgewählt"
#: ../liveusb/creator.py:91
msgid "You must run this application as root"
@@ -235,7 +236,7 @@ msgstr ""
#: ../liveusb/creator.py:497
msgid "Removing existing Live OS"
-msgstr ""
+msgstr "Entferne bestehendes Live BS"
#: ../liveusb/creator.py:506 ../liveusb/creator.py:517
#, python-format
@@ -326,17 +327,17 @@ msgstr ""
#: ../liveusb/creator.py:912 ../liveusb/creator.py:1236
msgid "Installing bootloader..."
-msgstr ""
+msgstr "Installiere Bootloader..."
#: ../liveusb/creator.py:935 ../liveusb/creator.py:1254
#, python-format
msgid "Removing %(file)s"
-msgstr ""
+msgstr "Entferne %(file)s"
#: ../liveusb/creator.py:1017
#, python-format
msgid "%s already bootable"
-msgstr ""
+msgstr "%s bereits bootfähig"
#: ../liveusb/creator.py:1037
msgid "Unable to find partition"
@@ -345,12 +346,12 @@ msgstr ""
#: ../liveusb/creator.py:1060
#, python-format
msgid "Formatting %(device)s as FAT32"
-msgstr ""
+msgstr "Formatiere %(device)s als FAT32"
#: ../liveusb/creator.py:1109
#, python-format
msgid "Resetting Master Boot Record of %s"
-msgstr ""
+msgstr "Setze Master Boot Record von %s zurück"
#: ../liveusb/creator.py:1112
msgid ""
@@ -364,7 +365,7 @@ msgstr ""
#: ../liveusb/creator.py:1119 ../liveusb/creator.py:1382
#, python-format
msgid "Calculating the SHA1 of %s"
-msgstr ""
+msgstr "Berechne den SHA-1-Hash von %s"
#: ../liveusb/creator.py:1143
msgid "Synchronizing data on disk..."
1
0

[ooni-probe/master] Make nettest and runner support test relative options.
by art@torproject.org 06 Oct '12
by art@torproject.org 06 Oct '12
06 Oct '12
commit c99b1657c35a2c83126001282fe01ecaee57b66f
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Sat Oct 6 10:21:20 2012 +0000
Make nettest and runner support test relative options.
---
nettests/httphost.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++
ooni/nettest.py | 2 +
ooni/plugins/httphost.py | 124 ----------------------------------------------
ooni/runner.py | 17 +++++-
4 files changed, 140 insertions(+), 127 deletions(-)
diff --git a/nettests/httphost.py b/nettests/httphost.py
new file mode 100644
index 0000000..b57a50d
--- /dev/null
+++ b/nettests/httphost.py
@@ -0,0 +1,124 @@
+"""
+ HTTP Host based filtering
+ *************************
+
+ This test detect HTTP Host field
+ based filtering.
+"""
+import os
+from datetime import datetime
+
+import urllib2
+import httplib
+try:
+ from BeautifulSoup import BeautifulSoup
+except:
+ print "Beautiful Soup not installed. HTTPHost may not work"
+
+# XXX reduce boilerplating.
+from zope.interface import implements
+from twisted.python import usage
+from twisted.plugin import IPlugin
+
+from ooni.plugoo.assets import Asset
+from ooni.plugoo.tests import ITest, OONITest
+
+class HTTPHostArgs(usage.Options):
+ optParameters = [['asset', 'a', None, 'Asset file'],
+ ['controlserver', 'c', 'google.com', 'Specify the control server'],
+ ['resume', 'r', 0, 'Resume at this index'],
+ ['other', 'o', None, 'Other arguments']]
+ def control(self, experiment_result, args):
+ print "Experiment Result:", experiment_result
+ print "Args", args
+ return experiment_result
+
+ def experiment(self, args):
+ import urllib
+ req = urllib.urlopen(args['asset'])
+ return {'page': req.readlines()}
+
+class HTTPHostAsset(Asset):
+ """
+ This is the asset that should be used by the Test. It will
+ contain all the code responsible for parsing the asset file
+ and should be passed on instantiation to the test.
+ """
+ def __init__(self, file=None):
+ self = Asset.__init__(self, file)
+
+ def parse_line(self, line):
+ return line.split(',')[1].replace('\n','')
+
+
+class HTTPHostTest(OONITest):
+ implements(IPlugin, ITest)
+
+ shortName = "httphost"
+ description = "HTTP Host plugin"
+ requirements = None
+ options = HTTPHostArgs
+ # Tells this to be blocking.
+ blocking = True
+
+ def check_response(self, response):
+ soup = BeautifulSoup(response)
+ if soup.head.title.string == "Blocked":
+ # Response indicates censorship
+ return True
+ else:
+ # Response does not indicate censorship
+ return False
+
+ def is_censored(self, response):
+ if response:
+ soup = BeautifulSoup(response)
+ censored = self.check_response(response)
+ else:
+ censored = "unreachable"
+ return censored
+
+ def urllib2_test(self, control_server, host):
+ req = urllib2.Request(control_server)
+ req.add_header('Host', host)
+ try:
+ r = urllib2.urlopen(req)
+ response = r.read()
+ censored = self.is_censored(response)
+ except Exception, e:
+ censored = "Error! %s" % e
+
+ return censored
+
+ def httplib_test(self, control_server, host):
+ censored = None
+ response = None
+ try:
+ conn = httplib.HTTPConnection(control_server)
+ conn.putrequest("GET", "", skip_host=True, skip_accept_encoding=True)
+ conn.putheader("Host", host)
+ conn.putheader("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6")
+ conn.endheaders()
+ r = conn.getresponse()
+ response = r.read()
+ censored = self.is_censored(response)
+ except Exception, e:
+ censored = "Error! %s" % e
+
+ return (censored, response)
+
+ def experiment(self, args):
+ control_server = self.local_options['controlserver']
+ url = 'http://torproject.org/' if not 'asset' in args else args['asset']
+ censored = self.httplib_test(control_server, url)
+ return {'control': control_server,
+ 'host': url,
+ 'censored': censored}
+
+ def load_assets(self):
+ if self.local_options and self.local_options['asset']:
+ return {'asset': Asset(self.local_options['asset'])}
+ else:
+ return {}
+
+httphost = HTTPHostTest(None, None, None)
diff --git a/ooni/nettest.py b/ooni/nettest.py
index d88c044..a01050e 100644
--- a/ooni/nettest.py
+++ b/ooni/nettest.py
@@ -80,6 +80,8 @@ class TestCase(unittest.TestCase):
report = {}
report['errors'] = []
+ optParameters = None
+
def getOptions(self):
if self.inputFile:
fp = open(self.inputFile)
diff --git a/ooni/plugins/httphost.py b/ooni/plugins/httphost.py
deleted file mode 100644
index 7c783a1..0000000
--- a/ooni/plugins/httphost.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
- HTTP Host based filtering
- *************************
-
- This test detect HTTP Host field
- based filtering.
-"""
-import os
-from datetime import datetime
-
-import urllib2
-import httplib
-try:
- from BeautifulSoup import BeautifulSoup
-except:
- print "Beautiful Soup not installed. HTTPHost may not work"
-
-# XXX reduce boilerplating.
-from zope.interface import implements
-from twisted.python import usage
-from twisted.plugin import IPlugin
-
-from plugoo.assets import Asset
-from plugoo.tests import ITest, OONITest
-
-class HTTPHostArgs(usage.Options):
- optParameters = [['asset', 'a', None, 'Asset file'],
- ['controlserver', 'c', 'google.com', 'Specify the control server'],
- ['resume', 'r', 0, 'Resume at this index'],
- ['other', 'o', None, 'Other arguments']]
- def control(self, experiment_result, args):
- print "Experiment Result:", experiment_result
- print "Args", args
- return experiment_result
-
- def experiment(self, args):
- import urllib
- req = urllib.urlopen(args['asset'])
- return {'page': req.readlines()}
-
-class HTTPHostAsset(Asset):
- """
- This is the asset that should be used by the Test. It will
- contain all the code responsible for parsing the asset file
- and should be passed on instantiation to the test.
- """
- def __init__(self, file=None):
- self = Asset.__init__(self, file)
-
- def parse_line(self, line):
- return line.split(',')[1].replace('\n','')
-
-
-class HTTPHostTest(OONITest):
- implements(IPlugin, ITest)
-
- shortName = "httphost"
- description = "HTTP Host plugin"
- requirements = None
- options = HTTPHostArgs
- # Tells this to be blocking.
- blocking = True
-
- def check_response(self, response):
- soup = BeautifulSoup(response)
- if soup.head.title.string == "Blocked":
- # Response indicates censorship
- return True
- else:
- # Response does not indicate censorship
- return False
-
- def is_censored(self, response):
- if response:
- soup = BeautifulSoup(response)
- censored = self.check_response(response)
- else:
- censored = "unreachable"
- return censored
-
- def urllib2_test(self, control_server, host):
- req = urllib2.Request(control_server)
- req.add_header('Host', host)
- try:
- r = urllib2.urlopen(req)
- response = r.read()
- censored = self.is_censored(response)
- except Exception, e:
- censored = "Error! %s" % e
-
- return censored
-
- def httplib_test(self, control_server, host):
- censored = None
- response = None
- try:
- conn = httplib.HTTPConnection(control_server)
- conn.putrequest("GET", "", skip_host=True, skip_accept_encoding=True)
- conn.putheader("Host", host)
- conn.putheader("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6")
- conn.endheaders()
- r = conn.getresponse()
- response = r.read()
- censored = self.is_censored(response)
- except Exception, e:
- censored = "Error! %s" % e
-
- return (censored, response)
-
- def experiment(self, args):
- control_server = self.local_options['controlserver']
- url = 'http://torproject.org/' if not 'asset' in args else args['asset']
- censored = self.httplib_test(control_server, url)
- return {'control': control_server,
- 'host': url,
- 'censored': censored}
-
- def load_assets(self):
- if self.local_options and self.local_options['asset']:
- return {'asset': Asset(self.local_options['asset'])}
- else:
- return {}
-
-httphost = HTTPHostTest(None, None, None)
diff --git a/ooni/runner.py b/ooni/runner.py
index 8ed844d..c6958bc 100644
--- a/ooni/runner.py
+++ b/ooni/runner.py
@@ -5,7 +5,7 @@ import time
import inspect
from twisted.internet import defer, reactor
-from twisted.python import reflect, failure
+from twisted.python import reflect, failure, usage
from twisted.trial import unittest
from twisted.trial.runner import TrialRunner, TestLoader
@@ -91,6 +91,16 @@ def adaptLegacyTest(obj, config):
return LegacyOONITest
+def processTest(obj, config):
+ if obj.optParameters:
+ class Options(usage.Options):
+ optParameters = obj.optParameters
+
+ options = Options()
+ options.parseOptions(config['subArgs'])
+
+ obj.localOptions = options
+ return obj
def findTestClassesFromConfig(config):
"""
@@ -106,7 +116,7 @@ def findTestClassesFromConfig(config):
module = filenameToModule(filename)
for name, val in inspect.getmembers(module):
if isTestCase(val):
- classes.append(val)
+ classes.append(processTest(val, config))
elif isLegacyTest(val):
classes.append(adaptLegacyTest(val, config))
return classes
@@ -135,7 +145,8 @@ def loadTestsAndOptions(classes):
for klass in classes:
try:
k = klass()
- options.append(k.getOptions())
+ opts = k.getOptions()
+ options.append(opts)
except AttributeError:
options.append([])
1
0

06 Oct '12
commit 9c7f354bcb842be19d687305c8a2e4e57cad6276
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Sat Oct 6 10:47:03 2012 +0000
Implement input File support for new API.
* Port HTTP Host test to new API
---
nettests/http_host.py | 28 +++--------
nettests/httphost.py | 124 -------------------------------------------------
ooni/nettest.py | 14 +++---
ooni/runner.py | 5 ++
4 files changed, 20 insertions(+), 151 deletions(-)
diff --git a/nettests/http_host.py b/nettests/http_host.py
index 4834ba3..2228c52 100644
--- a/nettests/http_host.py
+++ b/nettests/http_host.py
@@ -11,37 +11,25 @@
from ooni.templates import httpt
-good_http_server = "http://127.0.0.1:8090/"
-
class HTTPHost(httpt.HTTPTest):
name = "HTTP Host"
author = "Arturo Filastò"
version = 0.1
+ optParameters = [['url', 'u', 'http://torproject.org/', 'Test single site'],
+ ['backend', 'b', 'http://ooni.nu/test/', 'Test backend to use'],
+ ]
- inputs = ['google.com', 'wikileaks.org',
- 'torproject.org']
+ inputFile = ['urls', 'f', None, 'Urls file']
def test_send_host_header(self):
headers = {}
headers["Host"] = [self.input]
- return self.doRequest(good_http_server, headers=headers)
+ return self.doRequest(self.localOptions['backend'], headers=headers)
def processResponseBody(self, body):
- # XXX here shall go your logic
- # for processing the body
- if 'blocked' in body:
- self.report['censored'] = True
+ if 'not censored' in body:
+ self.report['trans_http_proxy'] = False
else:
- self.report['censored'] = False
-
- def processResponseHeaders(self, headers):
- # XXX place in here all the logic for handling the processing of HTTP
- # Headers.
- if headers.hasHeader('location'):
- self.report['redirect'] = True
-
- server = headers.getRawHeaders("Server")
- if server:
- self.report['http_server'] = str(server.pop())
+ self.report['trans_http_proxy'] = True
diff --git a/nettests/httphost.py b/nettests/httphost.py
deleted file mode 100644
index b57a50d..0000000
--- a/nettests/httphost.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
- HTTP Host based filtering
- *************************
-
- This test detect HTTP Host field
- based filtering.
-"""
-import os
-from datetime import datetime
-
-import urllib2
-import httplib
-try:
- from BeautifulSoup import BeautifulSoup
-except:
- print "Beautiful Soup not installed. HTTPHost may not work"
-
-# XXX reduce boilerplating.
-from zope.interface import implements
-from twisted.python import usage
-from twisted.plugin import IPlugin
-
-from ooni.plugoo.assets import Asset
-from ooni.plugoo.tests import ITest, OONITest
-
-class HTTPHostArgs(usage.Options):
- optParameters = [['asset', 'a', None, 'Asset file'],
- ['controlserver', 'c', 'google.com', 'Specify the control server'],
- ['resume', 'r', 0, 'Resume at this index'],
- ['other', 'o', None, 'Other arguments']]
- def control(self, experiment_result, args):
- print "Experiment Result:", experiment_result
- print "Args", args
- return experiment_result
-
- def experiment(self, args):
- import urllib
- req = urllib.urlopen(args['asset'])
- return {'page': req.readlines()}
-
-class HTTPHostAsset(Asset):
- """
- This is the asset that should be used by the Test. It will
- contain all the code responsible for parsing the asset file
- and should be passed on instantiation to the test.
- """
- def __init__(self, file=None):
- self = Asset.__init__(self, file)
-
- def parse_line(self, line):
- return line.split(',')[1].replace('\n','')
-
-
-class HTTPHostTest(OONITest):
- implements(IPlugin, ITest)
-
- shortName = "httphost"
- description = "HTTP Host plugin"
- requirements = None
- options = HTTPHostArgs
- # Tells this to be blocking.
- blocking = True
-
- def check_response(self, response):
- soup = BeautifulSoup(response)
- if soup.head.title.string == "Blocked":
- # Response indicates censorship
- return True
- else:
- # Response does not indicate censorship
- return False
-
- def is_censored(self, response):
- if response:
- soup = BeautifulSoup(response)
- censored = self.check_response(response)
- else:
- censored = "unreachable"
- return censored
-
- def urllib2_test(self, control_server, host):
- req = urllib2.Request(control_server)
- req.add_header('Host', host)
- try:
- r = urllib2.urlopen(req)
- response = r.read()
- censored = self.is_censored(response)
- except Exception, e:
- censored = "Error! %s" % e
-
- return censored
-
- def httplib_test(self, control_server, host):
- censored = None
- response = None
- try:
- conn = httplib.HTTPConnection(control_server)
- conn.putrequest("GET", "", skip_host=True, skip_accept_encoding=True)
- conn.putheader("Host", host)
- conn.putheader("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6")
- conn.endheaders()
- r = conn.getresponse()
- response = r.read()
- censored = self.is_censored(response)
- except Exception, e:
- censored = "Error! %s" % e
-
- return (censored, response)
-
- def experiment(self, args):
- control_server = self.local_options['controlserver']
- url = 'http://torproject.org/' if not 'asset' in args else args['asset']
- censored = self.httplib_test(control_server, url)
- return {'control': control_server,
- 'host': url,
- 'censored': censored}
-
- def load_assets(self):
- if self.local_options and self.local_options['asset']:
- return {'asset': Asset(self.local_options['asset'])}
- else:
- return {}
-
-httphost = HTTPHostTest(None, None, None)
diff --git a/ooni/nettest.py b/ooni/nettest.py
index a01050e..89dd279 100644
--- a/ooni/nettest.py
+++ b/ooni/nettest.py
@@ -24,11 +24,6 @@ class InputTestSuite(pyunit.TestSuite):
self._idx += 1
return result
-def lineByLine(fp):
- for x in fp.readlines():
- yield x.strip()
- fp.close()
-
class TestCase(unittest.TestCase):
"""
This is the monad of the OONI nettest universe. When you write a nettest
@@ -75,17 +70,22 @@ class TestCase(unittest.TestCase):
inputs = [None]
inputFile = None
- inputProcessor = lineByLine
+
report = {}
report['errors'] = []
optParameters = None
+ def inputProcessor(self, fp):
+ for x in fp.readlines():
+ yield x.strip()
+ fp.close()
+
def getOptions(self):
if self.inputFile:
fp = open(self.inputFile)
- self.inputs = inputProcessor(fp)
+ self.inputs = self.inputProcessor(fp)
# XXX perhaps we may want to name and version to be inside of a
# different object that is not called options.
return {'inputs': self.inputs,
diff --git a/ooni/runner.py b/ooni/runner.py
index c6958bc..a753d02 100644
--- a/ooni/runner.py
+++ b/ooni/runner.py
@@ -96,10 +96,15 @@ def processTest(obj, config):
class Options(usage.Options):
optParameters = obj.optParameters
+ inputFile = obj.inputFile
+ Options.optParameters.append(inputFile)
+
options = Options()
options.parseOptions(config['subArgs'])
obj.localOptions = options
+ obj.inputFile = options[inputFile[0]]
+
return obj
def findTestClassesFromConfig(config):
1
0

[ooni-probe/master] Fail gracefully when twisted is <= 10.x and we attempt to import RedirectAgent
by art@torproject.org 06 Oct '12
by art@torproject.org 06 Oct '12
06 Oct '12
commit 302dd54259fc6e69d3f276bcb458350344552887
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Sat Oct 6 09:49:01 2012 +0000
Fail gracefully when twisted is <= 10.x and we attempt to import RedirectAgent
---
ooni/nettest.py | 3 +++
ooni/templates/httpt.py | 11 +++++++++--
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/ooni/nettest.py b/ooni/nettest.py
index d7188c6..d88c044 100644
--- a/ooni/nettest.py
+++ b/ooni/nettest.py
@@ -77,6 +77,9 @@ class TestCase(unittest.TestCase):
inputFile = None
inputProcessor = lineByLine
+ report = {}
+ report['errors'] = []
+
def getOptions(self):
if self.inputFile:
fp = open(self.inputFile)
diff --git a/ooni/templates/httpt.py b/ooni/templates/httpt.py
index 4b8fa9b..963189a 100644
--- a/ooni/templates/httpt.py
+++ b/ooni/templates/httpt.py
@@ -65,8 +65,15 @@ class HTTPTest(TestCase):
self.agent = Agent(reactor)
if self.followRedirects:
- from twisted.web.client import RedirectAgent
- self.agent = RedirectAgent(self.agent)
+ try:
+ from twisted.web.client import RedirectAgent
+ self.agent = RedirectAgent(self.agent)
+ except:
+ log.err("Warning! You are running an old version of twisted"\
+ "(<= 10.1). I will not be able to follow redirects."\
+ "This may make the testing less precise.")
+ self.report['errors'].append("Could not import RedirectAgent")
+
self.request = {}
self.response = {}
1
0

06 Oct '12
commit 5cdc37ff80f1c8343e26f94fd7b7a29b3081ed03
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Sat Oct 6 09:37:11 2012 +0000
Port captive portal test to new API.
---
nettests/captiveportal.py | 57 +++++++++++++++++++--------------------------
ooni/runner.py | 2 +-
2 files changed, 25 insertions(+), 34 deletions(-)
diff --git a/nettests/captiveportal.py b/nettests/captiveportal.py
index 4bf99b3..6eed04d 100644
--- a/nettests/captiveportal.py
+++ b/nettests/captiveportal.py
@@ -18,13 +18,8 @@ import string
import urllib2
from urlparse import urlparse
-from zope.interface import implements
-from twisted.python import usage
-from twisted.plugin import IPlugin
-
-from ooni.plugoo.assets import Asset
-from ooni.plugoo.tests import ITest, OONITest
-from ooni.protocols import http
+from ooni import nettest
+from ooni.templates import httpt
from ooni.utils import log
try:
@@ -36,28 +31,21 @@ except ImportError:
__plugoo__ = "captiveportal"
__desc__ = "Captive portal detection test"
-class CaptivePortalArgs(usage.Options):
- optParameters = [['asset', 'a', None, 'Asset file'],
- ['resume', 'r', 0, 'Resume at this index'],
- ['experiment-url', 'e', 'http://google.com/', 'Experiment URL'],
- ['user-agent', 'u', random.choice(http.useragents),
- 'User agent for HTTP requests']
- ]
+optParameters = [['asset', 'a', None, 'Asset file'],
+ ['experiment-url', 'e', 'http://google.com/', 'Experiment URL'],
+ ['user-agent', 'u', random.choice(httpt.useragents),
+ 'User agent for HTTP requests']
+ ]
-class CaptivePortal(OONITest):
+class CaptivePortal(nettest.TestCase):
"""
Compares content and status codes of HTTP responses, and attempts
to determine if content has been altered.
"""
- implements(IPlugin, ITest)
-
- shortName = "captivep"
+ name = "captivep"
description = "Captive Portal Test"
requirements = None
- options = CaptivePortalArgs
- # Tells this to be blocking.
- blocking = True
def http_fetch(self, url, headers={}):
"""
@@ -549,7 +537,9 @@ class CaptivePortal(OONITest):
in the experimental content. Returns True if matches are found,
and False if otherwise.
"""
- experiment_url = self.local_options['experiment-url']
+ # XXX put this back to being parametrized
+ #experiment_url = self.local_options['experiment-url']
+ experiment_url = 'http://google.com/'
control_result = 'XX'
control_code = 200
ua = self.local_options['user-agent']
@@ -565,7 +555,8 @@ class CaptivePortal(OONITest):
if status_match and content_match:
log.msg("The test for '%s'" % experiment_url)
log.msg("was unable to detect a captive portal.")
- return experiment_result, True
+
+ self.report['result'] = True
elif status_match and not content_match:
log.msg("Retrying '%s' with fuzzy match enabled."
@@ -574,19 +565,20 @@ class CaptivePortal(OONITest):
control_result,
fuzzy=True)
if fuzzy_match:
- return experiment_result, True
+ self.report['result'] = True
else:
log.msg("Found modified content on '%s'," % experiment_url)
log.msg("which could indicate a captive portal.")
- return experiment_result, False
+ self.report['result'] = False
else:
log.msg("The content comparison test for ")
log.msg("'%s'" % experiment_url)
log.msg("shows that your HTTP traffic is filtered.")
- return experiment_result, False
- def experiment(self, args):
+ self.report['result'] = False
+
+ def test_captive_portal(self):
"""
Runs the CaptivePortal(Test).
@@ -608,22 +600,21 @@ class CaptivePortal(OONITest):
Any combination of the above tests can be run.
"""
- report = {}
log.msg("")
log.msg("Running vendor tests...")
- report['vendor_tests'] = self.run_vendor_tests()
+ self.report['vendor_tests'] = self.run_vendor_tests()
log.msg("")
log.msg("Running vendor DNS-based tests...")
- report['vendor_dns_tests'] = self.run_vendor_dns_tests()
+ self.report['vendor_dns_tests'] = self.run_vendor_dns_tests()
log.msg("")
log.msg("Checking that DNS requests are not being tampered...")
- report['check0x20'] = self.check_0x20_to_auth_ns('ooni.nu')
+ self.report['check0x20'] = self.check_0x20_to_auth_ns('ooni.nu')
log.msg("")
log.msg("Captive portal test finished!")
- return report
-cp = CaptivePortal(None, None, None)
+ self.control(self.report)
+
diff --git a/ooni/runner.py b/ooni/runner.py
index a4c88a9..8ed844d 100644
--- a/ooni/runner.py
+++ b/ooni/runner.py
@@ -6,7 +6,7 @@ import inspect
from twisted.internet import defer, reactor
from twisted.python import reflect, failure
-#from twisted.python import log
+
from twisted.trial import unittest
from twisted.trial.runner import TrialRunner, TestLoader
from twisted.trial.runner import isPackage, isTestCase, ErrorHolder
1
0

[ooni-probe/master] Move captive portal test to nettest directory for porting to new API
by art@torproject.org 06 Oct '12
by art@torproject.org 06 Oct '12
06 Oct '12
commit dea9dae2d32f4ce1cd90df8a4cbf66457763dfaf
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Sat Oct 6 09:05:23 2012 +0000
Move captive portal test to nettest directory for porting to new API
* Minor change to runner to support mutliple inputs
---
nettests/captiveportal.py | 629 +++++++++++++++++++++++++++++++++++++++++
ooni/plugins/captiveportal.py | 629 -----------------------------------------
ooni/runner.py | 1 +
3 files changed, 630 insertions(+), 629 deletions(-)
diff --git a/nettests/captiveportal.py b/nettests/captiveportal.py
new file mode 100644
index 0000000..4bf99b3
--- /dev/null
+++ b/nettests/captiveportal.py
@@ -0,0 +1,629 @@
+# -*- coding: utf-8 -*-
+"""
+ captiveportal
+ *************
+
+ This test is a collection of tests to detect the presence of a
+ captive portal. Code is taken, in part, from the old ooni-probe,
+ which was written by Jacob Appelbaum and Arturo Filastò.
+
+ :copyright: (c) 2012 Isis Lovecruft
+ :license: see LICENSE for more details
+"""
+import base64
+import os
+import random
+import re
+import string
+import urllib2
+from urlparse import urlparse
+
+from zope.interface import implements
+from twisted.python import usage
+from twisted.plugin import IPlugin
+
+from ooni.plugoo.assets import Asset
+from ooni.plugoo.tests import ITest, OONITest
+from ooni.protocols import http
+from ooni.utils import log
+
+try:
+ from dns import resolver
+except ImportError:
+ print "The dnspython module was not found. https://crate.io/packages/dnspython/"
+ resolver = None
+
+__plugoo__ = "captiveportal"
+__desc__ = "Captive portal detection test"
+
+class CaptivePortalArgs(usage.Options):
+ optParameters = [['asset', 'a', None, 'Asset file'],
+ ['resume', 'r', 0, 'Resume at this index'],
+ ['experiment-url', 'e', 'http://google.com/', 'Experiment URL'],
+ ['user-agent', 'u', random.choice(http.useragents),
+ 'User agent for HTTP requests']
+ ]
+
+class CaptivePortal(OONITest):
+ """
+ Compares content and status codes of HTTP responses, and attempts
+ to determine if content has been altered.
+ """
+
+ implements(IPlugin, ITest)
+
+ shortName = "captivep"
+ description = "Captive Portal Test"
+ requirements = None
+ options = CaptivePortalArgs
+ # Tells this to be blocking.
+ blocking = True
+
+ def http_fetch(self, url, headers={}):
+ """
+ Parses an HTTP url, fetches it, and returns a urllib2 response
+ object.
+ """
+ url = urlparse(url).geturl()
+ request = urllib2.Request(url, None, headers)
+ response = urllib2.urlopen(request)
+ response_headers = dict(response.headers)
+ return response, response_headers
+
+ def http_content_match_fuzzy_opt(self, experimental_url, control_result,
+ headers=None, fuzzy=False):
+ """
+ Makes an HTTP request on port 80 for experimental_url, then
+ compares the response_content of experimental_url with the
+ control_result. Optionally, if the fuzzy parameter is set to
+ True, the response_content is compared with a regex of the
+ control_result. If the response_content from the
+ experimental_url and the control_result match, returns True
+ with the HTTP status code and headers; False, status code, and
+ headers if otherwise.
+ """
+
+ if headers is None:
+ default_ua = self.local_options['user-agent']
+ headers = {'User-Agent': default_ua}
+
+ response, response_headers = self.http_fetch(experimental_url, headers)
+ response_content = response.read()
+ response_code = response.code
+ if response_content is None:
+ log.warn("HTTP connection appears to have failed.")
+ return False, False, False
+
+ if fuzzy:
+ pattern = re.compile(control_result)
+ match = pattern.search(response_content)
+ log.msg("Fuzzy HTTP content comparison for experiment URL")
+ log.msg("'%s'" % experimental_url)
+ if not match:
+ log.msg("does not match!")
+ return False, response_code, response_headers
+ else:
+ log.msg("and the expected control result yielded a match.")
+ return True, response_code, response_headers
+ else:
+ if str(response_content) != str(control_result):
+ log.msg("HTTP content comparison of experiment URL")
+ log.msg("'%s'" % experimental_url)
+ log.msg("and the expected control result do not match.")
+ return False, response_code, response_headers
+ else:
+ return True, response_code, response_headers
+
+ def http_status_code_match(self, experiment_code, control_code):
+ """
+ Compare two HTTP status codes, returns True if they match.
+ """
+ return int(experiment_code) == int(control_code)
+
+ def http_status_code_no_match(self, experiment_code, control_code):
+ """
+ Compare two HTTP status codes, returns True if they do not match.
+ """
+ return int(experiment_code) != int(control_code)
+
+ def dns_resolve(self, hostname, nameserver=None):
+ """
+ Resolves hostname(s) though nameserver to corresponding
+ address(es). hostname may be either a single hostname string,
+ or a list of strings. If nameserver is not given, use local
+ DNS resolver, and if that fails try using 8.8.8.8.
+ """
+ if not resolver:
+ log.msg("dnspython is not installed.\
+ Cannot perform DNS Resolve test")
+ return []
+ if isinstance(hostname, str):
+ hostname = [hostname]
+
+ if nameserver is not None:
+ res = resolver.Resolver(configure=False)
+ res.nameservers = [nameserver]
+ else:
+ res = resolver.Resolver()
+
+ response = []
+ answer = None
+
+ for hn in hostname:
+ try:
+ answer = res.query(hn)
+ except resolver.NoNameservers:
+ res.nameservers = ['8.8.8.8']
+ try:
+ answer = res.query(hn)
+ except resolver.NXDOMAIN:
+ log.msg("DNS resolution for %s returned NXDOMAIN" % hn)
+ response.append('NXDOMAIN')
+ except resolver.NXDOMAIN:
+ log.msg("DNS resolution for %s returned NXDOMAIN" % hn)
+ response.append('NXDOMAIN')
+ finally:
+ if not answer:
+ return response
+ for addr in answer:
+ response.append(addr.address)
+ return response
+
+ def dns_resolve_match(self, experiment_hostname, control_address):
+ """
+ Resolve experiment_hostname, and check to see that it returns
+ an experiment_address which matches the control_address. If
+ they match, returns True and experiment_address; otherwise
+ returns False and experiment_address.
+ """
+ experiment_address = self.dns_resolve(experiment_hostname)
+ if not experiment_address:
+ log.debug("dns_resolve() for %s failed" % experiment_hostname)
+ return None, experiment_address
+
+ if len(set(experiment_address) & set([control_address])) > 0:
+ return True, experiment_address
+ else:
+ log.msg("DNS comparison of control '%s' does not" % control_address)
+ log.msg("match experiment response '%s'" % experiment_address)
+ return False, experiment_address
+
+ def get_auth_nameservers(self, hostname):
+ """
+ Many CPs set a nameserver to be used. Let's query that
+ nameserver for the authoritative nameservers of hostname.
+
+ The equivalent of:
+ $ dig +short NS ooni.nu
+ """
+ if not resolver:
+ log.msg("dnspython not installed.")
+ log.msg("Cannot perform test.")
+ return []
+
+ res = resolver.Resolver()
+ answer = res.query(hostname, 'NS')
+ auth_nameservers = []
+ for auth in answer:
+ auth_nameservers.append(auth.to_text())
+ return auth_nameservers
+
+ def hostname_to_0x20(self, hostname):
+ """
+ MaKEs yOur HOsTnaME lOoK LiKE THis.
+
+ For more information, see:
+ D. Dagon, et. al. "Increased DNS Forgery Resistance
+ Through 0x20-Bit Encoding". Proc. CSS, 2008.
+ """
+ hostname_0x20 = ''
+ for char in hostname:
+ l33t = random.choice(['caps', 'nocaps'])
+ if l33t == 'caps':
+ hostname_0x20 += char.capitalize()
+ else:
+ hostname_0x20 += char.lower()
+ return hostname_0x20
+
+ def check_0x20_to_auth_ns(self, hostname, sample_size=None):
+ """
+ Resolve a 0x20 DNS request for hostname over hostname's
+ authoritative nameserver(s), and check to make sure that
+ the capitalization in the 0x20 request matches that of the
+ response. Also, check the serial numbers of the SOA (Start
+ of Authority) records on the authoritative nameservers to
+ make sure that they match.
+
+ If sample_size is given, a random sample equal to that number
+ of authoritative nameservers will be queried; default is 5.
+ """
+ log.msg("")
+ log.msg("Testing random capitalization of DNS queries...")
+ log.msg("Testing that Start of Authority serial numbers match...")
+
+ auth_nameservers = self.get_auth_nameservers(hostname)
+
+ if sample_size is None:
+ sample_size = 5
+ resolved_auth_ns = random.sample(self.dns_resolve(auth_nameservers),
+ sample_size)
+
+ querynames = []
+ answernames = []
+ serials = []
+
+ # Even when gevent monkey patching is on, the requests here
+ # are sent without being 0x20'd, so we need to 0x20 them.
+ hostname = self.hostname_to_0x20(hostname)
+
+ for auth_ns in resolved_auth_ns:
+ res = resolver.Resolver(configure=False)
+ res.nameservers = [auth_ns]
+ try:
+ answer = res.query(hostname, 'SOA')
+ except resolver.Timeout:
+ continue
+ querynames.append(answer.qname.to_text())
+ answernames.append(answer.rrset.name.to_text())
+ for soa in answer:
+ serials.append(str(soa.serial))
+
+ if len(set(querynames).intersection(answernames)) == 1:
+ log.msg("Capitalization in DNS queries and responses match.")
+ name_match = True
+ else:
+ log.msg("The random capitalization '%s' used in" % hostname)
+ log.msg("DNS queries to that hostname's authoritative")
+ log.msg("nameservers does not match the capitalization in")
+ log.msg("the response.")
+ name_match = False
+
+ if len(set(serials)) == 1:
+ log.msg("Start of Authority serial numbers all match.")
+ serial_match = True
+ else:
+ log.msg("Some SOA serial numbers did not match the rest!")
+ serial_match = False
+
+ ret = name_match, serial_match, querynames, answernames, serials
+
+ if name_match and serial_match:
+ log.msg("Your DNS queries do not appear to be tampered.")
+ return ret
+ elif name_match or serial_match:
+ log.msg("Something is tampering with your DNS queries.")
+ return ret
+ elif not name_match and not serial_match:
+ log.msg("Your DNS queries are definitely being tampered with.")
+ return ret
+
+ def get_random_url_safe_string(self, length):
+ """
+ Returns a random url-safe string of specified length, where
+ 0 < length <= 256. The returned string will always start with
+ an alphabetic character.
+ """
+ if (length <= 0):
+ length = 1
+ elif (length > 256):
+ length = 256
+
+ random_ascii = base64.urlsafe_b64encode(os.urandom(int(length)))
+
+ while not random_ascii[:1].isalpha():
+ random_ascii = base64.urlsafe_b64encode(os.urandom(int(length)))
+
+ three_quarters = int((len(random_ascii)) * (3.0/4.0))
+ random_string = random_ascii[:three_quarters]
+ return random_string
+
+ def get_random_hostname(self, length=None):
+ """
+ Returns a random hostname with SLD of specified length. If
+ length is unspecified, length=32 is used.
+
+ These *should* all resolve to NXDOMAIN. If they actually
+ resolve to a box that isn't part of a captive portal that
+ would be rather interesting.
+ """
+ if length is None:
+ length = 32
+
+ random_sld = self.get_random_url_safe_string(length)
+
+ # if it doesn't start with a letter, chuck it.
+ while not random_sld[:1].isalpha():
+ random_sld = self.get_random_url_safe_string(length)
+
+ tld_list = ['.com', '.net', '.org', '.info', '.test', '.invalid']
+ random_tld = urllib2.random.choice(tld_list)
+ random_hostname = random_sld + random_tld
+ return random_hostname
+
+ def compare_random_hostnames(self, hostname_count=None, hostname_length=None):
+ """
+ Get hostname_count number of random hostnames with SLD length
+ of hostname_length, and then attempt DNS resolution. If no
+ arguments are given, default to three hostnames of 32 bytes
+ each. These random hostnames *should* resolve to NXDOMAIN,
+ except in the case where a user is presented with a captive
+ portal and remains unauthenticated, in which case the captive
+ portal may return the address of the authentication page.
+
+ If the cardinality of the intersection of the set of resolved
+ random hostnames and the single element control set
+ (['NXDOMAIN']) are equal to one, then DNS properly resolved.
+
+ Returns true if only NXDOMAINs were returned, otherwise returns
+ False with the relative complement of the control set in the
+ response set.
+ """
+ if hostname_count is None:
+ hostname_count = 3
+
+ log.msg("Generating random hostnames...")
+ log.msg("Resolving DNS for %d random hostnames..." % hostname_count)
+
+ control = ['NXDOMAIN']
+ responses = []
+
+ for x in range(hostname_count):
+ random_hostname = self.get_random_hostname(hostname_length)
+ response_match, response_address = self.dns_resolve_match(random_hostname,
+ control[0])
+ for address in response_address:
+ if response_match is False:
+ log.msg("Strangely, DNS resolution of the random hostname")
+ log.msg("%s actually points to %s"
+ % (random_hostname, response_address))
+ responses = responses + [address]
+ else:
+ responses = responses + [address]
+
+ intersection = set(responses) & set(control)
+ relative_complement = set(responses) - set(control)
+ r = set(responses)
+
+ if len(intersection) == 1:
+ log.msg("All %d random hostnames properly resolved to NXDOMAIN."
+ % hostname_count)
+ return True, relative_complement
+ elif (len(intersection) == 1) and (len(r) > 1):
+ log.msg("Something odd happened. Some random hostnames correctly")
+ log.msg("resolved to NXDOMAIN, but several others resolved to")
+ log.msg("to the following addresses: %s" % relative_complement)
+ return False, relative_complement
+ elif (len(intersection) == 0) and (len(r) == 1):
+ log.msg("All random hostnames resolved to the IP address ")
+ log.msg("'%s', which is indicative of a captive portal." % r)
+ return False, relative_complement
+ else:
+ log.debug("Apparently, pigs are flying on your network, 'cause a")
+ log.debug("bunch of hostnames made from 32-byte random strings")
+ log.debug("just magically resolved to a bunch of random addresses.")
+ log.debug("That is definitely highly improbable. In fact, my napkin")
+ log.debug("tells me that the probability of just one of those")
+ log.debug("hostnames resolving to an address is 1.68e-59, making")
+ log.debug("it nearly twice as unlikely as an MD5 hash collision.")
+ log.debug("Either someone is seriously messing with your network,")
+ log.debug("or else you are witnessing the impossible. %s" % r)
+ return False, relative_complement
+
+ def google_dns_cp_test(self):
+ """
+ Google Chrome resolves three 10-byte random hostnames.
+ """
+ subtest = "Google Chrome DNS-based"
+
+ log.msg("")
+ log.msg("Running the Google Chrome DNS-based captive portal test...")
+
+ gmatch, google_dns_result = self.compare_random_hostnames(3, 10)
+
+ if gmatch:
+ log.msg("Google Chrome DNS-based captive portal test did not")
+ log.msg("detect a captive portal.")
+ return google_dns_result
+ else:
+ log.msg("Google Chrome DNS-based captive portal test believes")
+ log.msg("you are in a captive portal, or else something very")
+ log.msg("odd is happening with your DNS.")
+ return google_dns_result
+
+ def ms_dns_cp_test(self):
+ """
+ Microsoft "phones home" to a server which will always resolve
+ to the same address.
+ """
+ subtest = "Microsoft NCSI DNS-based"
+
+ log.msg("")
+ log.msg("Running the Microsoft NCSI DNS-based captive portal")
+ log.msg("test...")
+
+ msmatch, ms_dns_result = self.dns_resolve_match("dns.msftncsi.com",
+ "131.107.255.255")
+ if msmatch:
+ log.msg("Microsoft NCSI DNS-based captive portal test did not")
+ log.msg("detect a captive portal.")
+ return ms_dns_result
+ else:
+ log.msg("Microsoft NCSI DNS-based captive portal test ")
+ log.msg("believes you are in a captive portal.")
+ return ms_dns_result
+
+ def run_vendor_dns_tests(self):
+ """
+ Run the vendor DNS tests.
+ """
+ report = {}
+ report['google_dns_cp'] = self.google_dns_cp_test()
+ report['ms_dns_cp'] = self.ms_dns_cp_test()
+
+ return report
+
+ def run_vendor_tests(self, *a, **kw):
+ """
+ These are several vendor tests used to detect the presence of
+ a captive portal. Each test compares HTTP status code and
+ content to the control results and has its own User-Agent
+ string, in order to emulate the test as it would occur on the
+ device it was intended for. Vendor tests are defined in the
+ format:
+ [exp_url, ctrl_result, ctrl_code, ua, test_name]
+ """
+
+ vendor_tests = [['http://www.apple.com/library/test/success.html',
+ 'Success',
+ '200',
+ 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3',
+ 'Apple HTTP Captive Portal'],
+ ['http://tools.ietf.org/html/draft-nottingham-http-portal-02',
+ '428 Network Authentication Required',
+ '428',
+ 'Mozilla/5.0 (Windows NT 6.1; rv:5.0) Gecko/20100101 Firefox/5.0',
+ 'W3 Captive Portal'],
+ ['http://www.msftncsi.com/ncsi.txt',
+ 'Microsoft NCSI',
+ '200',
+ 'Microsoft NCSI',
+ 'MS HTTP Captive Portal',]]
+
+ cm = self.http_content_match_fuzzy_opt
+ sm = self.http_status_code_match
+ snm = self.http_status_code_no_match
+
+ def compare_content(status_func, fuzzy, experiment_url, control_result,
+ control_code, headers, test_name):
+ log.msg("")
+ log.msg("Running the %s test..." % test_name)
+
+ content_match, experiment_code, experiment_headers = cm(experiment_url,
+ control_result,
+ headers, fuzzy)
+ status_match = status_func(experiment_code, control_code)
+
+ if status_match and content_match:
+ log.msg("The %s test was unable to detect" % test_name)
+ log.msg("a captive portal.")
+ return True
+ else:
+ log.msg("The %s test shows that your network" % test_name)
+ log.msg("is filtered.")
+ return False
+
+ result = []
+ for vt in vendor_tests:
+ report = {}
+ report['vt'] = vt
+
+ experiment_url = vt[0]
+ control_result = vt[1]
+ control_code = vt[2]
+ headers = {'User-Agent': vt[3]}
+ test_name = vt[4]
+
+ args = (experiment_url, control_result, control_code, headers, test_name)
+
+ if test_name == "MS HTTP Captive Portal":
+ report['result'] = compare_content(sm, False, *args)
+
+ elif test_name == "Apple HTTP Captive Portal":
+ report['result'] = compare_content(sm, True, *args)
+
+ elif test_name == "W3 Captive Portal":
+ report['result'] = compare_content(snm, True, *args)
+
+ else:
+ log.warn("Ooni is trying to run an undefined CP vendor test.")
+ result.append(report)
+ return result
+
+ def control(self, experiment_result, args):
+ """
+ Compares the content and status code of the HTTP response for
+ experiment_url with the control_result and control_code
+ respectively. If the status codes match, but the experimental
+ content and control_result do not match, fuzzy matching is enabled
+ to determine if the control_result is at least included somewhere
+ in the experimental content. Returns True if matches are found,
+ and False if otherwise.
+ """
+ experiment_url = self.local_options['experiment-url']
+ control_result = 'XX'
+ control_code = 200
+ ua = self.local_options['user-agent']
+
+ cm = self.http_content_match_fuzzy_opt
+ sm = self.http_status_code_match
+ snm = self.http_status_code_no_match
+
+ log.msg("Running test for '%s'..." % experiment_url)
+ content_match, experiment_code, experiment_headers = cm(experiment_url,
+ control_result)
+ status_match = sm(experiment_code, control_code)
+ if status_match and content_match:
+ log.msg("The test for '%s'" % experiment_url)
+ log.msg("was unable to detect a captive portal.")
+ return experiment_result, True
+
+ elif status_match and not content_match:
+ log.msg("Retrying '%s' with fuzzy match enabled."
+ % experiment_url)
+ fuzzy_match, experiment_code, experiment_headers = cm(experiment_url,
+ control_result,
+ fuzzy=True)
+ if fuzzy_match:
+ return experiment_result, True
+ else:
+ log.msg("Found modified content on '%s'," % experiment_url)
+ log.msg("which could indicate a captive portal.")
+
+ return experiment_result, False
+ else:
+ log.msg("The content comparison test for ")
+ log.msg("'%s'" % experiment_url)
+ log.msg("shows that your HTTP traffic is filtered.")
+ return experiment_result, False
+
+ def experiment(self, args):
+ """
+ Runs the CaptivePortal(Test).
+
+ CONFIG OPTIONS
+ --------------
+
+ If "do_captive_portal_vendor_tests" is set to "true", then vendor
+ specific captive portal HTTP-based tests will be run.
+
+ If "do_captive_portal_dns_tests" is set to "true", then vendor
+ specific captive portal DNS-based tests will be run.
+
+ If "check_dns_requests" is set to "true", then Ooni-probe will
+ attempt to check that your DNS requests are not being tampered with
+ by a captive portal.
+
+ If "captive_portal" = "yourfilename.txt", then user-specified tests
+ will be run.
+
+ Any combination of the above tests can be run.
+ """
+ report = {}
+
+ log.msg("")
+ log.msg("Running vendor tests...")
+ report['vendor_tests'] = self.run_vendor_tests()
+
+ log.msg("")
+ log.msg("Running vendor DNS-based tests...")
+ report['vendor_dns_tests'] = self.run_vendor_dns_tests()
+
+ log.msg("")
+ log.msg("Checking that DNS requests are not being tampered...")
+ report['check0x20'] = self.check_0x20_to_auth_ns('ooni.nu')
+
+ log.msg("")
+ log.msg("Captive portal test finished!")
+ return report
+
+cp = CaptivePortal(None, None, None)
diff --git a/ooni/plugins/captiveportal.py b/ooni/plugins/captiveportal.py
deleted file mode 100644
index 4bf99b3..0000000
--- a/ooni/plugins/captiveportal.py
+++ /dev/null
@@ -1,629 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- captiveportal
- *************
-
- This test is a collection of tests to detect the presence of a
- captive portal. Code is taken, in part, from the old ooni-probe,
- which was written by Jacob Appelbaum and Arturo Filastò.
-
- :copyright: (c) 2012 Isis Lovecruft
- :license: see LICENSE for more details
-"""
-import base64
-import os
-import random
-import re
-import string
-import urllib2
-from urlparse import urlparse
-
-from zope.interface import implements
-from twisted.python import usage
-from twisted.plugin import IPlugin
-
-from ooni.plugoo.assets import Asset
-from ooni.plugoo.tests import ITest, OONITest
-from ooni.protocols import http
-from ooni.utils import log
-
-try:
- from dns import resolver
-except ImportError:
- print "The dnspython module was not found. https://crate.io/packages/dnspython/"
- resolver = None
-
-__plugoo__ = "captiveportal"
-__desc__ = "Captive portal detection test"
-
-class CaptivePortalArgs(usage.Options):
- optParameters = [['asset', 'a', None, 'Asset file'],
- ['resume', 'r', 0, 'Resume at this index'],
- ['experiment-url', 'e', 'http://google.com/', 'Experiment URL'],
- ['user-agent', 'u', random.choice(http.useragents),
- 'User agent for HTTP requests']
- ]
-
-class CaptivePortal(OONITest):
- """
- Compares content and status codes of HTTP responses, and attempts
- to determine if content has been altered.
- """
-
- implements(IPlugin, ITest)
-
- shortName = "captivep"
- description = "Captive Portal Test"
- requirements = None
- options = CaptivePortalArgs
- # Tells this to be blocking.
- blocking = True
-
- def http_fetch(self, url, headers={}):
- """
- Parses an HTTP url, fetches it, and returns a urllib2 response
- object.
- """
- url = urlparse(url).geturl()
- request = urllib2.Request(url, None, headers)
- response = urllib2.urlopen(request)
- response_headers = dict(response.headers)
- return response, response_headers
-
- def http_content_match_fuzzy_opt(self, experimental_url, control_result,
- headers=None, fuzzy=False):
- """
- Makes an HTTP request on port 80 for experimental_url, then
- compares the response_content of experimental_url with the
- control_result. Optionally, if the fuzzy parameter is set to
- True, the response_content is compared with a regex of the
- control_result. If the response_content from the
- experimental_url and the control_result match, returns True
- with the HTTP status code and headers; False, status code, and
- headers if otherwise.
- """
-
- if headers is None:
- default_ua = self.local_options['user-agent']
- headers = {'User-Agent': default_ua}
-
- response, response_headers = self.http_fetch(experimental_url, headers)
- response_content = response.read()
- response_code = response.code
- if response_content is None:
- log.warn("HTTP connection appears to have failed.")
- return False, False, False
-
- if fuzzy:
- pattern = re.compile(control_result)
- match = pattern.search(response_content)
- log.msg("Fuzzy HTTP content comparison for experiment URL")
- log.msg("'%s'" % experimental_url)
- if not match:
- log.msg("does not match!")
- return False, response_code, response_headers
- else:
- log.msg("and the expected control result yielded a match.")
- return True, response_code, response_headers
- else:
- if str(response_content) != str(control_result):
- log.msg("HTTP content comparison of experiment URL")
- log.msg("'%s'" % experimental_url)
- log.msg("and the expected control result do not match.")
- return False, response_code, response_headers
- else:
- return True, response_code, response_headers
-
- def http_status_code_match(self, experiment_code, control_code):
- """
- Compare two HTTP status codes, returns True if they match.
- """
- return int(experiment_code) == int(control_code)
-
- def http_status_code_no_match(self, experiment_code, control_code):
- """
- Compare two HTTP status codes, returns True if they do not match.
- """
- return int(experiment_code) != int(control_code)
-
- def dns_resolve(self, hostname, nameserver=None):
- """
- Resolves hostname(s) though nameserver to corresponding
- address(es). hostname may be either a single hostname string,
- or a list of strings. If nameserver is not given, use local
- DNS resolver, and if that fails try using 8.8.8.8.
- """
- if not resolver:
- log.msg("dnspython is not installed.\
- Cannot perform DNS Resolve test")
- return []
- if isinstance(hostname, str):
- hostname = [hostname]
-
- if nameserver is not None:
- res = resolver.Resolver(configure=False)
- res.nameservers = [nameserver]
- else:
- res = resolver.Resolver()
-
- response = []
- answer = None
-
- for hn in hostname:
- try:
- answer = res.query(hn)
- except resolver.NoNameservers:
- res.nameservers = ['8.8.8.8']
- try:
- answer = res.query(hn)
- except resolver.NXDOMAIN:
- log.msg("DNS resolution for %s returned NXDOMAIN" % hn)
- response.append('NXDOMAIN')
- except resolver.NXDOMAIN:
- log.msg("DNS resolution for %s returned NXDOMAIN" % hn)
- response.append('NXDOMAIN')
- finally:
- if not answer:
- return response
- for addr in answer:
- response.append(addr.address)
- return response
-
- def dns_resolve_match(self, experiment_hostname, control_address):
- """
- Resolve experiment_hostname, and check to see that it returns
- an experiment_address which matches the control_address. If
- they match, returns True and experiment_address; otherwise
- returns False and experiment_address.
- """
- experiment_address = self.dns_resolve(experiment_hostname)
- if not experiment_address:
- log.debug("dns_resolve() for %s failed" % experiment_hostname)
- return None, experiment_address
-
- if len(set(experiment_address) & set([control_address])) > 0:
- return True, experiment_address
- else:
- log.msg("DNS comparison of control '%s' does not" % control_address)
- log.msg("match experiment response '%s'" % experiment_address)
- return False, experiment_address
-
- def get_auth_nameservers(self, hostname):
- """
- Many CPs set a nameserver to be used. Let's query that
- nameserver for the authoritative nameservers of hostname.
-
- The equivalent of:
- $ dig +short NS ooni.nu
- """
- if not resolver:
- log.msg("dnspython not installed.")
- log.msg("Cannot perform test.")
- return []
-
- res = resolver.Resolver()
- answer = res.query(hostname, 'NS')
- auth_nameservers = []
- for auth in answer:
- auth_nameservers.append(auth.to_text())
- return auth_nameservers
-
- def hostname_to_0x20(self, hostname):
- """
- MaKEs yOur HOsTnaME lOoK LiKE THis.
-
- For more information, see:
- D. Dagon, et. al. "Increased DNS Forgery Resistance
- Through 0x20-Bit Encoding". Proc. CSS, 2008.
- """
- hostname_0x20 = ''
- for char in hostname:
- l33t = random.choice(['caps', 'nocaps'])
- if l33t == 'caps':
- hostname_0x20 += char.capitalize()
- else:
- hostname_0x20 += char.lower()
- return hostname_0x20
-
- def check_0x20_to_auth_ns(self, hostname, sample_size=None):
- """
- Resolve a 0x20 DNS request for hostname over hostname's
- authoritative nameserver(s), and check to make sure that
- the capitalization in the 0x20 request matches that of the
- response. Also, check the serial numbers of the SOA (Start
- of Authority) records on the authoritative nameservers to
- make sure that they match.
-
- If sample_size is given, a random sample equal to that number
- of authoritative nameservers will be queried; default is 5.
- """
- log.msg("")
- log.msg("Testing random capitalization of DNS queries...")
- log.msg("Testing that Start of Authority serial numbers match...")
-
- auth_nameservers = self.get_auth_nameservers(hostname)
-
- if sample_size is None:
- sample_size = 5
- resolved_auth_ns = random.sample(self.dns_resolve(auth_nameservers),
- sample_size)
-
- querynames = []
- answernames = []
- serials = []
-
- # Even when gevent monkey patching is on, the requests here
- # are sent without being 0x20'd, so we need to 0x20 them.
- hostname = self.hostname_to_0x20(hostname)
-
- for auth_ns in resolved_auth_ns:
- res = resolver.Resolver(configure=False)
- res.nameservers = [auth_ns]
- try:
- answer = res.query(hostname, 'SOA')
- except resolver.Timeout:
- continue
- querynames.append(answer.qname.to_text())
- answernames.append(answer.rrset.name.to_text())
- for soa in answer:
- serials.append(str(soa.serial))
-
- if len(set(querynames).intersection(answernames)) == 1:
- log.msg("Capitalization in DNS queries and responses match.")
- name_match = True
- else:
- log.msg("The random capitalization '%s' used in" % hostname)
- log.msg("DNS queries to that hostname's authoritative")
- log.msg("nameservers does not match the capitalization in")
- log.msg("the response.")
- name_match = False
-
- if len(set(serials)) == 1:
- log.msg("Start of Authority serial numbers all match.")
- serial_match = True
- else:
- log.msg("Some SOA serial numbers did not match the rest!")
- serial_match = False
-
- ret = name_match, serial_match, querynames, answernames, serials
-
- if name_match and serial_match:
- log.msg("Your DNS queries do not appear to be tampered.")
- return ret
- elif name_match or serial_match:
- log.msg("Something is tampering with your DNS queries.")
- return ret
- elif not name_match and not serial_match:
- log.msg("Your DNS queries are definitely being tampered with.")
- return ret
-
- def get_random_url_safe_string(self, length):
- """
- Returns a random url-safe string of specified length, where
- 0 < length <= 256. The returned string will always start with
- an alphabetic character.
- """
- if (length <= 0):
- length = 1
- elif (length > 256):
- length = 256
-
- random_ascii = base64.urlsafe_b64encode(os.urandom(int(length)))
-
- while not random_ascii[:1].isalpha():
- random_ascii = base64.urlsafe_b64encode(os.urandom(int(length)))
-
- three_quarters = int((len(random_ascii)) * (3.0/4.0))
- random_string = random_ascii[:three_quarters]
- return random_string
-
- def get_random_hostname(self, length=None):
- """
- Returns a random hostname with SLD of specified length. If
- length is unspecified, length=32 is used.
-
- These *should* all resolve to NXDOMAIN. If they actually
- resolve to a box that isn't part of a captive portal that
- would be rather interesting.
- """
- if length is None:
- length = 32
-
- random_sld = self.get_random_url_safe_string(length)
-
- # if it doesn't start with a letter, chuck it.
- while not random_sld[:1].isalpha():
- random_sld = self.get_random_url_safe_string(length)
-
- tld_list = ['.com', '.net', '.org', '.info', '.test', '.invalid']
- random_tld = urllib2.random.choice(tld_list)
- random_hostname = random_sld + random_tld
- return random_hostname
-
- def compare_random_hostnames(self, hostname_count=None, hostname_length=None):
- """
- Get hostname_count number of random hostnames with SLD length
- of hostname_length, and then attempt DNS resolution. If no
- arguments are given, default to three hostnames of 32 bytes
- each. These random hostnames *should* resolve to NXDOMAIN,
- except in the case where a user is presented with a captive
- portal and remains unauthenticated, in which case the captive
- portal may return the address of the authentication page.
-
- If the cardinality of the intersection of the set of resolved
- random hostnames and the single element control set
- (['NXDOMAIN']) are equal to one, then DNS properly resolved.
-
- Returns true if only NXDOMAINs were returned, otherwise returns
- False with the relative complement of the control set in the
- response set.
- """
- if hostname_count is None:
- hostname_count = 3
-
- log.msg("Generating random hostnames...")
- log.msg("Resolving DNS for %d random hostnames..." % hostname_count)
-
- control = ['NXDOMAIN']
- responses = []
-
- for x in range(hostname_count):
- random_hostname = self.get_random_hostname(hostname_length)
- response_match, response_address = self.dns_resolve_match(random_hostname,
- control[0])
- for address in response_address:
- if response_match is False:
- log.msg("Strangely, DNS resolution of the random hostname")
- log.msg("%s actually points to %s"
- % (random_hostname, response_address))
- responses = responses + [address]
- else:
- responses = responses + [address]
-
- intersection = set(responses) & set(control)
- relative_complement = set(responses) - set(control)
- r = set(responses)
-
- if len(intersection) == 1:
- log.msg("All %d random hostnames properly resolved to NXDOMAIN."
- % hostname_count)
- return True, relative_complement
- elif (len(intersection) == 1) and (len(r) > 1):
- log.msg("Something odd happened. Some random hostnames correctly")
- log.msg("resolved to NXDOMAIN, but several others resolved to")
- log.msg("to the following addresses: %s" % relative_complement)
- return False, relative_complement
- elif (len(intersection) == 0) and (len(r) == 1):
- log.msg("All random hostnames resolved to the IP address ")
- log.msg("'%s', which is indicative of a captive portal." % r)
- return False, relative_complement
- else:
- log.debug("Apparently, pigs are flying on your network, 'cause a")
- log.debug("bunch of hostnames made from 32-byte random strings")
- log.debug("just magically resolved to a bunch of random addresses.")
- log.debug("That is definitely highly improbable. In fact, my napkin")
- log.debug("tells me that the probability of just one of those")
- log.debug("hostnames resolving to an address is 1.68e-59, making")
- log.debug("it nearly twice as unlikely as an MD5 hash collision.")
- log.debug("Either someone is seriously messing with your network,")
- log.debug("or else you are witnessing the impossible. %s" % r)
- return False, relative_complement
-
- def google_dns_cp_test(self):
- """
- Google Chrome resolves three 10-byte random hostnames.
- """
- subtest = "Google Chrome DNS-based"
-
- log.msg("")
- log.msg("Running the Google Chrome DNS-based captive portal test...")
-
- gmatch, google_dns_result = self.compare_random_hostnames(3, 10)
-
- if gmatch:
- log.msg("Google Chrome DNS-based captive portal test did not")
- log.msg("detect a captive portal.")
- return google_dns_result
- else:
- log.msg("Google Chrome DNS-based captive portal test believes")
- log.msg("you are in a captive portal, or else something very")
- log.msg("odd is happening with your DNS.")
- return google_dns_result
-
- def ms_dns_cp_test(self):
- """
- Microsoft "phones home" to a server which will always resolve
- to the same address.
- """
- subtest = "Microsoft NCSI DNS-based"
-
- log.msg("")
- log.msg("Running the Microsoft NCSI DNS-based captive portal")
- log.msg("test...")
-
- msmatch, ms_dns_result = self.dns_resolve_match("dns.msftncsi.com",
- "131.107.255.255")
- if msmatch:
- log.msg("Microsoft NCSI DNS-based captive portal test did not")
- log.msg("detect a captive portal.")
- return ms_dns_result
- else:
- log.msg("Microsoft NCSI DNS-based captive portal test ")
- log.msg("believes you are in a captive portal.")
- return ms_dns_result
-
- def run_vendor_dns_tests(self):
- """
- Run the vendor DNS tests.
- """
- report = {}
- report['google_dns_cp'] = self.google_dns_cp_test()
- report['ms_dns_cp'] = self.ms_dns_cp_test()
-
- return report
-
- def run_vendor_tests(self, *a, **kw):
- """
- These are several vendor tests used to detect the presence of
- a captive portal. Each test compares HTTP status code and
- content to the control results and has its own User-Agent
- string, in order to emulate the test as it would occur on the
- device it was intended for. Vendor tests are defined in the
- format:
- [exp_url, ctrl_result, ctrl_code, ua, test_name]
- """
-
- vendor_tests = [['http://www.apple.com/library/test/success.html',
- 'Success',
- '200',
- 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3',
- 'Apple HTTP Captive Portal'],
- ['http://tools.ietf.org/html/draft-nottingham-http-portal-02',
- '428 Network Authentication Required',
- '428',
- 'Mozilla/5.0 (Windows NT 6.1; rv:5.0) Gecko/20100101 Firefox/5.0',
- 'W3 Captive Portal'],
- ['http://www.msftncsi.com/ncsi.txt',
- 'Microsoft NCSI',
- '200',
- 'Microsoft NCSI',
- 'MS HTTP Captive Portal',]]
-
- cm = self.http_content_match_fuzzy_opt
- sm = self.http_status_code_match
- snm = self.http_status_code_no_match
-
- def compare_content(status_func, fuzzy, experiment_url, control_result,
- control_code, headers, test_name):
- log.msg("")
- log.msg("Running the %s test..." % test_name)
-
- content_match, experiment_code, experiment_headers = cm(experiment_url,
- control_result,
- headers, fuzzy)
- status_match = status_func(experiment_code, control_code)
-
- if status_match and content_match:
- log.msg("The %s test was unable to detect" % test_name)
- log.msg("a captive portal.")
- return True
- else:
- log.msg("The %s test shows that your network" % test_name)
- log.msg("is filtered.")
- return False
-
- result = []
- for vt in vendor_tests:
- report = {}
- report['vt'] = vt
-
- experiment_url = vt[0]
- control_result = vt[1]
- control_code = vt[2]
- headers = {'User-Agent': vt[3]}
- test_name = vt[4]
-
- args = (experiment_url, control_result, control_code, headers, test_name)
-
- if test_name == "MS HTTP Captive Portal":
- report['result'] = compare_content(sm, False, *args)
-
- elif test_name == "Apple HTTP Captive Portal":
- report['result'] = compare_content(sm, True, *args)
-
- elif test_name == "W3 Captive Portal":
- report['result'] = compare_content(snm, True, *args)
-
- else:
- log.warn("Ooni is trying to run an undefined CP vendor test.")
- result.append(report)
- return result
-
- def control(self, experiment_result, args):
- """
- Compares the content and status code of the HTTP response for
- experiment_url with the control_result and control_code
- respectively. If the status codes match, but the experimental
- content and control_result do not match, fuzzy matching is enabled
- to determine if the control_result is at least included somewhere
- in the experimental content. Returns True if matches are found,
- and False if otherwise.
- """
- experiment_url = self.local_options['experiment-url']
- control_result = 'XX'
- control_code = 200
- ua = self.local_options['user-agent']
-
- cm = self.http_content_match_fuzzy_opt
- sm = self.http_status_code_match
- snm = self.http_status_code_no_match
-
- log.msg("Running test for '%s'..." % experiment_url)
- content_match, experiment_code, experiment_headers = cm(experiment_url,
- control_result)
- status_match = sm(experiment_code, control_code)
- if status_match and content_match:
- log.msg("The test for '%s'" % experiment_url)
- log.msg("was unable to detect a captive portal.")
- return experiment_result, True
-
- elif status_match and not content_match:
- log.msg("Retrying '%s' with fuzzy match enabled."
- % experiment_url)
- fuzzy_match, experiment_code, experiment_headers = cm(experiment_url,
- control_result,
- fuzzy=True)
- if fuzzy_match:
- return experiment_result, True
- else:
- log.msg("Found modified content on '%s'," % experiment_url)
- log.msg("which could indicate a captive portal.")
-
- return experiment_result, False
- else:
- log.msg("The content comparison test for ")
- log.msg("'%s'" % experiment_url)
- log.msg("shows that your HTTP traffic is filtered.")
- return experiment_result, False
-
- def experiment(self, args):
- """
- Runs the CaptivePortal(Test).
-
- CONFIG OPTIONS
- --------------
-
- If "do_captive_portal_vendor_tests" is set to "true", then vendor
- specific captive portal HTTP-based tests will be run.
-
- If "do_captive_portal_dns_tests" is set to "true", then vendor
- specific captive portal DNS-based tests will be run.
-
- If "check_dns_requests" is set to "true", then Ooni-probe will
- attempt to check that your DNS requests are not being tampered with
- by a captive portal.
-
- If "captive_portal" = "yourfilename.txt", then user-specified tests
- will be run.
-
- Any combination of the above tests can be run.
- """
- report = {}
-
- log.msg("")
- log.msg("Running vendor tests...")
- report['vendor_tests'] = self.run_vendor_tests()
-
- log.msg("")
- log.msg("Running vendor DNS-based tests...")
- report['vendor_dns_tests'] = self.run_vendor_dns_tests()
-
- log.msg("")
- log.msg("Checking that DNS requests are not being tampered...")
- report['check0x20'] = self.check_0x20_to_auth_ns('ooni.nu')
-
- log.msg("")
- log.msg("Captive portal test finished!")
- return report
-
-cp = CaptivePortal(None, None, None)
diff --git a/ooni/runner.py b/ooni/runner.py
index 0738bf8..a4c88a9 100644
--- a/ooni/runner.py
+++ b/ooni/runner.py
@@ -69,6 +69,7 @@ def adaptLegacyTest(obj, config):
test_class.local_options = subOptions
assets = test_class.load_assets()
+ inputs = [None]
# XXX here we are only taking assets that are set to one item only.
for key, inputs in assets.items():
pass
1
0

r25839: {website} someone noticed andrew used to sign rpms too, list out the f (in website/trunk: about/en docs/en)
by Andrew Lewman 06 Oct '12
by Andrew Lewman 06 Oct '12
06 Oct '12
Author: phobos
Date: 2012-10-06 01:48:05 +0000 (Sat, 06 Oct 2012)
New Revision: 25839
Modified:
website/trunk/about/en/financials.wml
website/trunk/docs/en/signing-keys.wml
Log:
someone noticed andrew used to sign rpms too, list out the first two 2011 financial docs.
Modified: website/trunk/about/en/financials.wml
===================================================================
--- website/trunk/about/en/financials.wml 2012-10-05 21:54:42 UTC (rev 25838)
+++ website/trunk/about/en/financials.wml 2012-10-06 01:48:05 UTC (rev 25839)
@@ -13,18 +13,33 @@
<div id="maincol">
<h1>Tor: Financial Reports</h1>
<dl>
+ <dt>Fiscal Year 2011</dt>
+ <dd><a href="findoc/2011-TorProject-Amended-Final-Report.pdf">2011
+ Financial Statements and Audit Report</a></dd>
+ <dd><a href="findoc/2011-TorProject-DCF.pdf">2011 Dept of Commerce
+ Data Collection Form</a></dd>
<dt>Fiscal Year 2010</dt>
- <dd><a href="findoc/2010-TorProject-combined-Form990_PC_Audit_Results.pdf">2010 IRS Form 990, State of MA Form PC, and Independent Audit Results</a></dd>
+ <dd><a
+ href="findoc/2010-TorProject-combined-Form990_PC_Audit_Results.pdf">2010
+ IRS Form 990, State of MA Form PC, and Independent Audit
+ Results</a></dd>
<dt>Fiscal Year 2009</dt>
- <dd><a href="findoc/2009-TorProject-Annual-Report.pdf">2009 Annual Report</a></dd>
- <dd><a href="findoc/2009-TorProject-Form990andPC.pdf">2009 IRS Form 990 and State of MA Form PC</a></dd>
- <dd><a href="findoc/2009-TorProject-FinancialStatements.pdf">2009 Financial Statements and Audit results</a></dd>
- <dd><a href="findoc/2009-TorProject-DCF.pdf">2009 Dept of Commerce Data Collection Form</a></dd>
+ <dd><a href="findoc/2009-TorProject-Annual-Report.pdf">2009
+ Annual Report</a></dd>
+ <dd><a href="findoc/2009-TorProject-Form990andPC.pdf">2009 IRS
+ Form 990 and State of MA Form PC</a></dd>
+ <dd><a href="findoc/2009-TorProject-FinancialStatements.pdf">2009
+ Financial Statements and Audit results</a></dd>
+ <dd><a href="findoc/2009-TorProject-DCF.pdf">2009 Dept of Commerce
+ Data Collection Form</a></dd>
<dt>Fiscal Year 2008</dt>
- <dd><a href="findoc/2008-TorProject-Form990.pdf">2008 IRS Form 990</a></dd>
- <dd><a href="findoc/2008-TorProject-FinancialStatements.pdf">2008 Financial Statements and Audit Results</a></dd>
+ <dd><a href="findoc/2008-TorProject-Form990.pdf">2008 IRS Form
+ 990</a></dd>
+ <dd><a href="findoc/2008-TorProject-FinancialStatements.pdf">2008
+ Financial Statements and Audit Results</a></dd>
<dt>Fiscal Year 2007</dt>
- <dd><a href="findoc/2007-TorProject-Form990.pdf">2007 IRS Form 990</a></dd>
+ <dd><a href="findoc/2007-TorProject-Form990.pdf">2007 IRS Form
+ 990</a></dd>
</dl>
</div>
<!-- END MAINCOL -->
Modified: website/trunk/docs/en/signing-keys.wml
===================================================================
--- website/trunk/docs/en/signing-keys.wml 2012-10-05 21:54:42 UTC (rev 25838)
+++ website/trunk/docs/en/signing-keys.wml 2012-10-06 01:48:05 UTC (rev 25839)
@@ -19,8 +19,8 @@
tarballs.</li>
<li>Erinn Clark (0x63FEE659) signs the Tor Browser Bundles, Vidalia
bundles, and many other packages. She signs RPMs with her other key
- (0xF1F5C9B5). Andrew Lewman (0x31B0974B, 0x6B4D6475) used to sign packages for
- Windows and OS X.</li>
+ (0xF1F5C9B5). Andrew Lewman (0x31B0974B, 0x6B4D6475) used to sign
+ packages for RPMs, Windows, and OS X.</li>
<li>Tor Project Archive (0x886DDD89) signs the deb.torproject.org
repositories and archives.</li>
<li>Tomás Touceda (0x9A753A6B) signs current Vidalia tarballs. Matt
@@ -79,9 +79,7 @@
pub 4096R/6B4D6475 2012-02-29
Key fingerprint = 0291 ECCB E42B 2206 8E68 5545 627D EE28 6B4D 6475
- uid Andrew Lewman <andrew(a)lewman.is>
uid Andrew Lewman <andrew(a)torproject.org>
- uid Andrew Lewman <andrew(a)lewman.com>
uid Andrew Lewman <andrew(a)torproject.is>
sub 4096R/BE713AC6 2012-02-29
1
0