tor-commits
Threads by month
- ----- 2025 -----
- November
- October
- September
- August
- July
- June
- 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
August 2019
- 19 participants
- 2738 discussions
[translation/tails-misc] Update translations for tails-misc
by translation@torproject.org 16 Aug '19
by translation@torproject.org 16 Aug '19
16 Aug '19
commit 65bb4c037a98ba38302216a85e21d7f4bac5f62e
Author: Translation commit bot <translation(a)torproject.org>
Date: Fri Aug 16 01:46:47 2019 +0000
Update translations for tails-misc
---
ka.po | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/ka.po b/ka.po
index be6d039f9..ce1b8ac5e 100644
--- a/ka.po
+++ b/ka.po
@@ -11,7 +11,7 @@ msgstr ""
"Project-Id-Version: Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-12 11:57+0200\n"
-"PO-Revision-Date: 2019-08-16 01:16+0000\n"
+"PO-Revision-Date: 2019-08-16 01:20+0000\n"
"Last-Translator: Georgianization\n"
"Language-Team: Georgian (http://www.transifex.com/otf/torproject/language/ka/)\n"
"MIME-Version: 1.0\n"
@@ -109,7 +109,7 @@ msgid ""
"<i>${filename}</i>\n"
"\n"
"Renaming it to <i>keepassx.kdbx</i> would allow <i>KeePassXC</i> to open it automatically in the future."
-msgstr ""
+msgstr "<b><big>გსურთ სახელი გადაარქვათ თქვენი <i>KeePassXC</i>-ის მონაცემთა ბაზას?</big></b>\n\nთქვენი <i>KeePassXC</i>-ის მონაცემთა ბაზა მდებარეობს<i>მუდმივ</i> მეხსიერებაზე საქაღალდეში:\n\n<i>${filename}</i>\n\n<i>keepassx.kdbx</i> სახელის დარქმევით, <i>KeePassXC</i>-ს მისი ავტომატურად გახსნის საშუალება მიეცემა მომავალში."
#: config/chroot_local-includes/usr/local/bin/keepassxc:23
msgid "Rename"
@@ -593,7 +593,7 @@ msgstr "ვერ მოხერხდა ჩაკეტვა დანაყ
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:338
msgid "Locking the volume failed"
-msgstr ""
+msgstr "დანაყოფის ჩაკეტვა ვერ მოხერხდა"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py:83
msgid "No file containers added"
@@ -731,11 +731,11 @@ msgstr "Tails-ის დამხმარე მასალები"
#: ../config/chroot_local-includes/usr/share/applications/root-terminal.desktop.in.h:1
msgid "Root Terminal"
-msgstr ""
+msgstr "ტერმინალი ძირეული უფლებებით"
#: ../config/chroot_local-includes/usr/share/applications/root-terminal.desktop.in.h:2
msgid "Opens a terminal as the root user, using gksu to ask for the password"
-msgstr ""
+msgstr "გახსნის ტერმინალს ძირეული მომხმარებლით, პაროლის მოთხოვნით gksu-ს მეშვეობით"
#: ../config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in.h:2
msgid "Learn how to use Tails"
1
0
[translation/tails-misc] Update translations for tails-misc
by translation@torproject.org 16 Aug '19
by translation@torproject.org 16 Aug '19
16 Aug '19
commit d217814eb3267b2c8c8b2558f1c9c844b0e7bd52
Author: Translation commit bot <translation(a)torproject.org>
Date: Fri Aug 16 01:16:45 2019 +0000
Update translations for tails-misc
---
ka.po | 304 +++++++++++++++++++++++++++++++++---------------------------------
1 file changed, 152 insertions(+), 152 deletions(-)
diff --git a/ka.po b/ka.po
index ee71aa5c6..be6d039f9 100644
--- a/ka.po
+++ b/ka.po
@@ -11,7 +11,7 @@ msgstr ""
"Project-Id-Version: Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-12 11:57+0200\n"
-"PO-Revision-Date: 2019-08-12 16:11+0000\n"
+"PO-Revision-Date: 2019-08-16 01:16+0000\n"
"Last-Translator: Georgianization\n"
"Language-Team: Georgian (http://www.transifex.com/otf/torproject/language/ka/)\n"
"MIME-Version: 1.0\n"
@@ -22,11 +22,11 @@ msgstr ""
#: config/chroot_local-includes/etc/NetworkManager/dispatcher.d/60-tor-ready.sh:39
msgid "Tor is ready"
-msgstr ""
+msgstr "Tor áááááá"
#: config/chroot_local-includes/etc/NetworkManager/dispatcher.d/60-tor-ready.sh:40
msgid "You can now access the Internet."
-msgstr ""
+msgstr "áá®áá á£ááá ášáááá«áááá áááááá§áááá ááá¢áá ááá¢á."
#: config/chroot_local-includes/etc/whisperback/config.py:69
#, python-format
@@ -43,24 +43,24 @@ msgid ""
"an opportunity for eavesdroppers, like your email or Internet provider, to\n"
"confirm that you are using Tails.\n"
"</p>\n"
-msgstr ""
+msgstr "<h1>áááááá®ááá áá á®áá ááááááá¡ ááŠááá€á®áá áášá!</h1>\n<p>á¬áááááá®áá<a href=\"%s\">á©áááá áááááááááá á®áá ááááááá¡ ááá®á¡ááááááá</a>.</p>\n<p><strong>ááá®á¡áááááá¡ áᣠááá£á áááá áááááᢠááá áá ááá€áá áááªááá¡!</strong></p>\n<h2>ááá¡á€áá¡á¢áá¡ ááá¬áááááá¡ ááááááá</h2>\n<p>\nááá€áá¡á¢áá¡ ááá¡áááá ááá¡ ááá¬áááááá, á¡áášá£ááááá ááááááªááá áááááááášáá ááá\ná®áá áááášá á£ááá ááá¡áá áááááá. áá¡ á¡áááá áá á£ááá¢áá¡ ášáááá®ááááášá á®áá ááááááá¡\nááá®á¡áááááá¡áá¡, á ááááá ááááá®ááá£á ááá
á¡ ášáá¡áá«áááááááá¡ ááá áášá, áááá á ááá®á¡ááááá\ná£áá áááá ááááá£á¡áááááá áá. áááá á áá®á áá, áá á¡ááááá¡ á¡áá€á áá®ááª, á áá ášáá¡áá«áá\náááá§á£á áááááááá¡, ááááááááá áá¥áááá ááá€áá¡á¢áá¡ áá ááá¢áá ááá¢áááá¡áá®á£á áááá¡\náááá¬áááááááá¡, ášááá«ááááá áááááá¡á¢á£á áá, á áá ááááááááá á¡áá ááááááá Tails-áá.\n</p>\n"
#: config/chroot_local-includes/usr/share/tails/additional-software/configuration-window.ui:8
#: ../config/chroot_local-includes/usr/share/applications/org.boum.tails.additional-software-config.desktop.in.h:1
msgid "Additional Software"
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá ááá"
#: config/chroot_local-includes/usr/share/tails/additional-software/configuration-window.ui:51
msgid ""
"You can install additional software automatically from your persistent "
"storage when starting Tails."
-msgstr ""
+msgstr "ááá¢áááá¢á£á áá ášáááá«áááá ááááá¢ááááá áá ááá áááá¡ ááá§ááááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªáááááá Tails-áá¡ ááášááááá¡áá¡."
#: config/chroot_local-includes/usr/share/tails/additional-software/configuration-window.ui:77
msgid ""
"The following software is installed automatically from your persistent "
"storage when starting Tails."
-msgstr ""
+msgstr "áááªááá£áá áá ááá ááá ááá¢áááá¢á£á áá ááá§áááááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªáááááá Tails-áá¡ ááášááááá¡áá¡."
#: config/chroot_local-includes/usr/share/tails/additional-software/configuration-window.ui:135
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:171
@@ -68,36 +68,36 @@ msgid ""
"To add more, install some software using <a "
"href=\"synaptic.desktop\">Synaptic Package Manager</a> or <a "
"href=\"org.gnome.Terminal.desktop\">APT on the command line</a>."
-msgstr ""
+msgstr "ááá¡áááá¢ááááá, áááá§áááá á áááááááá áá ááá ááá <a href=\"synaptic.desktop\">Synaptic-áá¡ ááááá¢áááá¡ áááá áááááá¡</a> á¡áášá£áááááá áá <a href=\"org.gnome.Terminal.desktop\">APT-áá á«áááááááá</a>."
#: config/chroot_local-includes/usr/share/tails/additional-software/configuration-window.ui:154
msgid "_Create persistent storage"
-msgstr ""
+msgstr "_áá£ááááá á¡ááªáááá¡ ášáá¥ááá"
#: config/chroot_local-includes/usr/local/bin/electrum:57
msgid "Persistence is disabled for Electrum"
-msgstr ""
+msgstr "áá£ááááá ááá®á¡ááá ááá ááááá áá£ááá Electrum-áá¡áááá¡"
#: config/chroot_local-includes/usr/local/bin/electrum:59
msgid ""
"When you reboot Tails, all of Electrum's data will be lost, including your "
"Bitcoin wallet. It is strongly recommended to only run Electrum when its "
"persistence feature is activated."
-msgstr ""
+msgstr "á áááá¡á᪠á®áááá®áá áááášáááá Tails, Electrum-áá¡ á§áááá áááááªááá áááááá áááá, ááá ášáá áá¡ áá¥áááá Bitcoin-á¡áá€á£áá. ááááá¯áááááá ááá á©ááá ááá£ášááá Electrum áá®áááá áá ášáááá®ááááášá, áᣠááá¡á áá£ááááá ááá®á¡ááá áááá¡ á¡ááªááá á©áá áá£ááá."
#: config/chroot_local-includes/usr/local/bin/electrum:60
msgid "Do you want to start Electrum anyway?"
-msgstr ""
+msgstr "áááá᪠áá¡á£á á, ááá£ášááá Electrum?"
#: config/chroot_local-includes/usr/local/bin/electrum:63
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:41
msgid "_Launch"
-msgstr ""
+msgstr "_ááášáááá"
#: config/chroot_local-includes/usr/local/bin/electrum:64
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:42
msgid "_Exit"
-msgstr ""
+msgstr "_ááá®á£á áá"
#: config/chroot_local-includes/usr/local/bin/keepassxc:15
#, sh-format
@@ -113,56 +113,56 @@ msgstr ""
#: config/chroot_local-includes/usr/local/bin/keepassxc:23
msgid "Rename"
-msgstr ""
+msgstr "ááááá á¥áááá"
#: config/chroot_local-includes/usr/local/bin/keepassxc:24
msgid "Keep current name"
-msgstr ""
+msgstr "áá á¡ááá£áá á¡áá®áááá¡ ááá¢ááááá"
#: config/chroot_local-includes/usr/local/bin/replace-su-with-sudo:21
msgid "su is disabled. Please use sudo instead."
-msgstr ""
+msgstr "su ááááášá£ááá. ááá®ááá, á¡ááááªáááá áááááá§áááá sudo."
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/status-menu-helper@tails.boum.org/extension.js:75
msgid "Lock screen"
-msgstr ""
+msgstr "á©ááááá¢á ááá ááá"
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/status-menu-helper@tails.boum.org/extension.js:79
msgid "Suspend"
-msgstr ""
+msgstr "áááááááá"
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/status-menu-helper@tails.boum.org/extension.js:83
msgid "Restart"
-msgstr ""
+msgstr "á®áááá®áá ááášáááá"
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/status-menu-helper@tails.boum.org/extension.js:87
msgid "Power Off"
-msgstr ""
+msgstr "ááááášáá"
#: config/chroot_local-includes/usr/local/bin/tails-about:22
#: ../config/chroot_local-includes/usr/share/desktop-directories/Tails.directory.in.h:1
msgid "Tails"
-msgstr ""
+msgstr "Tails"
#: config/chroot_local-includes/usr/local/bin/tails-about:25
#: ../config/chroot_local-includes/usr/share/applications/tails-about.desktop.in.h:1
msgid "About Tails"
-msgstr ""
+msgstr "Tails-áá¡ ášáá¡áá®áá"
#: config/chroot_local-includes/usr/local/bin/tails-about:35
msgid "The Amnesic Incognito Live System"
-msgstr ""
+msgstr "áááááááá¡ ááá€áá ááá¡ áá ááááá¬á§áááá¡ ááá ááááá á á¡áá¡á¢ááá"
#: config/chroot_local-includes/usr/local/bin/tails-about:36
#, python-format
msgid ""
"Build information:\n"
"%s"
-msgstr ""
+msgstr "áááá¬á§áááá¡ áááááªááááá:\n%s"
#: config/chroot_local-includes/usr/local/bin/tails-about:54
msgid "not available"
-msgstr ""
+msgstr "áá áá á®ááááá¡áá¬ááááá"
#. Translators: Don't translate {details}, it's a placeholder and will
#. be replaced.
@@ -171,32 +171,32 @@ msgstr ""
msgid ""
"{details} Please check your list of additional software or read the system "
"log to understand the problem."
-msgstr ""
+msgstr "{details} ááá®ááá, áááááááá¬ááá ááááá¢ááááá áá ááá áááááá¡ á¡áá áá ááá®áá á¡áá¡á¢áááá¡ ááŠá ááªá®á£áá á©áááá¬áá ááá á®áá ááááá¡ ááááááá¡ ááááá¡áá áááááá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:153
msgid ""
"Please check your list of additional software or read the system log to "
"understand the problem."
-msgstr ""
+msgstr "ááá®ááá áááááááá¬ááá ááááá¢ááááá áá ááá áááááá¡ á¡áá áá ááá®áá á¡áá¡á¢áááá¡ ááŠá ááªá®á£áá á©áááá¬áá ááá á®áá ááááá¡ ááááááá¡ ááááá¡áá áááááá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:157
msgid "Show Log"
-msgstr ""
+msgstr "á©áááá¬áá áááá¡ á©áááááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:157
msgid "Configure"
-msgstr ""
+msgstr "ááááá ááá"
#. Translators: Don't translate {beginning} or {last}, they are
#. placeholders and will be replaced.
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:223
#, python-brace-format
msgid "{beginning} and {last}"
-msgstr ""
+msgstr "{beginning} áá {last}"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:224
msgid ", "
-msgstr ""
+msgstr ", "
#. Translators: Don't translate {packages}, it's a placeholder and will
#. be replaced.
@@ -204,115 +204,115 @@ msgstr ""
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:320
#, python-brace-format
msgid "Add {packages} to your additional software?"
-msgstr ""
+msgstr "á©ááááá¢áá¡ {packages} áá¥áááá¡ ááááá¢áááá áá ááá áááášá?"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:292
msgid ""
"To install it automatically from your persistent storage when starting "
"Tails."
-msgstr ""
+msgstr "ááá¡á ááá¢áááá¢á£á áá ááá§ááááá áá£ááááá ááá®á¡ááá áááááá Tails-áá¡ ááášááááá¡áá¡."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:294
msgid "Install Every Time"
-msgstr ""
+msgstr "á§áááá á¯áá áá ááá§ááááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:295
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:326
msgid "Install Only Once"
-msgstr ""
+msgstr "áá®áááá áá áá®áá ááá§ááááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:301
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:331
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:372
msgid "The configuration of your additional software failed."
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá áááá¡ ááááá ááá ááá ááá®áá á®áá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:322
msgid ""
"To install it automatically when starting Tails, you can create a persistent"
" storage and activate the <b>Additional Software</b> feature."
-msgstr ""
+msgstr "ááá¢áááá¢á£á áá ááá¡áá§ááááááá Tails-áá¡ ááášááááá¡áá¡, ášáááá«áááá ášáá¥áááá áá£ááááá á¡ááªááá áá ááááá¥ááááá <b>ááááá¢ááááá áá ááá áááá¡</b> ášáá¡áá«ááááááá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:325
msgid "Create Persistent Storage"
-msgstr ""
+msgstr "áá£ááááá á¡ááªáááá¡ ášáá¥ááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:333
msgid "Creating your persistent storage failed."
-msgstr ""
+msgstr "áá£ááááá á¡ááªáááá¡ ášáá¥ááá ááá ááá®áá á®áá."
#. Translators: Don't translate {packages}, it's a placeholder and
#. will be replaced.
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:342
#, python-brace-format
msgid "You could install {packages} automatically when starting Tails"
-msgstr ""
+msgstr "ášáááá«áááá áááá§áááá {packages} ááá¢áááá¢á£á áá Tails-áá¡ ááášááááá¡áá¡"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:345
msgid ""
"To do so, you need to run Tails from a USB stick installed using <i>Tails "
"Installer</i>."
-msgstr ""
+msgstr "áááá¡áááá¡, á¡áááá áá Tails ááá£ášááá USB-ááá®á¡ááá áááááá <i>Tails-áá¡ ááá¡áá§ááááááá</i> áá ááá áááá."
#. Translators: Don't translate {packages}, it's a placeholder and will be
#. replaced.
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:360
#, python-brace-format
msgid "Remove {packages} from your additional software?"
-msgstr ""
+msgstr "á¬ááášáááá¡ {packages} áá¥áááá ááááá¢ááááá áá ááá áááááááá?"
#. Translators: Don't translate {packages}, it's a placeholder
#. and will be replaced.
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:364
#, python-brace-format
msgid "This will stop installing {packages} automatically."
-msgstr ""
+msgstr "ášáááááá, ááŠáá ááá§áááááá {packages} ááá¢áááá¢á£á áá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:366
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:154
msgid "Remove"
-msgstr ""
+msgstr "á¬áášáá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:367
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:119
#: config/chroot_local-includes/usr/local/bin/tor-browser:46
msgid "Cancel"
-msgstr ""
+msgstr "ááá£á¥áááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:545
msgid "Installing your additional software from persistent storage..."
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá áááá¡ áá£ááááá ááá®á¡ááá áááá¡ á¡ááªáááááá ááá§ááááá..."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:547
msgid "This can take several minutes."
-msgstr ""
+msgstr "ášááá«áááá ááá¡á¢áááá¡ á áááááááá á¬á£áá¡."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:560
msgid "The installation of your additional software failed"
-msgstr ""
+msgstr "áá¥áááá ááááá¢ááááá áá ááá áááá¡ ááá§ááááá ááá ááá®áá á®áá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:575
msgid "Additional software installed successfully"
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá ááá ááá§áááá á¬áá ááá¢áááá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:595
msgid "The check for upgrades of your additional software failed"
-msgstr ""
+msgstr "áá¥áááá ááááá¢ááááá áá ááá áááá¡ ááááá®ááááááá ášáááá¬áááá ááá ááá®áá á®áá"
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:597
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:605
msgid ""
"Please check your network connection, restart Tails, or read the system log "
"to understand the problem."
-msgstr ""
+msgstr "ááá®ááá, áááááááá¬ááá á¥á¡ááááá áááášáá á, á®áááá®áá ááá£ášááá Tails áá ááá®áá á¡áá¡á¢áááá¡ ááŠá ááªá®á£áá á©áááá¬áá ááá á®áá ááááá¡ ááááááááá¡ ááááá¡áá áááááá."
#: config/chroot_local-includes/usr/local/sbin/tails-additional-software:604
msgid "The upgrade of your additional software failed"
-msgstr ""
+msgstr "áá¥áááá ááááá¢ááááá áá ááá áááá¡ ááááá®áááá ááá ááá®áá á®áá"
#: config/chroot_local-includes/usr/local/lib/tails-additional-software-notify:37
msgid "Documentation"
-msgstr ""
+msgstr "áááá®ááá á ááá¡ááááá"
#. Translators: Don't translate {package}, it's a placeholder and will be
#. replaced.
@@ -321,32 +321,32 @@ msgstr ""
msgid ""
"Remove {package} from your additional software? This will stop installing "
"the package automatically."
-msgstr ""
+msgstr "á¬ááášáááá¡ {package} áá¥áááá áááá®ááá á áá ááá áááááá? ášáááááá ááááá¢áá¡ ááá¢áááá¢á£á á ááá§ááááá ášáá¬á§áááá."
#. Translators: Don't translate {pkg}, it's a placeholder and will be
#. replaced.
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:105
#, python-brace-format
msgid "Failed to remove {pkg}"
-msgstr ""
+msgstr "á¬áášáá ááá ááá®áá á®áá {pkg}"
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:122
msgid "Failed to read additional software configuration"
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá áááá¡ ááá áááá¢á áááá¡ á¬ááááá®áá ááá ááá®áá á®áá"
#. Translators: Don't translate {package}, it's a placeholder and will be
#. replaced.
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:152
#, python-brace-format
msgid "Stop installing {package} automatically"
-msgstr ""
+msgstr "ááŠáá ááá§ááááá¡ {package} ááá¢áááá¢á£á áá"
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:178
msgid ""
"To do so, install some software using <a href=\"synaptic.desktop\">Synaptic "
"Package Manager</a> or <a href=\"org.gnome.Terminal.desktop\">APT on the "
"command line</a>."
-msgstr ""
+msgstr "áááá¡áááá¡, áááá§áááá á áááááááá áá ááá ááá <a href=\"synaptic.desktop\">Synaptic-áá¡ ááááá¢áááá¡ áááá áááááá¡</a> á¡áášá£áááááá áá <a href=\"org.gnome.Terminal.desktop\">APT-áá á«áááááááá</a>."
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:187
msgid ""
@@ -354,89 +354,89 @@ msgid ""
"some software using <a href=\"synaptic.desktop\">Synaptic Package "
"Manager</a> or <a href=\"org.gnome.Terminal.desktop\">APT on the command "
"line</a>."
-msgstr ""
+msgstr "áááá¡áááá¡, ááá®á¡áááá áá¥áááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªááá Tails-áá¡ ááášááááá¡áá¡ áá áááá§áááá á áááááááá áá ááá ááá <a href=\"synaptic.desktop\">Synaptic-áá¡ ááááá¢áááá¡ áááá áááááá¡</a> á¡áášá£áááááá áá <a href=\"org.gnome.Terminal.desktop\">APT-áá á«áááááááá</a>."
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:197
msgid ""
"To do so, create a persistent storage and install some software using <a "
"href=\"synaptic.desktop\">Synaptic Package Manager</a> or <a "
"href=\"org.gnome.Terminal.desktop\">APT on the command line</a>."
-msgstr ""
+msgstr "áááá¡áááá¡, ášáá¥ááááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªááá áá áááá§áááá á áááááááá áá ááá ááá <a href=\"synaptic.desktop\">Synaptic-áá¡ ááááá¢áááá¡ áááá áááááá¡</a> á¡áášá£áááááá áá <a href=\"org.gnome.Terminal.desktop\">APT-áá á«áááááááá</a>."
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:205
msgid ""
"To do so, install Tails on a USB stick using <a href=\"tails-"
"installer.desktop\">Tails Installer</a> and create a persistent storage."
-msgstr ""
+msgstr "áááá¡áááá¡, Tails áááá§áááá USB-ááá¬á§ááááááááá <a href=\"tails-installer.desktop\">Tails-áá¡ ááá¡áá§ááááááá áá ááá áááá¡</a> ááášáááááá áá ášáá¥ááááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªááá."
#: config/chroot_local-includes/usr/local/bin/tails-additional-software-config:252
msgid "[package not available]"
-msgstr ""
+msgstr "[package not available]"
#: config/chroot_local-includes/usr/local/lib/tails-htp-notify-user:52
msgid "Synchronizing the system's clock"
-msgstr ""
+msgstr "á¡áá¡á¢áááá¡ á¡ááááá¡ á¡ááá¥á ááááááªáá"
#: config/chroot_local-includes/usr/local/lib/tails-htp-notify-user:53
msgid ""
"Tor needs an accurate clock to work properly, especially for Hidden "
"Services. Please wait..."
-msgstr ""
+msgstr "á¡áááá áá á¡ááááá¡ ááá¡á¬áá ááá á¡áááááááá áá£ášááááá¡áááá¡, áááá¡ááá£áá áááá á€áá á£áá áááá¡áá®á£á áááááá¡áááá¡. ááá®ááá, ááááááááá..."
#: config/chroot_local-includes/usr/local/lib/tails-htp-notify-user:87
msgid "Failed to synchronize the clock!"
-msgstr ""
+msgstr "á¡ááááá¡ á¡ááá¥á ááááááªáá ááá ááá®áá á®áá!"
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:110
msgid "Lock Screen"
-msgstr ""
+msgstr "á©ááááá¢á ááá ááá"
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:125
msgid "Screen Locker"
-msgstr ""
+msgstr "ááá áááá¡ á©ááááá¢á"
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:131
msgid "Set up a password to unlock the screen."
-msgstr ""
+msgstr "áááá§áááá ááá ááá ááá áááá¡ ááá¡áá®á¡ááááá"
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:149
msgid "Password"
-msgstr ""
+msgstr "ááá ááá"
#: config/chroot_local-includes/usr/local/bin/tails-screen-locker:150
msgid "Confirm"
-msgstr ""
+msgstr "áááááá¡á¢á£á áá"
#: config/chroot_local-includes/usr/local/bin/tails-security-check:124
msgid "This version of Tails has known security issues:"
-msgstr ""
+msgstr "Tails-áá¡ áá ááá á¡ááá¡ á£á¡áá€á áá®ááááá¡ áªáááááá á®áá áááááá áááá©ááá:"
#: config/chroot_local-includes/usr/local/bin/tails-security-check:135
msgid "Known security issues"
-msgstr ""
+msgstr "á£á¡áá€á áá®ááááá¡ áªáááááá á®áá áááááá"
#: config/chroot_local-includes/usr/local/lib/tails-spoof-mac:52
#, sh-format
msgid "Network card ${nic} disabled"
-msgstr ""
+msgstr " á¥á¡áááá¡ ááá€á ${nic} ááááášá£ááá"
#: config/chroot_local-includes/usr/local/lib/tails-spoof-mac:53
#, sh-format
msgid ""
"MAC spoofing failed for network card ${nic_name} (${nic}) so it is temporarily disabled.\n"
"You might prefer to restart Tails and disable MAC spoofing."
-msgstr ""
+msgstr "MAC-ááá¡áááá ááá¡ áááááááááá ááá ááá®áá á®áá á¥á¡áááá¡ ááá€áá¡áááá¡ ${nic_name} (${nic}), áá¡á á áá áá ááááá ááááá áá£ááá. ášáá¡áá«ááá ááá¯áááááá Tails-áá¡ á®áááá®áá ááášáááá, MAC-ááá¡áááá ááá¡ ááááááááááá¡ ááá áášá."
#: config/chroot_local-includes/usr/local/lib/tails-spoof-mac:62
msgid "All networking disabled"
-msgstr ""
+msgstr "á§áááá á¥á¡ááá ááááášá£ááá"
#: config/chroot_local-includes/usr/local/lib/tails-spoof-mac:63
#, sh-format
msgid ""
"MAC spoofing failed for network card ${nic_name} (${nic}). The error recovery also failed so all networking is disabled.\n"
"You might prefer to restart Tails and disable MAC spoofing."
-msgstr ""
+msgstr "MAC-ááá¡áááá ááá¡ áááááááááá ááá ááá®áá á®áá á¥á¡áááá¡ ááá€áá¡áááá¡ ${nic_name} (${nic}). ááá ᪠ááŠááááá ááá®áá á®áá ášááªááááá¡ ášááááá, áá¡á á áá á§áááá á¥á¡ááá ááááášá£ááá.\nášáá¡áá«ááá ááá¯áááááá Tails-áá¡ á®áááá®áá ááášáááá áá MAC-ááá¡áááá ááá¡ ááááááááááá¡ ááááášáá."
#: config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper:35
msgid ""
@@ -449,25 +449,25 @@ msgid ""
"\n"
"Or do a manual upgrade.\n"
"See https://tails.boum.org/doc/first_steps/upgrade#manual\""
-msgstr ""
+msgstr "\"<b>ááá®á¡ááá ááá áá áá¡ááááá áá¡áá ááááá®ááááááá ášáááá¬ááááá¡áááá¡.</b>\n\nááá á¬áá£áááá, á áá áá¡ á¡áá¡á¢ááá ááááá§áá€ááááá¡ á¡áááá á áááá®áááááá¡ Tails-áá¡ ááá¡áášááááá.\náá®áááá file:///usr/share/doc/tails/website/doc/about/requirements.en.html\n\ná¡áªáááá Tails-áá¡ á®áááá®áá ááášáááá ááááá®ááááááá¡ ášáááá¬ááááá¡áááá¡.\n\náá á®áááá áááááá®ááá.\náá®áááá https://tails.boum.org/doc/first_steps/upgrade#manual\""
#: config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper:72
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:27
msgid "error:"
-msgstr ""
+msgstr "ášááªáááá:"
#: config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper:73
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:28
msgid "Error"
-msgstr ""
+msgstr "ášááªáááá"
#: config/chroot_local-includes/usr/local/lib/tails-virt-notify-user:71
msgid "Warning: virtual machine detected!"
-msgstr ""
+msgstr "á§á£á áááŠááá: ááŠááá©áááááá ááá á¢á£ááá£á á áááá¥ááá!"
#: config/chroot_local-includes/usr/local/lib/tails-virt-notify-user:74
msgid "Warning: non-free virtual machine detected!"
-msgstr ""
+msgstr "ááá€á áá®ááááá: ááŠááá©áááááá áá áááááá¡á£á€ááá ááá á¢á£ááá£á á áááá¥ááá!"
#: config/chroot_local-includes/usr/local/lib/tails-virt-notify-user:77
msgid ""
@@ -475,66 +475,66 @@ msgid ""
"monitor what you are doing in Tails. Only free software can be considered "
"trustworthy, for both the host operating system and the virtualization "
"software."
-msgstr ""
+msgstr "áá áááá¡, á¡ááááá ááªáá á¡áá¡á¢áááá¡á᪠áá ááá á¢á£ááá£á áá ááá áááá¡á᪠ášáá£á«ááá ááááá ááááááá¡ áá¥áááá¡ ááá¥áááááááá¡ Tails-ášá. áá®áááá ááááá¡á£á€ááá áá ááá ááá ášááá«áááá á©áááááááá¡ á¡ááááá, á áááá ᪠á¡ááááá ááªáá á¡áá¡á¢áááá¡, áá¡ááá ááá á¢á£ááá£á á áá ááá áááá¡ ášáááá®ááááášá."
#: config/chroot_local-includes/usr/local/lib/tails-virt-notify-user:81
msgid "Learn more"
-msgstr ""
+msgstr "áá®áááá áá áªááá"
#: config/chroot_local-includes/usr/local/bin/tor-browser:43
msgid "Tor is not ready"
-msgstr ""
+msgstr "Tor áá áá áááá"
#: config/chroot_local-includes/usr/local/bin/tor-browser:44
msgid "Tor is not ready. Start Tor Browser anyway?"
-msgstr ""
+msgstr "Tor áá áá áááá. áááá᪠áááášááá¡ Tor-áá áá£ááá á?"
#: config/chroot_local-includes/usr/local/bin/tor-browser:45
msgid "Start Tor Browser"
-msgstr ""
+msgstr "Tor-áá áá£ááá áá¡ ááášáááá"
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/torstatus@tails.boum.org/extension.js:35
msgid "Tor Status"
-msgstr ""
+msgstr "Tor-áá¡ ááááááá áááá"
#: config/chroot_local-includes/usr/share/gnome-shell/extensions/torstatus@tails.boum.org/extension.js:50
msgid "Open Onion Circuits"
-msgstr ""
+msgstr "Onion-á¬á áááááá¡ ááá®á¡áá"
#. Translators: Don't translate {volume_label} or {volume_size},
#. they are placeholders and will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:58
#, python-brace-format
msgid "{volume_label} ({volume_size})"
-msgstr ""
+msgstr "{volume_label} ({volume_size})"
#. Translators: Don't translate {partition_name} or {partition_size},
#. they are placeholders and will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:63
#, python-brace-format
msgid "{partition_name} ({partition_size})"
-msgstr ""
+msgstr "{partition_name} ({partition_size})"
#. Translators: Don't translate {volume_size}, it's a placeholder
#. and will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:68
#, python-brace-format
msgid "{volume_size} Volume"
-msgstr ""
+msgstr "{volume_size} ááááá§áá€á"
#. Translators: Don't translate {volume_name}, it's a placeholder and
#. will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:107
#, python-brace-format
msgid "{volume_name} (Read-Only)"
-msgstr ""
+msgstr "{volume_name} (áá®áááá-á¬ááááá®ááá)"
#. Translators: Don't translate {partition_name} and {container_path}, they
#. are placeholders and will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:115
#, python-brace-format
msgid "{partition_name} in {container_path}"
-msgstr ""
+msgstr "{partition_name} â {container_path}"
#. Translators: Don't translate {volume_name} and {path_to_file_container},
#. they are placeholders and will be replaced. You should only have to
@@ -543,14 +543,14 @@ msgstr ""
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:122
#, python-brace-format
msgid "{volume_name} â {path_to_file_container}"
-msgstr ""
+msgstr "{volume_name} â {path_to_file_container}"
#. Translators: Don't translate {partition_name} and {drive_name}, they
#. are placeholders and will be replaced.
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:128
#, python-brace-format
msgid "{partition_name} on {drive_name}"
-msgstr ""
+msgstr "{partition_name} â {drive_name}"
#. Translators: Don't translate {volume_name} and {drive_name},
#. they are placeholders and will be replaced. You should only have to
@@ -559,15 +559,15 @@ msgstr ""
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:135
#, python-brace-format
msgid "{volume_name} â {drive_name}"
-msgstr ""
+msgstr "{volume_name} â {drive_name}"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:222
msgid "Wrong passphrase or parameters"
-msgstr ""
+msgstr "áá áá¡á¬áá á ááá ááá áá ááá áááá¢á ááá"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:224
msgid "Error unlocking volume"
-msgstr ""
+msgstr "ášááªáááá ááááá§áá€áá¡ ááá®á¡ááá¡áá¡"
#. Translators: Don't translate {volume_name} or {error_message},
#. they are placeholder and will be replaced.
@@ -576,11 +576,11 @@ msgstr ""
msgid ""
"Couldn't unlock volume {volume_name}:\n"
"{error_message}"
-msgstr ""
+msgstr "ááá ááá®áá á®áá ááá®á¡áá ááááá§áá€áá¡ {volume_name}:\n{error_message}"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:330
msgid "One or more applications are keeping the volume busy."
-msgstr ""
+msgstr "ááááá§áá€á áááááááá£ááá áá áá áá á áááááááá áá ááá áááá¡ áááá ."
#. Translators: Don't translate {volume_name} or {error_message},
#. they are placeholder and will be replaced.
@@ -589,7 +589,7 @@ msgstr ""
msgid ""
"Couldn't lock volume {volume_name}:\n"
"{error_message}"
-msgstr ""
+msgstr "ááá ááá®áá á®áá á©áááá¢áá ááááá§áá€áá¡ {volume_name}:\n{error_message}"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py:338
msgid "Locking the volume failed"
@@ -597,29 +597,29 @@ msgstr ""
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py:83
msgid "No file containers added"
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡ááá áá ááááá¢ááá£áá"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py:98
msgid "No VeraCrypt devices detected"
-msgstr ""
+msgstr "ááá áááááá®á ááá¬á§ááááááá VeraCrypt"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:40
#: ../config/chroot_local-includes/usr/share/applications/unlock-veracrypt-volumes.desktop.in.h:1
msgid "Unlock VeraCrypt Volumes"
-msgstr ""
+msgstr "VeraCrypt-áá¡ ááááá§áá€áááá¡ ááá®á¡áá"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:114
msgid "Container already added"
-msgstr ""
+msgstr "á¡ááááá¡á á£ááá ááááá¢ááá£ááá"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:115
#, python-format
msgid "The file container %s should already be listed."
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡á %s á£ááá ááŠá ááªá®á£áá á£ááá áá§áá¡"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:131
msgid "Container opened read-only"
-msgstr ""
+msgstr "á¡ááááá¡á ááá®á¡ááááá áá®áááá á¬ááááá®ááá¡ á ááááášá"
#. Translators: Don't translate {path}, it's a placeholder and will be
#. replaced.
@@ -628,106 +628,106 @@ msgstr ""
msgid ""
"The file container {path} could not be opened with write access. It was opened read-only instead. You will not be able to modify the content of the container.\n"
"{error_message}"
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡á {path} ááá áááá®á¡áááá á©áá¬áá áá¡ ášáá¡áá«áááááááá. ááá®á¡ááááá áá®áááá á¬ááááá®ááá¡ á ááááášá. ááá ášáá«áááá á¡ááááá¡áá¡ ášáááááá¡áá¡ ášááªáááá¡.\n{error_message}"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:138
msgid "Error opening file"
-msgstr ""
+msgstr "ášááªáááá á€ááááá¡ ááá®á¡ááá¡áá¡"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:160
msgid "Not a VeraCrypt container"
-msgstr ""
+msgstr "áá áá VeraCrypt-áá¡ á¡ááááá¡á"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:161
#, python-format
msgid "The file %s does not seem to be a VeraCrypt container."
-msgstr ""
+msgstr "á€áááá %s áá á¬áá áááááááá¡ VeraCrypt-áá¡ á¡ááááá¡á¡."
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:163
msgid "Failed to add container"
-msgstr ""
+msgstr "á¡ááááá¡áá¡ ááááá¢ááá ááá ááá®áá á®áá"
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:164
#, python-format
msgid ""
"Could not add file container %s: Timeout while waiting for loop setup.\n"
"Please try using the <i>Disks</i> application instead."
-msgstr ""
+msgstr "ááá áááá¢ááá á€ááááá¡ á¡ááááá¡á %s: ááá§áááááá, ááááááá áááá ááá§áááááááá. \nááá®ááá, á¡ááááªáááá áááááá§áááá <i>ááá¡ááááá¡</i> áá ááá ááá."
#: config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py:209
msgid "Choose File Container"
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡áá¡ áá á©ááá"
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:38
msgid "Do you really want to launch the Unsafe Browser?"
-msgstr ""
+msgstr "ááááááááá áá¡á£á á ááá£áªáááá áá áá£ááá áá¡ ááášáááá?"
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:40
msgid ""
"Network activity within the Unsafe Browser is <b>not anonymous</b>.\\nOnly "
"use the Unsafe Browser if necessary, for example\\nif you have to login or "
"register to activate your Internet connection."
-msgstr ""
+msgstr "á¥á¡ááášá ááá¥áááááááá ááá£áªáááá áá áá£ááá áá <b>ááá á€áá ááá¡ áááááááá¡</b>.\\nááá£áªáááá áá áá£ááá á áááááá§áááá áá®áááá áá£áªááááááá á¡áááá ááááá¡áá¡, ááááááááá\\nááááá áášáá¡ ášáá¥áááá¡áááá¡ áá ášáá¡áááá¡áááá¡, ááá¢áá ááá¢áááášáá áá¡ á©áá¡áá ááááá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:51
msgid "Starting the Unsafe Browser..."
-msgstr ""
+msgstr "áášáááá ááá£áªáááá áá áá£ááá á..."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:52
msgid "This may take a while, so please be patient."
-msgstr ""
+msgstr "ášáá¡áá«ááá ááá áááá£á á®ááá¡ ááá¡á¢áááá¡, ááá®ááá ááááááááá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:57
msgid "Shutting down the Unsafe Browser..."
-msgstr ""
+msgstr "ááá£áªáááá áá áá£ááá áá¡ ááá®á£á áá..."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:58
msgid ""
"This may take a while, and you may not restart the Unsafe Browser until it "
"is properly shut down."
-msgstr ""
+msgstr "ášáá¡áá«ááá ááá áááá£á á®ááá¡ ááá¡á¢áááá¡, áá ááááá ááá ááá£áªáááá áá áá£ááá á, á¡áááá á¡áááááááá áá áááá®á£á ááá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:70
msgid "Failed to restart Tor."
-msgstr ""
+msgstr "Tor-áá¡ á®áááá®áá ááášáááá ááá ááá®áá á®áá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:84
#: ../config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in.h:1
msgid "Unsafe Browser"
-msgstr ""
+msgstr "ááá£áªáááá áá áá£ááá á"
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:91
msgid ""
"Another Unsafe Browser is currently running, or being cleaned up. Please "
"retry in a while."
-msgstr ""
+msgstr "á¡á®áá ááá£áªáááá áá áá£ááá á ááášáááá£ááá áá áááááááá áááá¡ ááá¡á ááá¡á£á€áááááá. ááá®ááá, á¡áªáááá á®áááá®áá áªáá¢á á®ááášá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:99
msgid "Failed to setup chroot."
-msgstr ""
+msgstr "ááá á®áá á®áááá chroot-áá¡ ááááá ááá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:104
msgid "Failed to configure browser."
-msgstr ""
+msgstr "áá áá£ááá áá¡ ááááá ááá ááá ááá®áá á®áá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:110
msgid ""
"No DNS server was obtained through DHCP or manually configured in "
"NetworkManager."
-msgstr ""
+msgstr "DNS-áááá¡áá®á£á ááá áá áá á®ááááá¡áá¬ááááá DHCP-áá áá á®áááá ááááá áá£áá á¥á¡áááá¡ áááá áááááá."
#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:121
msgid "Failed to run browser."
-msgstr ""
+msgstr "áá áá£ááá áá¡ ááášáááá ááá ááá®áá á®áá."
#: ../config/chroot_local-includes/etc/skel/Desktop/Report_an_error.desktop.in.h:1
msgid "Report an error"
-msgstr ""
+msgstr "ášááªááááá¡ ááá®á¡ááááá"
#: ../config/chroot_local-includes/etc/skel/Desktop/tails-documentation.desktop.in.h:1
#: ../config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in.h:1
msgid "Tails documentation"
-msgstr ""
+msgstr "Tails-áá¡ áááá®ááá á ááá¡ááááá"
#: ../config/chroot_local-includes/usr/share/applications/root-terminal.desktop.in.h:1
msgid "Root Terminal"
@@ -739,94 +739,94 @@ msgstr ""
#: ../config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in.h:2
msgid "Learn how to use Tails"
-msgstr ""
+msgstr "áá®áááá, á áááá á£ááá áááááá§áááá Tails"
#: ../config/chroot_local-includes/usr/share/applications/tails-about.desktop.in.h:2
msgid "Learn more about Tails"
-msgstr ""
+msgstr "áá áªááá áá®áááá Tails-áá¡ ášáá¡áá®áá"
#: ../config/chroot_local-includes/usr/share/applications/tor-browser.desktop.in.h:1
msgid "Tor Browser"
-msgstr ""
+msgstr "Tor-áá áá£ááá á"
#: ../config/chroot_local-includes/usr/share/applications/tor-browser.desktop.in.h:2
msgid "Anonymous Web Browser"
-msgstr ""
+msgstr "áá áá£ááá á áááááááá¡ ááá¡áá€áá áá"
#: ../config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in.h:2
msgid "Browse the World Wide Web without anonymity"
-msgstr ""
+msgstr "áááááá®á£ááá ááááááá áááá áááááááá¡ ááá£áá®áááá"
#: ../config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in.h:3
msgid "Unsafe Web Browser"
-msgstr ""
+msgstr "ááá£áªáááá áá áá£ááá á"
#: ../config/chroot_local-includes/usr/share/applications/unlock-veracrypt-volumes.desktop.in.h:2
msgid "Mount VeraCrypt encrypted file containers and devices"
-msgstr ""
+msgstr "VeraCrypt-áá¡ á€ááááááá¡ ááášáá€á á£áá á¡ááááá¡áááá¡á áá ááá¬á§áááááááááá¡ áááá áááá"
#: ../config/chroot_local-includes/usr/share/applications/org.boum.tails.additional-software-config.desktop.in.h:2
msgid ""
"Configure the additional software installed from your persistent storage "
"when starting Tails"
-msgstr ""
+msgstr "ááááá ááá áá¥áááá áá£ááááá ááá®á¡ááá áááá¡ á¡ááªáááááá ááá§ááááá£áá ááááá¢ááááá áá ááá ááá Tails-áá¡ ááášááááá¡áá¡"
#: ../config/chroot_local-includes/usr/share/desktop-directories/Tails.directory.in.h:2
msgid "Tails specific tools"
-msgstr ""
+msgstr "Tails-áá¡ á¡áááááááá á®ááá¡áá¬á§áááá"
#: ../config/chroot_local-includes/usr/share/polkit-1/actions/org.boum.tails.root-terminal.policy.in.h:1
msgid "To start a Root Terminal, you need to authenticate."
-msgstr ""
+msgstr "á«áá áá£áá á¢áá ááááááá¡ ááá¡áášááááá, á¡áááá áá ááááá áášá áááááªááááá."
#: ../config/chroot_local-includes/usr/share/polkit-1/actions/org.boum.tails.additional-software.policy.in.h:1
msgid "Remove an additional software package"
-msgstr ""
+msgstr "ááááá¢ááááá áá ááá áááá¡ ááááá¢áá¡ á¬áášáá"
#: ../config/chroot_local-includes/usr/share/polkit-1/actions/org.boum.tails.additional-software.policy.in.h:2
msgid ""
"Authentication is required to remove a package from your additional software"
" ($(command_line))"
-msgstr ""
+msgstr "ááááá áášáá¡ áááááªáááááá¡ ááááááááá á¡áááá á ááááá¢áá¡ ááááá¢ááááá áá ááá áááááá áááá¡áášááááá ($(command_line)"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/main.ui.in:61
msgid "File Containers"
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡ááá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/main.ui.in:80
msgid "_Add"
-msgstr ""
+msgstr "_ááááá¢ááá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/main.ui.in:86
msgid "Add a file container"
-msgstr ""
+msgstr "á€ááááá¡ á¡ááááá¡áá¡ ááááá¢ááá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/main.ui.in:103
msgid "Partitions and Drives"
-msgstr ""
+msgstr "ááááá§áá€ááá áá ááá¡áááá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/main.ui.in:121
msgid ""
"This application is not affiliated with or endorsed by the VeraCrypt project"
" or IDRIX."
-msgstr ""
+msgstr "áá¡ áá ááá ááá áá áá ááá¬ááááá£áá áá ááááá¬áááá£áá VeraCrypt-áá ááá¥á¢áá¡ áá IDRIX-áá¡ áááá ."
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/volume.ui.in:29
msgid "_Open"
-msgstr ""
+msgstr "_ááá®á¡áá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/volume.ui.in:38
msgid "Lock this volume"
-msgstr ""
+msgstr "ááááá§áá€áá¡ á©áááá¢áá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/volume.ui.in:52
msgid "_Unlock"
-msgstr ""
+msgstr "_áááŠááá"
#: ../config/chroot_local-includes/usr/share/tails/unlock-veracrypt-volumes/volume.ui.in:61
msgid "Detach this volume"
-msgstr ""
+msgstr "ááááá§áá€áá¡ ááá®á¡áá"
#: ../config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in.h:1
msgid "TrueCrypt/VeraCrypt container"
-msgstr ""
+msgstr "TrueCrypt/VeraCrypt-áá¡ á¡ááááá¡á"
1
0
15 Aug '19
commit 0a1de3f58abe20f73510a43198365fb1fb9f9580
Merge: 46dde4c ed37530
Author: Philipp Winter <phw(a)nymity.ch>
Date: Thu Aug 15 14:57:20 2019 -0700
Merge branch 'fix/22755' into develop
.test.requirements.txt | 1 -
.travis.requirements.txt | 1 -
CHANGELOG | 4 +
README.rst | 6 +-
bridgedb/main.py | 10 +-
bridgedb/runner.py | 37 +-----
doc/HACKING.md | 10 +-
scripts/create_descriptors | 298 +++++++++++++++++++++++++++++++++++++++++++++
scripts/setup-tests | 2 +-
setup.py | 2 +-
10 files changed, 316 insertions(+), 55 deletions(-)
1
0
[bridgedb/develop] Use stem instead of leekspin to make descriptors.
by phw@torproject.org 15 Aug '19
by phw@torproject.org 15 Aug '19
15 Aug '19
commit ed37530c24fbf8ef80c7976ee4da1b667e9ba725
Author: Philipp Winter <phw(a)nymity.ch>
Date: Tue Jun 11 16:02:15 2019 -0700
Use stem instead of leekspin to make descriptors.
Stem now provides the ability to create and sign descriptors, so
there's no reason to use leekspin anymore. Stem is faster at creating
descriptors and we're already heavily depending on stem anyway.
This patch improves the preliminary work that Isis Lovecruft and
Damian Johnson already did. Some code in this patch was taken from
leekspin.
This fixes bug 22755: <https://bugs.torproject.org/22755>
---
.test.requirements.txt | 1 -
.travis.requirements.txt | 1 -
CHANGELOG | 4 +
README.rst | 6 +-
bridgedb/main.py | 10 +-
bridgedb/runner.py | 37 +-----
doc/HACKING.md | 10 +-
scripts/create_descriptors | 298 +++++++++++++++++++++++++++++++++++++++++++++
scripts/setup-tests | 2 +-
setup.py | 2 +-
10 files changed, 316 insertions(+), 55 deletions(-)
diff --git a/.test.requirements.txt b/.test.requirements.txt
index c84fb58..ad5342f 100644
--- a/.test.requirements.txt
+++ b/.test.requirements.txt
@@ -6,7 +6,6 @@
# $ make coverage
#
coverage==4.2
-git+https://git.torproject.org/user/phw/leekspin.git@d34c804cd0f01af5206833e62c0dedec8565b235#egg=leekspin
mechanize==0.2.5
pep8==1.5.7
# pylint must be pinned until pylint bug #203 is fixed. See
diff --git a/.travis.requirements.txt b/.travis.requirements.txt
index 2d56b79..e6eaf10 100644
--- a/.travis.requirements.txt
+++ b/.travis.requirements.txt
@@ -15,7 +15,6 @@
#------------------------------------------------------------------------------
coverage==4.2
coveralls==1.2.0
-git+https://git.torproject.org/user/phw/leekspin.git@d34c804cd0f01af5206833e62c0dedec8565b235#egg=leekspin
mechanize==0.2.5
sure==1.2.2
Babel==0.9.6
diff --git a/CHANGELOG b/CHANGELOG
index ae0e651..32e6fe5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -12,6 +12,10 @@ Changes in version 0.8.0 - YYYY-MM-DD
* FIXES #26542 https://bugs.torproject.org/26542
Make BridgeDB distribute vanilla IPv6 bridges again.
+ * FIXES #22755 https://bugs.torproject.org/22755
+ Use stem instead of leekspin to create test descriptors. We now don't
+ need to depend on leekspin anymore.
+
Changes in version 0.7.1 - 2019-06-07
* FIXES #28496 https://bugs.torproject.org/28496
diff --git a/README.rst b/README.rst
index ee1c979..4f05ea5 100644
--- a/README.rst
+++ b/README.rst
@@ -315,11 +315,7 @@ To create a bunch of fake bridge descriptors to test BridgeDB, do::
bridgedb mock [-n NUMBER_OF_DESCRIPTORS]
-Note that you will need to install
-`leekspin <https://pypi.python.org/pypi/leekspin>`__ in order to run the
-``bridgedb mock`` command. See ``doc/HACKING.md`` for details.
-
-And finally, to run the test suites, do::
+To run the test suites, do::
make coverage
diff --git a/bridgedb/main.py b/bridgedb/main.py
index 6b99127..5d9b0c6 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -551,15 +551,9 @@ def runSubcommand(options, config):
# mentioned above with the email.server and https.server.
from bridgedb import runner
- statuscode = 0
-
if options.subCommand is not None:
logging.debug("Running BridgeDB command: '%s'" % options.subCommand)
if 'descriptors' in options.subOptions:
- statuscode = runner.generateDescriptors(
- options.subOptions['descriptors'], config.RUN_IN_DIR)
-
- logging.info("Subcommand '%s' finished with status %s."
- % (options.subCommand, statuscode))
- sys.exit(statuscode)
+ runner.generateDescriptors(int(options.subOptions['descriptors']), config.RUN_IN_DIR)
+ sys.exit(0)
diff --git a/bridgedb/runner.py b/bridgedb/runner.py
index b1a21d2..b6117e1 100644
--- a/bridgedb/runner.py
+++ b/bridgedb/runner.py
@@ -81,17 +81,6 @@ def find(filename):
def generateDescriptors(count=None, rundir=None):
"""Run a script which creates fake bridge descriptors for testing purposes.
- This will run Leekspin_ to create bridge server descriptors, bridge
- extra-info descriptors, and networkstatus document.
-
- .. warning: This function can take a very long time to run, especially in
- headless environments where entropy sources are minimal, because it
- creates the keys for each mocked OR, which are embedded in the server
- descriptors, used to calculate the OR fingerprints, and sign the
- descriptors, among other things.
-
- .. _Leekspin: https://gitweb.torproject.org/user/phw/leekspin.git
-
:param integer count: Number of mocked bridges to generate descriptor
for. (default: 3)
:type rundir: string or None
@@ -100,25 +89,11 @@ def generateDescriptors(count=None, rundir=None):
directory MUST already exist, and the descriptor files will be created
in it. If None, use the whatever directory we are currently in.
"""
- import subprocess
- import os.path
+ from stem.descriptor.server_descriptor import RelayDescriptor
- proc = None
- statuscode = 0
- script = 'leekspin'
- rundir = rundir if os.path.isdir(rundir) else None
count = count if count else 3
- try:
- proc = subprocess.Popen([script, '-n', str(count)],
- close_fds=True, cwd=rundir)
- finally:
- if proc is not None:
- proc.wait()
- if proc.returncode:
- print("There was an error generating bridge descriptors.",
- "(Returncode: %d)" % proc.returncode)
- statuscode = proc.returncode
- else:
- print("Sucessfully generated %s descriptors." % str(count))
- del subprocess
- return statuscode
+ rundir = rundir if rundir else os.getcwd()
+
+ for i in range(count):
+ with open(os.path.join(rundir, 'descriptor_%i' % i), 'w') as descriptor_file:
+ descriptor_file.write(RelayDescriptor.content(sign = True))
diff --git a/doc/HACKING.md b/doc/HACKING.md
index aa6c119..a8ec640 100644
--- a/doc/HACKING.md
+++ b/doc/HACKING.md
@@ -12,20 +12,16 @@ with password ```writecode```.
## Generating bridge descriptors
Developers wishing to test BridgeDB will need to generate mock bridge
-descriptors. This is accomplished through the [leekspin
-script](https://gitweb.torproject.org/user/phw/leekspin.git). To generate 20
-bridge descriptors, change to the bridgedb running directory and do:
+descriptors. This is accomplished through the file **create-descriptors**. To
+generate 20 bridge descriptors, change to the bridgedb running directory and do:
- $ leekspin -n 20
+ $ ./scripts/create-descriptors 20
It is recommended that you generate at least 250 descriptors for testing.
Ideally, even more descriptors should be generated, somewhere in the realm of
2000, as certain bugs do not emerge until BridgeDB is processing thousands of
descriptors.
-**Leekspin is for testing purposes only and should never be deployed on a
-production server.** We do not want to distribute fake bridges.
-
## Git Workflow
See this article on git branching [workflow][workflow]. The only modifications
diff --git a/scripts/create_descriptors b/scripts/create_descriptors
new file mode 100755
index 0000000..839a6ba
--- /dev/null
+++ b/scripts/create_descriptors
@@ -0,0 +1,298 @@
+#!/usr/bin/env python2.7
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+
+import os
+import random
+import sys
+import time
+import ipaddr
+import math
+import argparse
+import hashlib
+
+# A bunch of Tor version numbers.
+SERVER_VERSIONS = ["0.2.2.39",
+ "0.2.3.24-rc",
+ "0.2.3.25",
+ "0.2.4.5-alpha",
+ "0.2.4.6-alpha",
+ "0.2.4.7-alpha",
+ "0.2.4.8-alpha",
+ "0.2.4.9-alpha",
+ "0.2.4.10-alpha",
+ "0.2.4.11-alpha",
+ "0.2.4.12-alpha",
+ "0.2.4.14-alpha",
+ "0.2.4.15-rc",
+ "0.2.4.16-rc",
+ "0.2.4.17-rc",
+ "0.2.4.18-rc",
+ "0.2.4.19",
+ "0.2.4.20",
+ "0.2.5.1-alpha",
+ ]
+
+try:
+ import stem
+ import stem.descriptor
+ from stem.descriptor.server_descriptor import RelayDescriptor
+ from stem.descriptor.extrainfo_descriptor import RelayExtraInfoDescriptor
+ from stem.descriptor.networkstatus import NetworkStatusDocumentV3
+except ImportError:
+ print("Creating descriptors requires stem <https://stem.torproject.org>")
+ sys.exit(1)
+
+if not hasattr(stem.descriptor, "create_signing_key"):
+ print("This requires stem version 1.6 or later but you are running "
+ "version %s" % stem.__version__)
+ sys.exit(1)
+
+
+def make_output_dir():
+ if not os.path.exists(os.getcwd()):
+ os.mkdir(os.getcwd())
+
+
+def write_descriptors(descs, filename):
+ make_output_dir()
+ with open(os.path.join(os.getcwd(), filename), "w") as descriptor_file:
+ for descriptor in descs:
+ descriptor_file.write(str(descriptor))
+
+
+def write_descriptor(desc, filename):
+ make_output_dir()
+ with open(os.path.join(os.getcwd(), filename), "w") as descriptor_file:
+ descriptor_file.write(str(desc))
+
+
+def check_ip_validity(ip):
+ if (ip.is_link_local or
+ ip.is_loopback or
+ ip.is_multicast or
+ ip.is_private or
+ ip.is_unspecified or
+ ((ip.version == 6) and ip.is_site_local) or
+ ((ip.version == 4) and ip.is_reserved)):
+ return False
+ return True
+
+
+def get_transport_line(probing_resistant, addr, port):
+ """
+ If probing_resistant is True, add a transport protocol that's resistant to
+ active probing attacks.
+ """
+
+ transports = []
+ if probing_resistant:
+ transports.append("obfs2 %s:%s" % (addr, port-10))
+ iat_mode = random.randint(0, 1)
+ node_id = hashlib.sha1(bytes(random.getrandbits(8))).hexdigest()
+ public_key = hashlib.sha256(bytes(random.getrandbits(8))).hexdigest()
+ transports.append("obfs4 %s:%s iat-mode=%s,node-id=%s,public-key=%s" %
+ (addr, port-20, iat_mode, node_id, public_key))
+
+ # Always include obfs4 and occasionally include scramblesuit.
+
+ if random.randint(0, 1) > 0:
+ transports.append("scramblesuit 216.117.3.62:63174 "
+ "password=ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
+ else:
+ transports.append("obfs2 %s:%s" % (addr, port-10))
+ transports.append("obfs3 %s:%s" % (addr, port-20))
+
+ return "\ntransport ".join(transports)
+
+
+def get_hex_string(size):
+ hexstr = ""
+ for _ in range(size):
+ hexstr += random.choice("ABCDEF0123456789")
+ return hexstr
+
+
+def get_random_ipv6_addr():
+ valid_addr = None
+ while not valid_addr:
+ maybe = ipaddr.IPv6Address(random.getrandbits(128))
+ valid = check_ip_validity(maybe)
+ if valid:
+ valid_addr = maybe
+ break
+ return str(valid_addr)
+
+
+def get_random_ipv4_addr():
+ return "%i.%i.%i.%i" % (random.randint(0, 255),
+ random.randint(0, 255),
+ random.randint(0, 255),
+ random.randint(0, 255))
+
+
+def get_protocol(tor_version):
+ line = ""
+ if tor_version is not None:
+ line += "opt "
+ line += "protocols Link 1 2 Circuit 1"
+ return line
+
+
+def make_timestamp(now=None, fmt=None, variation=False, period=None):
+ now = int(now) if now is not None else int(time.time())
+ fmt = fmt if fmt else "%Y-%m-%d %H:%M:%S"
+
+ if variation:
+ then = 1
+ if period is not None:
+ secs = int(period) * 3600
+ then = now - secs
+ # Get a random number between one epochseconds number and another
+ diff = random.randint(then, now)
+ # Then rewind the clock
+ now = diff
+
+ return time.strftime(fmt, time.localtime(now))
+
+
+def make_bandwidth(variance=30):
+ observed = random.randint(20 * 2**10, 2 * 2**30)
+ percentage = float(variance) / 100.
+ burst = int(observed + math.ceil(observed * percentage))
+ bandwidths = [burst, observed]
+ nitems = len(bandwidths) if (len(bandwidths) > 0) else float("nan")
+ avg = int(math.ceil(float(sum(bandwidths)) / nitems))
+ return "%s %s %s" % (avg, burst, observed)
+
+
+def make_bridge_distribution_request():
+
+ methods = [
+ "any",
+ "none",
+ "https",
+ "email",
+ "moat",
+ ]
+
+ return random.choice(methods)
+
+
+def create_server_desc(signing_key):
+ """
+ Create and return a server descriptor.
+ """
+
+ nickname = ("Unnamed%i" % random.randint(0, 100000000000000))[:19]
+
+ # We start at port 10 because we subtract from this port to get port
+ # numbers for IPv6 and obfuscation protocols.
+
+ port = random.randint(10, 65535)
+ tor_version = random.choice(SERVER_VERSIONS)
+ timestamp = make_timestamp(variation=True, period=36)
+
+ server_desc = RelayDescriptor.create({
+ "router": "%s %s %s 0 0" % (nickname, get_random_ipv4_addr(), port),
+ "or-address": "[%s]:%s" % (get_random_ipv6_addr(), port-1),
+ "platform": "Tor %s on Linux" % tor_version,
+ get_protocol(tor_version): "",
+ "published": timestamp,
+ "uptime": str(int(random.randint(1800, 63072000))),
+ "bandwidth": make_bandwidth(),
+ "contact": "Somebody <somebody(a)example.com>",
+ "bridge-distribution-request": make_bridge_distribution_request(),
+ "reject": "*:*",
+ }, signing_key=signing_key)
+
+ return server_desc
+
+
+def create_extrainfo_desc(server_desc, signing_key, probing_resistant):
+ """
+ Create and return an extrainfo descriptor.
+ """
+
+ ts = server_desc.published
+
+ extrainfo_desc = RelayExtraInfoDescriptor.create({
+ "extra-info": "%s %s" % (server_desc.nickname,
+ server_desc.fingerprint),
+ "transport": get_transport_line(probing_resistant,
+ server_desc.address,
+ server_desc.or_port),
+ "write-history": "%s (900 s) 3188736,2226176,2866176" % ts,
+ "read-history": "%s (900 s) 3891200,2483200,2698240" % ts,
+ "dirreq-write-history": "%s (900 s) 1024,0,2048" % ts,
+ "dirreq-read-history": "%s (900 s) 0,0,0" % ts,
+ "geoip-db-digest": "%s" % get_hex_string(40),
+ "geoip6-db-digest": "%s" % get_hex_string(40),
+ "dirreq-stats-end": "%s (86400 s)" % ts,
+ "dirreq-v3-ips": "",
+ "dirreq-v3-reqs": "",
+ "dirreq-v3-resp": "ok=16,not-enough-sigs=0,unavailable=0,"
+ "not-found=0,not-modified=0,busy=0",
+ "dirreq-v3-direct-dl": "complete=0,timeout=0,running=0",
+ "dirreq-v3-tunneled-dl": "complete=12,timeout=0,running=0",
+ "bridge-stats-end": "%s (86400 s)" % ts,
+ "bridge-ips": "ca=8",
+ "bridge-ip-versions": "v4=8,v6=0",
+ "bridge-ip-transports": "<OR>=8",
+ }, signing_key=signing_key)
+
+ return extrainfo_desc
+
+
+def make_descriptors(count, num_probing_resistant):
+ """
+ Create fake descriptors and write them to the working directory.
+ """
+
+ consensus_entries = []
+ server_descriptors = []
+ extrainfos_old = []
+ extrainfos_new = []
+
+ for i in range(count):
+ signing_key = stem.descriptor.create_signing_key()
+
+ server_desc = create_server_desc(signing_key)
+ server_descriptors.append(server_desc)
+ consensus_entries.append(server_desc.make_router_status_entry())
+
+ extrainfo_desc = create_extrainfo_desc(server_desc,
+ signing_key,
+ num_probing_resistant > 0)
+ if random.random() > 0.75:
+ extrainfos_new.append(extrainfo_desc)
+ else:
+ extrainfos_old.append(extrainfo_desc)
+
+ if num_probing_resistant > 0:
+ num_probing_resistant -= 1
+
+ consensus = NetworkStatusDocumentV3.create(routers=consensus_entries)
+ write_descriptor(consensus, "networkstatus-bridges")
+ write_descriptors(server_descriptors, "bridge-descriptors")
+ write_descriptors(extrainfos_old, "cached-extrainfo")
+ write_descriptors(extrainfos_new, "cached-extrainfo.new")
+
+
+if __name__ == "__main__":
+
+ parser = argparse.ArgumentParser(description="Create fake descriptors.")
+ parser.add_argument("num_descs",
+ type=int,
+ help="The number of descriptors to create.")
+ parser.add_argument("--num-resistant-descs",
+ dest="num_resistant_descs",
+ type=int,
+ default=-1,
+ help="The number of active probing-resistant "
+ "descriptors to create")
+ args = parser.parse_args()
+ if args.num_resistant_descs == -1:
+ args.num_resistant_descs = args.num_descs
+
+ make_descriptors(args.num_descs, args.num_resistant_descs)
diff --git a/scripts/setup-tests b/scripts/setup-tests
index ccfd6cd..1de3c71 100755
--- a/scripts/setup-tests
+++ b/scripts/setup-tests
@@ -29,7 +29,7 @@ sed -r -i -e "s/(SERVER_PUBLIC_FQDN = )(.*)/\1'127.0.0.1:6788'/" run/bridgedb.co
sed -r -i -e "s/(MOAT_HTTP_IP = )(None)/\1'127.0.0.1'/" run/bridgedb.conf
sed -r -i -e "s/(MOAT_HTTP_PORT = )(None)/\16790/" run/bridgedb.conf
# Create descriptors
-leekspin -n 100 -xp 50
+./scripts/create_descriptors 200 --num-resistant-descs 100
cp -t run/from-authority networkstatus-bridges cached-extrainfo* bridge-descriptors
cp -t run/from-bifroest networkstatus-bridges cached-extrainfo* bridge-descriptors
# Create TLS certificates
diff --git a/setup.py b/setup.py
index a31391c..d416289 100644
--- a/setup.py
+++ b/setup.py
@@ -387,7 +387,7 @@ setuptools.setup(
'scripts/get-tor-exits'],
extras_require={'test': ["sure==1.2.2",
"coverage==4.2",
- "leekspin==1.1.4"]},
+ "cryptography==1.9"]},
zip_safe=False,
cmdclass=get_cmdclass(),
include_package_data=True,
1
0
commit 85a69d1be3f14a8ba0c5ba8e4840e40214217941
Author: Philipp Winter <phw(a)nymity.ch>
Date: Mon Aug 12 13:57:43 2019 -0700
Update comment.
Several variables that are referenced in the comment no longer exist.
---
bridgedb/main.py | 19 +++----------------
1 file changed, 3 insertions(+), 16 deletions(-)
diff --git a/bridgedb/main.py b/bridgedb/main.py
index 1a617b2..4d1d38a 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -368,10 +368,9 @@ def run(options, reactor=reactor):
State should be saved before calling this method, and will be saved
again at the end of it.
- The internal variables, ``cfg``, ``hashring``, ``proxyList``,
- ``ipDistributor``, and ``emailDistributor`` are all taken from a
- :class:`~bridgedb.persistent.State` instance, which has been saved to
- a statefile with :meth:`bridgedb.persistent.State.save`.
+ The internal variables ``cfg`` and ``hashring`` are taken from a
+ :class:`~bridgedb.persistent.State` instance, which has been saved to a
+ statefile with :meth:`bridgedb.persistent.State.save`.
:type cfg: :class:`Conf`
:ivar cfg: The current configuration, including any in-memory
@@ -380,18 +379,6 @@ def run(options, reactor=reactor):
:type hashring: A :class:`~bridgedb.Bridges.BridgeSplitter`
:ivar hashring: A class which takes an HMAC key and splits bridges
into their hashring assignments.
- :type proxyList: :class:`~bridgedb.proxy.ProxySet`
- :ivar proxyList: The container for the IP addresses of any currently
- known open proxies.
- :ivar ipDistributor: A
- :class:`~bridgedb.distributors.https.distributor.HTTPSDistributor`.
- :ivar emailDistributor: A
- :class:`~bridgedb.distributors.email.distributor.EmailDistributor`.
- :ivar dict tasks: A dictionary of ``{name: task}``, where name is a
- string to associate with the ``task``, and ``task`` is some
- scheduled event, repetitive or otherwise, for the :class:`reactor
- <twisted.internet.epollreactor.EPollReactor>`. See the classes
- within the :api:`twisted.internet.tasks` module.
"""
logging.debug("Caught SIGHUP")
logging.info("Reloading...")
1
0
15 Aug '19
commit 46dde4ce5842a5bf73f16c2c4de451bb5c21dcc4
Merge: 081ff36 5cde59d
Author: Philipp Winter <phw(a)nymity.ch>
Date: Thu Aug 15 14:05:05 2019 -0700
Merge branch 'feature/9316' into develop
CHANGELOG | 11 +-
bridgedb.conf | 16 +-
bridgedb/distributors/email/autoresponder.py | 16 +
bridgedb/distributors/https/server.py | 11 +
bridgedb/distributors/moat/server.py | 15 +
bridgedb/main.py | 55 ++--
bridgedb/metrics.py | 461 +++++++++++++++++++++++++++
bridgedb/proxy.py | 15 +-
bridgedb/test/test_metrics.py | 204 ++++++++++++
scripts/get-tor-exits | 2 +-
10 files changed, 775 insertions(+), 31 deletions(-)
1
0
15 Aug '19
commit 0d5ed52e5906260e142ef9cfa12752810fd1ffa2
Author: Philipp Winter <phw(a)nymity.ch>
Date: Mon Aug 12 13:52:10 2019 -0700
Fix broken download of Tor exit relays.
The periodic download of Tor exit relays was broken for a number of
reasons. Here's what we're doing to fix this issue:
1. We're moving the proxies variable outside of state. The problem is
that the state object is written to disk and reloaded every 30
minutes (a cron job is triggering this reload by running
~/bridgedb-admin/reload-bridgedb). The reload causes state.proxies
to be at a different memory address than before the reload, which
breaks the looping call that fetches new exit relays every three
hours. This looping call expects to write exit relays to the same
memory address each time, but after BridgeDB's first reload, the
memory address changed, so exit relays are no longer updated.
There's no need to keep our proxies in BridgeDB's state. We fetch
them continuously anyway, and also right after BridgeDB starts.
2. We're adding the method replaceExitRelays(). Once we have a new
batch of exit relay addresses, this method allows us to completely
overwrite the past batch.
3. We're adding the argument "setStdout=False" to the call to
startLogging() because otherwise we're missing the download script's
output.
---
bridgedb/main.py | 7 +++----
bridgedb/proxy.py | 15 +++++++++++++--
scripts/get-tor-exits | 2 +-
3 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/bridgedb/main.py b/bridgedb/main.py
index c1e6250..1a617b2 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -359,7 +359,6 @@ def run(options, reactor=reactor):
moatDistributor = None
# Save our state
- state.proxies = proxies
state.key = key
state.save()
@@ -411,13 +410,13 @@ def run(options, reactor=reactor):
logging.info("Reloading the list of open proxies...")
for proxyfile in cfg.PROXY_LIST_FILES:
logging.info("Loading proxies from: %s" % proxyfile)
- proxy.loadProxiesFromFile(proxyfile, state.proxies, removeStale=True)
+ proxy.loadProxiesFromFile(proxyfile, proxies, removeStale=True)
logging.info("Reparsing bridge descriptors...")
(hashring,
emailDistributorTmp,
ipDistributorTmp,
- moatDistributorTmp) = createBridgeRings(cfg, state.proxies, key)
+ moatDistributorTmp) = createBridgeRings(cfg, proxies, key)
logging.info("Bridges loaded: %d" % len(hashring))
# Initialize our DB.
@@ -483,7 +482,7 @@ def run(options, reactor=reactor):
if config.TASKS['GET_TOR_EXIT_LIST']:
tasks['GET_TOR_EXIT_LIST'] = task.LoopingCall(
proxy.downloadTorExits,
- state.proxies,
+ proxies,
config.SERVER_PUBLIC_EXTERNAL_IP)
if config.TASKS.get('DELETE_UNPARSEABLE_DESCRIPTORS'):
diff --git a/bridgedb/proxy.py b/bridgedb/proxy.py
index 39cd109..6d93c48 100644
--- a/bridgedb/proxy.py
+++ b/bridgedb/proxy.py
@@ -51,7 +51,7 @@ def downloadTorExits(proxyList, ipaddress, port=443, protocol=None):
"""
proto = ExitListProtocol() if protocol is None else protocol()
args = [proto.script, '--stdout', '-a', ipaddress, '-p', str(port)]
- proto.deferred.addCallback(proxyList.addExitRelays)
+ proto.deferred.addCallback(proxyList.replaceExitRelays)
proto.deferred.addErrback(logging.exception)
transport = reactor.spawnProcess(proto, proto.script, args=args, env={})
return proto.deferred
@@ -76,7 +76,7 @@ def loadProxiesFromFile(filename, proxySet=None, removeStale=False):
:returns: A list of all the proxies listed in the **files* (regardless of
whether they were added or removed).
"""
- logging.info("Reloading proxy lists...")
+ logging.info("Reloading proxy lists from file %s" % filename)
addresses = []
@@ -256,6 +256,17 @@ class ProxySet(MutableSet):
logging.info("Loading exit relays into proxy list...")
[self.add(x, self._exitTag) for x in relays]
+ def replaceExitRelays(self, relays):
+ existingExitRelays = self.getAllWithTag(self._exitTag)
+ logging.debug("Replacing %d existing with %d new exit relays." %
+ (len(existingExitRelays), len(relays)))
+
+ for relay in existingExitRelays:
+ self.discard(relay)
+
+ self.addExitRelays(relays)
+
+
def getTag(self, ip):
"""Get the tag for an **ip** in this ``ProxySet``, if available.
diff --git a/scripts/get-tor-exits b/scripts/get-tor-exits
index fcd9fff..6ab2201 100755
--- a/scripts/get-tor-exits
+++ b/scripts/get-tor-exits
@@ -36,7 +36,7 @@ from twisted.internet.error import DNSLookupError
from twisted.internet.error import TimeoutError
-log.startLogging(sys.stderr)
+log.startLogging(sys.stderr, setStdout=False)
def backupFile(filename):
1
0
commit 5cde59d9ccafdb248ca8aa9c1c9abbfe2edb5dc6
Author: Philipp Winter <phw(a)nymity.ch>
Date: Mon Aug 12 14:05:48 2019 -0700
Make BridgeDB export usage metrics.
Until now, we had no insight into how BridgeDB is being used. We don't
know the relative popularity of our distribution method; we don't know
how many users BridgeDB sees; we don't know how many requests succeed or
fail; and we don't know the relative popularity of transports that users
request.
This patch attempts to answer these questions by making BridgeDB export
usage metrics. At the end of each 24-hour measurement interval,
BridgeDB will append usage metrics to the file METRICS_FILE, which is
configured in bridgedb.conf.
Our metrics keep track of the number of (un)successful requests per
transport type per country code (or email provider) per distribution
method. This way, we get to learn that, say, over the last 24 hours
there were 31-40 users in Iran who successfully requested an obfs4
bridge over Moat. The corresponding metrics line would look as follows:
bridgedb-metric-count moat.obfs4.ir.success.none 40
To make the metrics preserve user privacy, we don't collect
user-identifying information and we introduce noise by rounding up
metrics to our bin size which defaults to 10.
This patch also extends the looping calls that BridgeDB spawns. When
BridgeDB first starts, it loads proxies from the files PROXY_LIST_FILES.
It augments this list of proxies with Tor exit relays that we download
every three hours.
---
CHANGELOG | 11 +-
bridgedb.conf | 16 +-
bridgedb/distributors/email/autoresponder.py | 16 +
bridgedb/distributors/https/server.py | 11 +
bridgedb/distributors/moat/server.py | 15 +
bridgedb/main.py | 29 +-
bridgedb/metrics.py | 461 +++++++++++++++++++++++++++
bridgedb/test/test_metrics.py | 204 ++++++++++++
8 files changed, 755 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index 1229578..ae0e651 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,13 @@
-Changes in version 0.7.2 - YYYY-MM-DD
+Changes in version 0.8.0 - YYYY-MM-DD
+
+ * FIXES https://bugs.torproject.org/9316
+ Make BridgeDB export usage metrics every 24 hours. At the end of each
+ 24-hour measurement interval, BridgeDB will append usage metrics to the
+ file METRICS_FILE, which is configured in bridgedb.conf. Our metrics
+ keep track of the number of (un)successful requests per transport type
+ per country code (or email provider) per distribution method. This way,
+ we get to learn that, say, over the last 24 hours there were 31-40 users
+ in Iran who successfully requested an obfs4 bridge over Moat.
* FIXES #26542 https://bugs.torproject.org/26542
Make BridgeDB distribute vanilla IPv6 bridges again.
diff --git a/bridgedb.conf b/bridgedb.conf
index 66b1983..ba43bb6 100644
--- a/bridgedb.conf
+++ b/bridgedb.conf
@@ -177,6 +177,9 @@ MASTER_KEY_FILE = "secret_key"
# File to which we dump bridge pool assignments for statistics.
ASSIGNMENTS_FILE = "assignments.log"
+# Name of the file that contains BridgeDB's metrics.
+METRICS_FILE = "bridgedb-metrics.log"
+
#------------------
# Logging Options \
#------------------------------------------------------------------------------
@@ -260,16 +263,19 @@ FORCE_FLAGS = [("Stable", 1)]
# Only consider routers whose purpose matches this string.
BRIDGE_PURPOSE = "bridge"
-# TASKS is a dictionary mapping the names of tasks to the frequency with which
-# they should be run (in seconds). If a task's value is set to 0, it will not
-# be scheduled to run.
+# TASKS is a dictionary mapping the names of tasks to a tuple consisting of the
+# frequency with which they should be run (in seconds) and a boolean value
+# expressing if the task should be run immediately after start up. If a task's
+# frequency is set to 0, it will not be scheduled to run.
TASKS = {
# Download a list of Tor exit relays once every three hours (by running
# scripts/get-exit-list) and add those exit relays to the list of proxies
# loaded from the PROXY_LIST_FILES:
- 'GET_TOR_EXIT_LIST': 3 * 60 * 60,
+ 'GET_TOR_EXIT_LIST': (3 * 60 * 60, True),
# Delete *.unparseable descriptor files which are more than 24 hours old:
- 'DELETE_UNPARSEABLE_DESCRIPTORS': 24 * 60 * 60,
+ 'DELETE_UNPARSEABLE_DESCRIPTORS': (24 * 60 * 60, False),
+ # Export usage metrics every 24 hours:
+ 'EXPORT_METRICS': (24 * 60 * 60, False),
}
# SUPPORTED_TRANSPORTS is a dictionary mapping Pluggable Transport methodnames
diff --git a/bridgedb/distributors/email/autoresponder.py b/bridgedb/distributors/email/autoresponder.py
index ff65a73..e69f78a 100644
--- a/bridgedb/distributors/email/autoresponder.py
+++ b/bridgedb/distributors/email/autoresponder.py
@@ -48,6 +48,8 @@ from twisted.internet import reactor
from twisted.mail import smtp
from twisted.python import failure
+from bridgedb import strings
+from bridgedb import metrics
from bridgedb import safelog
from bridgedb.crypto import NEW_BUFFER_INTERFACE
from bridgedb.distributors.email import dkim
@@ -62,6 +64,10 @@ from bridgedb.parse.addr import canonicalizeEmailDomain
from bridgedb.util import levenshteinDistance
from bridgedb import translations
+# We use our metrics singleton to keep track of BridgeDB metrics such as
+# "number of failed HTTPS bridge requests."
+metrix = metrics.EmailMetrics()
+
def createResponseBody(lines, context, client, lang='en'):
"""Parse the **lines** from an incoming email request and determine how to
@@ -424,6 +430,16 @@ class SMTPAutoresponder(smtp.SMTPClient):
body = createResponseBody(self.incoming.lines,
self.incoming.context,
client, lang)
+
+ # The string EMAIL_MISC_TEXT[1] shows up in an email if BridgeDB
+ # responds with bridges. Everything else we count as an invalid
+ # request.
+ translator = translations.installTranslations(lang)
+ if body is not None and translator.gettext(strings.EMAIL_MISC_TEXT[1]) in body:
+ metrix.recordValidEmailRequest(self)
+ else:
+ metrix.recordInvalidEmailRequest(self)
+
if not body: return # The client was already warned.
messageID = self.incoming.message.getheader("Message-ID", None)
diff --git a/bridgedb/distributors/https/server.py b/bridgedb/distributors/https/server.py
index 8c50bc1..732f8bf 100644
--- a/bridgedb/distributors/https/server.py
+++ b/bridgedb/distributors/https/server.py
@@ -52,6 +52,7 @@ from bridgedb import crypto
from bridgedb import strings
from bridgedb import translations
from bridgedb import txrecaptcha
+from bridgedb import metrics
from bridgedb.distributors.common.http import setFQDN
from bridgedb.distributors.common.http import getFQDN
from bridgedb.distributors.common.http import getClientIP
@@ -85,6 +86,10 @@ logging.debug("Set template root to %s" % TEMPLATE_DIR)
#: Localisations which BridgeDB supports which should be rendered right-to-left.
rtl_langs = ('ar', 'he', 'fa', 'gu_IN', 'ku')
+# We use our metrics singleton to keep track of BridgeDB metrics such as
+# "number of failed HTTPS bridge requests."
+metrix = metrics.HTTPSMetrics()
+
def replaceErrorPage(request, error, template_name=None, html=True):
"""Create a general error page for displaying in place of tracebacks.
@@ -495,6 +500,7 @@ class CaptchaProtectedResource(CustomErrorHandlingResource, CSPResource):
try:
if self.checkSolution(request) is True:
+ metrix.recordValidHTTPSRequest(request)
return self.resource.render(request)
except ValueError as err:
logging.debug(err.message)
@@ -504,11 +510,14 @@ class CaptchaProtectedResource(CustomErrorHandlingResource, CSPResource):
# work of art" as pennance for their sins.
d = task.deferLater(reactor, 1, lambda: request)
d.addCallback(redirectMaliciousRequest)
+ metrix.recordInvalidHTTPSRequest(request)
return NOT_DONE_YET
except Exception as err:
logging.debug(err.message)
+ metrix.recordInvalidHTTPSRequest(request)
return replaceErrorPage(request, err)
+ metrix.recordInvalidHTTPSRequest(request)
logging.debug("Client failed a CAPTCHA; returning redirect to %s"
% request.uri)
return redirectTo(request.uri, request)
@@ -764,10 +773,12 @@ class ReCaptchaProtectedResource(CaptchaProtectedResource):
# breaking). Hence, the 'no cover' pragma.
if solution.is_valid: # pragma: no cover
logging.info("Valid CAPTCHA solution from %r." % clientIP)
+ metrix.recordValidHTTPSRequest(request)
return (True, request)
else:
logging.info("Invalid CAPTCHA solution from %r: %r"
% (clientIP, solution.error_code))
+ metrix.recordInvalidHTTPSRequest(request)
return (False, request)
d = txrecaptcha.submit(challenge, response, self.secretKey,
diff --git a/bridgedb/distributors/moat/server.py b/bridgedb/distributors/moat/server.py
index 509d471..73d2423 100644
--- a/bridgedb/distributors/moat/server.py
+++ b/bridgedb/distributors/moat/server.py
@@ -38,6 +38,7 @@ from twisted.internet.error import CannotListenError
from twisted.web import resource
from twisted.web.server import Site
+from bridgedb import metrics
from bridgedb import captcha
from bridgedb import crypto
from bridgedb.distributors.common.http import setFQDN
@@ -49,6 +50,10 @@ from bridgedb.schedule import Unscheduled
from bridgedb.schedule import ScheduledInterval
from bridgedb.util import replaceControlChars
+# We use our metrics singleton to keep track of BridgeDB metrics such as
+# "number of failed HTTPS bridge requests."
+metrix = metrics.MoatMetrics()
+
#: The current version of the moat JSON API that we speak
MOAT_API_VERSION = '0.1.0'
@@ -681,6 +686,8 @@ class CaptchaCheckResource(CaptchaResource):
error = self.checkRequestHeaders(request)
if error: # pragma: no cover
+ logging.debug("Error while checking moat request headers.")
+ metrix.recordInvalidMoatRequest(request)
return error.render(request)
data = {
@@ -694,7 +701,11 @@ class CaptchaCheckResource(CaptchaResource):
}
try:
+ pos = request.content.tell()
encoded_client_data = request.content.read()
+ # We rewind the stream to its previous position to allow the
+ # metrix module to read the request's content too.
+ request.content.seek(pos)
client_data = json.loads(encoded_client_data)["data"][0]
clientIP = self.getClientIP(request)
@@ -704,16 +715,19 @@ class CaptchaCheckResource(CaptchaResource):
valid = self.checkSolution(challenge, solution, clientIP)
except captcha.CaptchaExpired:
logging.debug("The challenge had timed out")
+ metrix.recordInvalidMoatRequest(request)
return self.failureResponse(5, request)
except Exception as impossible:
logging.warn("Unhandled exception while processing a POST /fetch request!")
logging.error(impossible)
+ metrix.recordInvalidMoatRequest(request)
return self.failureResponse(4, request)
if valid:
qrcode = None
bridgeRequest = self.createBridgeRequest(clientIP, client_data)
bridgeLines = self.getBridgeLines(bridgeRequest)
+ metrix.recordValidMoatRequest(request)
# If we can only return less than the configured
# MOAT_BRIDGES_PER_ANSWER then log a warning.
@@ -736,6 +750,7 @@ class CaptchaCheckResource(CaptchaResource):
return self.formatDataForResponse(data, request)
else:
+ metrix.recordInvalidMoatRequest(request)
return self.failureResponse(4, request)
diff --git a/bridgedb/main.py b/bridgedb/main.py
index 4d1d38a..6b99127 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -25,6 +25,7 @@ from bridgedb import persistent
from bridgedb import proxy
from bridgedb import runner
from bridgedb import util
+from bridgedb import metrics
from bridgedb.bridges import MalformedBridgeInfo
from bridgedb.bridges import MissingServerDescriptorDigest
from bridgedb.bridges import ServerDescriptorDigestMismatch
@@ -72,6 +73,22 @@ def writeAssignments(hashring, filename):
except IOError:
logging.info("I/O error while writing assignments to: '%s'" % filename)
+def writeMetrics(filename, measurementInterval):
+ """Dump usage metrics to disk.
+
+ :param str filename: The filename to write the metrics to.
+ :param int measurementInterval: The number of seconds after which we rotate
+ and dump our metrics.
+ """
+
+ logging.debug("Dumping metrics to file: '%s'" % filename)
+
+ try:
+ with open(filename, 'a') as fh:
+ metrics.export(fh, measurementInterval)
+ except IOError as err:
+ logging.error("Failed to write metrics to '%s': %s" % (filename, err))
+
def load(state, hashring, clear=False):
"""Read and parse all descriptors, and load into a bridge hashring.
@@ -398,6 +415,7 @@ def run(options, reactor=reactor):
for proxyfile in cfg.PROXY_LIST_FILES:
logging.info("Loading proxies from: %s" % proxyfile)
proxy.loadProxiesFromFile(proxyfile, proxies, removeStale=True)
+ metrics.setProxies(proxies)
logging.info("Reparsing bridge descriptors...")
(hashring,
@@ -463,6 +481,8 @@ def run(options, reactor=reactor):
if config.EMAIL_DIST and config.EMAIL_SHARE:
addSMTPServer(config, emailDistributor)
+ metrics.setSupportedTransports(config.SUPPORTED_TRANSPORTS)
+
tasks = {}
# Setup all our repeating tasks:
@@ -483,14 +503,19 @@ def run(options, reactor=reactor):
runner.cleanupUnparseableDescriptors,
os.path.dirname(config.STATUS_FILE), delUnparseableSecs)
+ measurementInterval, _ = config.TASKS['EXPORT_METRICS']
+ tasks['EXPORT_METRICS'] = task.LoopingCall(
+ writeMetrics, state.METRICS_FILE, measurementInterval)
+
# Schedule all configured repeating tasks:
- for name, seconds in config.TASKS.items():
+ for name, value in config.TASKS.items():
+ seconds, startNow = value
if seconds:
try:
# Set now to False to get the servers up and running when
# first started, rather than spend a bunch of time in
# scheduled tasks.
- tasks[name].start(abs(seconds), now=False)
+ tasks[name].start(abs(seconds), now=startNow)
except KeyError:
logging.info("Task %s is disabled and will not run." % name)
else:
diff --git a/bridgedb/metrics.py b/bridgedb/metrics.py
new file mode 100644
index 0000000..4e1c880
--- /dev/null
+++ b/bridgedb/metrics.py
@@ -0,0 +1,461 @@
+# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_metrics ; -*-
+# _____________________________________________________________________________
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: please see included AUTHORS file
+# :copyright: (c) 2019, The Tor Project, Inc.
+# (c) 2019, Philipp Winter
+# :license: see LICENSE for licensing information
+# _____________________________________________________________________________
+
+"""API for keeping track of BridgeDB statistics, e.g., the demand for bridges
+over time.
+"""
+
+import logging
+import ipaddr
+import operator
+import json
+import datetime
+
+from bridgedb import geo
+from bridgedb.distributors.common.http import getClientIP
+from bridgedb.distributors.email import request
+from bridgedb.distributors.email.distributor import EmailRequestedHelp
+
+from twisted.mail.smtp import Address
+
+# Our data structure to keep track of exit relays. The variable is of type
+# bridgedb.proxy.ProxySet. We reserve a special country code (determined by
+# PROXY_CC below) for exit relays and other proxies.
+PROXIES = None
+
+# Our custom country code for IP addresses that we couldn't map to a country.
+# This can happen for private IP addresses or if our geo-location provider has
+# no mapping.
+UNKNOWN_CC = "??"
+
+# Our custom country code for IP addresses that are proxies, e.g., Tor exit
+# relays. The code "zz" is free for assignment for user needs as specified
+# here: <https://en.wikipedia.org/w/index.php?title=ISO_3166-1_alpha-2&oldid=9066112…>
+PROXY_CC = "ZZ"
+
+# We use BIN_SIZE to reduce the granularity of our counters. We round up
+# numbers to the next multiple of BIN_SIZE, e.g., 28 is rounded up to:
+# 10 * 3 = 30.
+BIN_SIZE = 10
+
+# The prefix length that we use to keep track of the number of unique subnets
+# we have seen HTTPS requests from.
+SUBNET_CTR_PREFIX_LEN = 20
+
+# All of the pluggable transports BridgeDB currently supports.
+SUPPORTED_TRANSPORTS = None
+
+# Major and minor version number for our statistics format.
+METRICS_MAJOR_VERSION = 1
+METRICS_MINOR_VERSION = 0
+
+
+def setProxies(proxies):
+ """Set the given proxies.
+
+ :type proxies: :class:`~bridgedb.proxy.ProxySet`
+ :param proxies: The container for the IP addresses of any currently
+ known open proxies.
+ """
+ logging.debug("Setting %d proxies." % len(proxies))
+ global PROXIES
+ PROXIES = proxies
+
+
+def setSupportedTransports(supportedTransports):
+ """Set the given supported transports.
+
+ :param dict supportedTransports: The transport types that BridgeDB
+ currently supports.
+ """
+
+ logging.debug("Setting %d supported transports." %
+ len(supportedTransports))
+ global SUPPORTED_TRANSPORTS
+ SUPPORTED_TRANSPORTS = supportedTransports
+
+
+def isTransportSupported(transport):
+ """Return `True' if the given transport is supported or `False' otherwise.
+
+ :param str transport: The transport protocol.
+ """
+
+ if SUPPORTED_TRANSPORTS is None:
+ logging.error("Bug: Variable SUPPORTED_TRANSPORTS is None.")
+ return False
+
+ return transport in SUPPORTED_TRANSPORTS
+
+
+def export(fh, measurementInterval):
+ """Export metrics by writing them to the given file handle.
+
+ :param file fh: The file handle to which we're writing our metrics.
+ :param int measurementInterval: The number of seconds after which we rotate
+ and dump our metrics.
+ """
+
+ httpsMetrix = HTTPSMetrics()
+ emailMetrix = EmailMetrics()
+ moatMetrix = MoatMetrics()
+
+ # Rotate our metrics.
+ httpsMetrix.rotate()
+ emailMetrix.rotate()
+ moatMetrix.rotate()
+
+ numProxies = len(PROXIES) if PROXIES is not None else 0
+ if numProxies == 0:
+ logging.error("Metrics module doesn't have any proxies.")
+ else:
+ logging.debug("Metrics module knows about %d proxies." % numProxies)
+
+ now = datetime.datetime.utcnow()
+ fh.write("bridgedb-stats-end %s (%d s)\n" % (
+ now.strftime("%Y-%m-%d %H:%M:%S"),
+ measurementInterval))
+ fh.write("bridgedb-stats-version %d.%d\n" % (METRICS_MAJOR_VERSION,
+ METRICS_MINOR_VERSION))
+
+ httpsLines = httpsMetrix.getMetrics()
+ for line in httpsLines:
+ fh.write("bridgedb-metric-count %s\n" % line)
+
+ moatLines = moatMetrix.getMetrics()
+ for line in moatLines:
+ fh.write("bridgedb-metric-count %s\n" % line)
+
+ emailLines = emailMetrix.getMetrics()
+ for line in emailLines:
+ fh.write("bridgedb-metric-count %s\n" % line)
+
+
+def resolveCountryCode(ipAddr):
+ """Return the country code of the given IP address.
+
+ :param str ipAddr: The IP address to resolve.
+
+ :rtype: str
+ :returns: A two-letter country code.
+ """
+
+ if ipAddr is None:
+ logging.warning("Given IP address was None. Using %s as country "
+ "code." % UNKNOWN_CC)
+ return UNKNOWN_CC
+
+ if PROXIES is None:
+ logging.warning("Proxies are not yet set.")
+ elif ipAddr in PROXIES:
+ return PROXY_CC
+
+ countryCode = geo.getCountryCode(ipaddr.IPAddress(ipAddr))
+
+ # countryCode may be None if GeoIP is unable to map an IP address to a
+ # country.
+ return UNKNOWN_CC if countryCode is None else countryCode
+
+
+class Singleton(type):
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super(Singleton, cls).__call__(*args,
+ **kwargs)
+ return cls._instances[cls]
+
+ def clear(cls):
+ """Drop the instance (necessary for unit tests)."""
+ try:
+ del cls._instances[cls]
+ except KeyError:
+ pass
+
+
+class Metrics(object):
+ """Base class representing metrics.
+
+ This class provides functionality that our three distribution mechanisms
+ share.
+ """
+
+ # We're using a meta class to implement a singleton for Metrics.
+ __metaclass__ = Singleton
+
+ def __init__(self, binSize=BIN_SIZE):
+ logging.debug("Instantiating metrics class.")
+ self.binSize = binSize
+
+ # Metrics cover a 24 hour period. To that end, we're maintaining two
+ # data structures: our "hot" metrics are currently being populated
+ # while our "cold" metrics are finished, and valid for 24 hours. After
+ # that, our hot metrics turn into cold metrics, and we start over.
+ self.hotMetrics = dict()
+ self.coldMetrics = dict()
+
+ def rotate(self):
+ """Rotate our metrics."""
+
+ self.coldMetrics = self.hotMetrics
+ self.hotMetrics = dict()
+
+ def findAnomaly(self, request):
+ anomaly = "none"
+
+ # TODO: Inspect email for traces of bots, Sherlock Homes-style!
+ # See <https://bugs.torproject.org/9316#comment:19> for the rationale.
+ # All classes that inherit from Metrics() should implement this method.
+
+ return anomaly
+
+ def getMetrics(self):
+ """Get our sanitized current metrics, one per line.
+
+ Metrics are of the form:
+
+ [
+ "moat.obfs4.us.success.none 10",
+ "https.vanilla.de.success.none 30",
+ ...
+ ]
+
+ :rtype: list
+ :returns: A list of metric lines.
+ """
+ lines = []
+ for key, value in self.coldMetrics.iteritems():
+ # Round up our value to the nearest multiple of self.binSize to
+ # reduce the accuracy of our real values.
+ if (value % self.binSize) > 0:
+ value += self.binSize - (value % self.binSize)
+ lines.append("%s %d" % (key, value))
+ return lines
+
+ def set(self, key, value):
+ """Set the given key to the given value.
+
+ :param str key: The time series key.
+ :param int value: The time series value.
+ """
+ self.hotMetrics[key] = value
+
+ def inc(self, key):
+ """Increment the given key.
+
+ :param str key: The time series key.
+ """
+ if key in self.hotMetrics:
+ self.hotMetrics[key] += 1
+ else:
+ self.set(key, 1)
+
+ def createKey(self, distMechanism, bridgeType, countryOrProvider,
+ success, anomaly):
+ """Create and return a time series key.
+
+ :param str distMechanism: A string representing our distribution
+ mechanism, e.g., "https".
+ :param str bridgeType: A string representing the requested bridge
+ type, e.g., "vanilla" or "obfs4".
+ :param str countryOrProvider: A string representing the client's
+ two-letter country code or email provider, e.g., "it" or
+ "yahoo.com".
+ :param bool success: ``True`` if the request was successful and
+ BridgeDB handed out a bridge; ``False`` otherwise.
+ :param str anomaly: ``None`` if the request was not anomalous and hence
+ believed to have come from a real user; otherwise a string
+ representing the type of anomaly.
+ :rtype: str
+ :returns: A key that uniquely identifies the given metrics
+ combinations.
+ """
+
+ countryOrProvider = countryOrProvider.lower()
+ bridgeType = bridgeType.lower()
+ success = "success" if success else "fail"
+
+ key = "%s.%s.%s.%s.%s" % (distMechanism, bridgeType,
+ countryOrProvider, success, anomaly)
+
+ return key
+
+
+class HTTPSMetrics(Metrics):
+
+ def __init__(self):
+ super(HTTPSMetrics, self).__init__()
+
+ # Maps subnets (e.g., "1.2.0.0/16") to the number of times we've seen
+ # requests from the given subnet.
+ self.subnetCounter = dict()
+ self.keyPrefix = "https"
+
+ def getTopNSubnets(self, n=10):
+
+ sortedByNum = sorted(self.subnetCounter.items(),
+ key=operator.itemgetter(1),
+ reverse=True)
+ return sortedByNum[:n]
+
+ def _recordHTTPSRequest(self, request, success):
+
+ logging.debug("HTTPS request has user agent: %s" %
+ request.requestHeaders.getRawHeaders("User-Agent"))
+
+ # Pull the client's IP address out of the request and convert it to a
+ # two-letter country code.
+ ipAddr = getClientIP(request,
+ useForwardedHeader=True,
+ skipLoopback=False)
+ self.updateSubnetCounter(ipAddr)
+ countryCode = resolveCountryCode(ipAddr)
+
+ transports = request.args.get("transport", list())
+ if len(transports) > 1:
+ logging.warning("Expected a maximum of one transport but %d are "
+ "given." % len(transports))
+
+ if len(transports) == 0:
+ bridgeType = "vanilla"
+ elif transports[0] == "" or transports[0] == "0":
+ bridgeType = "vanilla"
+ else:
+ bridgeType = transports[0]
+
+ # BridgeDB's HTTPS interface exposes transport types as a drop down
+ # menu but users can still request anything by manipulating HTTP
+ # parameters.
+ if not isTransportSupported(bridgeType):
+ logging.warning("User requested unsupported transport type %s "
+ "over HTTPS." % bridgeType)
+ return
+
+ logging.debug("Recording %svalid HTTPS request for %s from %s (%s)." %
+ ("" if success else "in",
+ bridgeType, ipAddr, countryCode))
+
+ # Now update our metrics.
+ key = self.createKey(self.keyPrefix, bridgeType, countryCode,
+ success, self.findAnomaly(request))
+ self.inc(key)
+
+ def recordValidHTTPSRequest(self, request):
+ self._recordHTTPSRequest(request, True)
+
+ def recordInvalidHTTPSRequest(self, request):
+ self._recordHTTPSRequest(request, False)
+
+ def updateSubnetCounter(self, ipAddr):
+
+ if ipAddr is None:
+ return
+
+ nw = ipaddr.IPNetwork(ipAddr + "/" + str(SUBNET_CTR_PREFIX_LEN),
+ strict=False)
+ subnet = nw.network.compressed
+ logging.debug("Updating subnet counter with %s" % subnet)
+
+ num = self.subnetCounter.get(subnet, 0)
+ self.subnetCounter[subnet] = num + 1
+
+
+class EmailMetrics(Metrics):
+
+ def __init__(self):
+ super(EmailMetrics, self).__init__()
+ self.keyPrefix = "email"
+
+ def _recordEmailRequest(self, smtpAutoresp, success):
+
+ emailAddrs = smtpAutoresp.getMailTo()
+ if len(emailAddrs) == 0:
+ # This is just for unit tests.
+ emailAddr = Address("foo(a)gmail.com")
+ else:
+ emailAddr = emailAddrs[0]
+
+ # Get the requested transport protocol.
+ try:
+ br = request.determineBridgeRequestOptions(
+ smtpAutoresp.incoming.lines)
+ except EmailRequestedHelp:
+ return
+ bridgeType = "vanilla" if not len(br.transports) else br.transports[0]
+
+ # Over email, transports are requested by typing them. Typos happen
+ # and users can request anything, really.
+ if not isTransportSupported(bridgeType):
+ logging.warning("User requested unsupported transport type %s "
+ "over email." % bridgeType)
+ return
+
+ logging.debug("Recording %svalid email request for %s from %s." %
+ ("" if success else "in", bridgeType, emailAddr))
+ sld = emailAddr.domain.split(".")[0]
+
+ # Now update our metrics.
+ key = self.createKey(self.keyPrefix, bridgeType, sld, success,
+ self.findAnomaly(request))
+ self.inc(key)
+
+ def recordValidEmailRequest(self, smtpAutoresp):
+ self._recordEmailRequest(smtpAutoresp, True)
+
+ def recordInvalidEmailRequest(self, smtpAutoresp):
+ self._recordEmailRequest(smtpAutoresp, False)
+
+
+class MoatMetrics(Metrics):
+
+ def __init__(self):
+ super(MoatMetrics, self).__init__()
+ self.keyPrefix = "moat"
+
+ def _recordMoatRequest(self, request, success):
+
+ logging.debug("Moat request has user agent: %s" %
+ request.requestHeaders.getRawHeaders("User-Agent"))
+
+ ipAddr = getClientIP(request,
+ useForwardedHeader=True,
+ skipLoopback=False)
+ countryCode = resolveCountryCode(ipAddr)
+
+ try:
+ encodedClientData = request.content.read()
+ clientData = json.loads(encodedClientData)["data"][0]
+ transport = clientData["transport"]
+ bridgeType = "vanilla" if not len(transport) else transport
+ except Exception as err:
+ logging.warning("Could not decode request: %s" % err)
+ return
+
+ if not isTransportSupported(bridgeType):
+ logging.warning("User requested unsupported transport type %s "
+ "over moat." % bridgeType)
+ return
+
+ logging.debug("Recording %svalid moat request for %s from %s (%s)." %
+ ("" if success else "in",
+ bridgeType, ipAddr, countryCode))
+
+ # Now update our metrics.
+ key = self.createKey(self.keyPrefix, bridgeType,
+ countryCode, success, self.findAnomaly(request))
+ self.inc(key)
+
+ def recordValidMoatRequest(self, request):
+ self._recordMoatRequest(request, True)
+
+ def recordInvalidMoatRequest(self, request):
+ self._recordMoatRequest(request, False)
diff --git a/bridgedb/test/test_metrics.py b/bridgedb/test/test_metrics.py
new file mode 100644
index 0000000..a870fc2
--- /dev/null
+++ b/bridgedb/test/test_metrics.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_metrics ; -*-
+# _____________________________________________________________________________
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: please see included AUTHORS file
+# :copyright: (c) 2019, The Tor Project, Inc.
+# (c) 2019, Philipp Winter
+# :license: see LICENSE for licensing information
+# _____________________________________________________________________________
+
+"""Unittests for the :mod:`bridgedb.metrics` module.
+
+These tests are meant to ensure that the :mod:`bridgedb.metrics` module is
+functioning as expected.
+"""
+
+import StringIO
+import json
+import os
+
+from bridgedb import metrics
+from bridgedb.test.https_helpers import DummyRequest
+from bridgedb.distributors.email.server import SMTPMessage
+from bridgedb.test.email_helpers import _createMailServerContext
+from bridgedb.test.email_helpers import _createConfig
+from bridgedb.distributors.moat import server
+
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+class StateTest(unittest.TestCase):
+
+ def setUp(self):
+ self.topDir = os.getcwd().rstrip('_trial_temp')
+ self.captchaDir = os.path.join(self.topDir, 'captchas')
+
+ # Clear all singletons before each test to prevent cross-test
+ # interference.
+ type(metrics.HTTPSMetrics()).clear()
+ type(metrics.EmailMetrics()).clear()
+ type(metrics.MoatMetrics()).clear()
+
+ metrics.setSupportedTransports({
+ 'obfs2': False,
+ 'obfs3': True,
+ 'obfs4': True,
+ 'scramblesuit': True,
+ 'fte': True,
+ })
+
+ self.metrix = metrics.HTTPSMetrics()
+ self.key = self.metrix.createKey("https", "obfs4", "de", True, None)
+
+ def test_binning(self):
+
+ key = self.metrix.createKey("https", "obfs4", "de", True, None)
+ self.metrix.coldMetrics = self.metrix.hotMetrics
+
+ # A value of 1 should be rounded up to 10.
+ self.metrix.inc(key)
+ metrixLines = self.metrix.getMetrics()
+ key, value = metrixLines[0].split(" ")
+ self.assertTrue(int(value) == 10)
+
+ # A value of 10 should remain 10.
+ self.metrix.set(key, 10)
+ metrixLines = self.metrix.getMetrics()
+ key, value = metrixLines[0].split(" ")
+ self.assertTrue(int(value) == 10)
+
+ # A value of 11 should be rounded up to 20.
+ self.metrix.inc(key)
+ metrixLines = self.metrix.getMetrics()
+ key, value = metrixLines[0].split(" ")
+ self.assertTrue(int(value) == 20)
+
+ def test_key_manipulation(self):
+
+ self.metrix = metrics.HTTPSMetrics()
+ key = self.metrix.createKey("email", "obfs4", "de", True, "none")
+ self.assertTrue(key == "email.obfs4.de.success.none")
+
+ self.metrix.inc(key)
+ self.assertEqual(self.metrix.hotMetrics[key], 1)
+
+ self.metrix.set(key, 10)
+ self.assertEqual(self.metrix.hotMetrics[key], 10)
+
+ def test_rotation(self):
+
+ key = self.metrix.createKey("moat", "obfs4", "de", True, "none")
+ self.metrix.inc(key)
+ oldHotMetrics = self.metrix.hotMetrics
+ self.metrix.rotate()
+
+ self.assertEqual(len(self.metrix.coldMetrics), 1)
+ self.assertEqual(len(self.metrix.hotMetrics), 0)
+ self.assertEqual(self.metrix.coldMetrics, oldHotMetrics)
+
+ def test_export(self):
+
+ self.metrix.inc(self.key)
+
+ self.metrix.coldMetrics = self.metrix.hotMetrics
+ pseudo_fh = StringIO.StringIO()
+ metrics.export(pseudo_fh, 0)
+
+ self.assertTrue(len(pseudo_fh.getvalue()) > 0)
+
+ lines = pseudo_fh.getvalue().split("\n")
+ self.assertTrue(lines[0].startswith("bridgedb-stats-end"))
+ self.assertTrue(lines[1].startswith("bridgedb-stats-version"))
+ self.assertTrue(lines[2] ==
+ "bridgedb-metric-count https.obfs4.de.success.None 10")
+
+ def test_https_metrics(self):
+
+ origFunc = metrics.resolveCountryCode
+ metrics.resolveCountryCode = lambda _: "US"
+
+ key1 = "https.obfs4.us.success.none"
+ req1 = DummyRequest([b"bridges?transport=obfs4"])
+ # We have to set the request args manually when using a DummyRequest.
+ req1.args.update({'transport': ['obfs4']})
+ req1.getClientIP = lambda: "3.3.3.3"
+
+ self.metrix.recordValidHTTPSRequest(req1)
+ self.assertTrue(self.metrix.hotMetrics[key1] == 1)
+
+ key2 = "https.obfs4.us.fail.none"
+ req2 = DummyRequest([b"bridges?transport=obfs4"])
+ # We have to set the request args manually when using a DummyRequest.
+ req2.args.update({'transport': ['obfs4']})
+ req2.getClientIP = lambda: "3.3.3.3"
+ self.metrix.recordInvalidHTTPSRequest(req2)
+ self.assertTrue(self.metrix.hotMetrics[key2] == 1)
+
+ metrics.resolveCountryCode = origFunc
+
+ def test_email_metrics(self):
+
+ config = _createConfig()
+ context = _createMailServerContext(config)
+ message = SMTPMessage(context)
+ message.lines = [
+ "From: foo(a)gmail.com",
+ "To: bridges(a)torproject.org",
+ "Subject: testing",
+ "",
+ "get transport obfs4",
+ ]
+
+ message.message = message.getIncomingMessage()
+ responder = message.responder
+ tr = proto_helpers.StringTransportWithDisconnection()
+ tr.protocol = responder
+ responder.makeConnection(tr)
+
+ email_metrix = metrics.EmailMetrics()
+
+ key1 = "email.obfs4.gmail.success.none"
+ email_metrix.recordValidEmailRequest(responder)
+ self.assertTrue(email_metrix.hotMetrics[key1] == 1)
+
+ key2 = "email.obfs4.gmail.fail.none"
+ email_metrix.recordInvalidEmailRequest(responder)
+ self.assertTrue(email_metrix.hotMetrics[key2] == 1)
+
+ def test_moat_metrics(self):
+
+ def create_moat_request():
+ encoded_data = json.dumps({
+ 'data': [{
+ 'id': '2',
+ 'type': 'moat-solution',
+ 'version': server.MOAT_API_VERSION,
+ 'transport': 'obfs4',
+ 'solution': 'Tvx74PMy',
+ 'qrcode': False,
+ }]
+ })
+
+ request = DummyRequest(["fetch"])
+ request.requestHeaders.addRawHeader('Content-Type',
+ 'application/vnd.api+json')
+ request.requestHeaders.addRawHeader('Accept',
+ 'application/vnd.api+json')
+ request.requestHeaders.addRawHeader('X-Forwarded-For', '3.3.3.3')
+ request.headers['X-Forwarded-For'.lower()] = '3.3.3.3'
+ request.method = b'POST'
+ request.writeContent(encoded_data)
+
+ return request
+
+ metrix = metrics.MoatMetrics()
+ metrix.recordValidMoatRequest(create_moat_request())
+ metrix.recordInvalidMoatRequest(create_moat_request())
+
+ key1 = "moat.obfs4.us.success.none"
+ key2 = "moat.obfs4.us.fail.none"
+ self.assertTrue(metrix.hotMetrics[key1] == 1)
+ self.assertTrue(metrix.hotMetrics[key2] == 1)
1
0
commit 4e5a50f2b54db62991e4ce3313aa9b7f92a1c573
Author: Arlo Breault <arlolra(a)gmail.com>
Date: Wed Aug 14 13:45:15 2019 -0400
Start localization
Trac 30310
---
.gitignore | 1 +
proxy/init-badge.js | 53 ++++++++++++++++++++++++-------
proxy/make.js | 7 ++--
proxy/static/_locales/en_US/messages.json | 32 +++++++++++++++++++
proxy/static/embed.html | 6 ++--
proxy/static/index.html | 2 +-
proxy/static/popup.js | 12 +++++++
proxy/webext/embed.js | 25 +++++++++++----
proxy/webext/manifest.json | 3 +-
9 files changed, 115 insertions(+), 26 deletions(-)
diff --git a/.gitignore b/.gitignore
index f3af78e..1bae622 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,5 +19,6 @@ proxy/webext/popup.js
proxy/webext/embed.html
proxy/webext/embed.css
proxy/webext/assets/
+proxy/webext/_locales/
ignore/
npm-debug.log
diff --git a/proxy/init-badge.js b/proxy/init-badge.js
index dbe7fea..b906f62 100644
--- a/proxy/init-badge.js
+++ b/proxy/init-badge.js
@@ -4,6 +4,20 @@
UI
*/
+class Messages {
+ constructor(json) {
+ this.json = json;
+ }
+ getMessage(m, ...rest) {
+ let message = this.json[m].message;
+ return message.replace(/\$(\d+)/g, (...args) => {
+ return rest[Number(args[1]) - 1];
+ });
+ }
+}
+
+let messages = null;
+
class BadgeUI extends UI {
constructor() {
@@ -16,7 +30,7 @@ class BadgeUI extends UI {
missingFeature(missing) {
this.popup.setEnabled(false);
this.popup.setActive(false);
- this.popup.setStatusText("Snowflake is off");
+ this.popup.setStatusText(messages.getMessage('popupStatusOff'));
this.popup.setStatusDesc(missing, true);
this.popup.hideButton();
}
@@ -24,20 +38,23 @@ class BadgeUI extends UI {
turnOn() {
const clients = this.active ? 1 : 0;
this.popup.setChecked(true);
- this.popup.setToggleText('Turn Off');
- this.popup.setStatusText(`${clients} client${(clients !== 1) ? 's' : ''} connected.`);
+ this.popup.setToggleText(messages.getMessage('popupTurnOff'));
+ if (clients > 0) {
+ this.popup.setStatusText(messages.getMessage('popupStatusOn', String(clients)));
+ } else {
+ this.popup.setStatusText(messages.getMessage('popupStatusReady'));
+ }
// FIXME: Share stats from webext
- const total = 0;
- this.popup.setStatusDesc(`Your snowflake has helped ${total} user${(total !== 1) ? 's' : ''} circumvent censorship in the last 24 hours.`);
+ this.popup.setStatusDesc('');
this.popup.setEnabled(true);
this.popup.setActive(this.active);
}
turnOff() {
this.popup.setChecked(false);
- this.popup.setToggleText('Turn On');
- this.popup.setStatusText("Snowflake is off");
- this.popup.setStatusDesc("");
+ this.popup.setToggleText(messages.getMessage('popupTurnOn'));
+ this.popup.setStatusText(messages.getMessage('popupStatusOff'));
+ this.popup.setStatusDesc('');
this.popup.setEnabled(false);
this.popup.setActive(false);
}
@@ -108,12 +125,12 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific
ui = new BadgeUI();
if (!Util.hasWebRTC()) {
- ui.missingFeature("WebRTC feature is not detected.");
+ ui.missingFeature(messages.getMessage('popupWebRTCOff'));
return;
}
if (!Util.hasCookies()) {
- ui.missingFeature("Cookies are not enabled.");
+ ui.missingFeature(messages.getMessage('badgeCookiesOff'));
return;
}
@@ -153,6 +170,20 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific
return null;
};
- window.onload = init;
+ window.onload = function() {
+ const lang = 'en_US';
+ fetch(`./_locales/${lang}/messages.json`)
+ .then((res) => {
+ if (!res.ok) { return; }
+ return res.json();
+ })
+ .then((json) => {
+ messages = new Messages(json);
+ Popup.fill(document.body, (m) => {
+ return messages.getMessage(m);
+ });
+ init();
+ });
+ }
}());
diff --git a/proxy/make.js b/proxy/make.js
index 59165b2..58b7fce 100755
--- a/proxy/make.js
+++ b/proxy/make.js
@@ -32,7 +32,8 @@ var SHARED_FILES = [
'embed.html',
'embed.css',
'popup.js',
- 'assets'
+ 'assets',
+ '_locales',
];
var concatJS = function(outDir, init, outFile) {
@@ -67,7 +68,7 @@ task('test', 'snowflake unit tests', function() {
});
task('build', 'build the snowflake proxy', function() {
- execSync('rm -r build');
+ execSync('rm -rf build');
execSync('cp -r ' + STATIC + '/ build/');
concatJS('build', 'badge', 'embed.js');
console.log('Snowflake prepared.');
@@ -87,7 +88,7 @@ task('node', 'build the node binary', function() {
});
task('clean', 'remove all built files', function() {
- execSync('rm -r build test spec/support');
+ execSync('rm -rf build test spec/support');
});
var cmd = process.argv[2];
diff --git a/proxy/static/_locales/en_US/messages.json b/proxy/static/_locales/en_US/messages.json
new file mode 100644
index 0000000..f9de9d4
--- /dev/null
+++ b/proxy/static/_locales/en_US/messages.json
@@ -0,0 +1,32 @@
+{
+ "appDesc": {
+ "message": "Snowflake is a WebRTC pluggable transport for Tor."
+ },
+ "popupTurnOn": {
+ "message": "Turn On"
+ },
+ "popupTurnOff": {
+ "message": "Turn Off"
+ },
+ "popupLearnMore": {
+ "message": "Learn more"
+ },
+ "popupStatusOff": {
+ "message": "Snowflake is off"
+ },
+ "popupStatusOn": {
+ "message": "Number of users currently connected: $1"
+ },
+ "popupStatusReady": {
+ "message": "Your Snowflake is ready to help users circumvent censorship!"
+ },
+ "popupWebRTCOff": {
+ "message": "WebRTC feature is not detected."
+ },
+ "popupDescOn": {
+ "message": "Number of users your Snowflake has helped circumvent censorship in the last 24 hours: $1"
+ },
+ "badgeCookiesOff": {
+ "message": "Cookies are not enabled."
+ }
+}
diff --git a/proxy/static/embed.html b/proxy/static/embed.html
index 441241a..eb75c30 100644
--- a/proxy/static/embed.html
+++ b/proxy/static/embed.html
@@ -11,18 +11,18 @@
<body>
<div id="active">
<div id="statusimg"></div>
- <p id="statustext">Snowflake is off</p>
+ <p id="statustext">__MSG_popupStatusOff__</p>
<p id="statusdesc"></p>
</div>
<div class="b button">
- <label id="toggle" for="enabled">Turn On</label>
+ <label id="toggle" for="enabled">__MSG_popupTurnOn__</label>
<label class="switch">
<input id="enabled" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
<div class="b learn">
- <a target="_blank" href="https://snowflake.torproject.org/">Learn more</a>
+ <a target="_blank" href="https://snowflake.torproject.org/">__MSG_popupLearnMore__</a>
</div>
</body>
</html>
diff --git a/proxy/static/index.html b/proxy/static/index.html
index 20fe5c8..5607e07 100644
--- a/proxy/static/index.html
+++ b/proxy/static/index.html
@@ -86,7 +86,7 @@
<p>Which looks like this:</p>
- <iframe src="embed.html" width="320px" height="200px" frameborder="0" scrolling="no"></iframe>
+ <iframe src="embed.html" width="320px" height="240px" frameborder="0" scrolling="no"></iframe>
</div>
</body>
diff --git a/proxy/static/popup.js b/proxy/static/popup.js
index 9ff8121..c59f842 100644
--- a/proxy/static/popup.js
+++ b/proxy/static/popup.js
@@ -38,4 +38,16 @@ class Popup {
setToggleText(txt) {
document.getElementById('toggle').innerText = txt;
}
+ static fill(n, func) {
+ switch(n.nodeType) {
+ case 3: { // Node.TEXT_NODE
+ const m = /^__MSG_([^_]*)__$/.exec(n.nodeValue);
+ if (m) { n.nodeValue = func(m[1]); }
+ break;
+ }
+ case 1: // Node.ELEMENT_NODE
+ n.childNodes.forEach(c => Popup.fill(c, func));
+ break;
+ }
+ }
}
diff --git a/proxy/webext/embed.js b/proxy/webext/embed.js
index 62c97a5..7e0dac9 100644
--- a/proxy/webext/embed.js
+++ b/proxy/webext/embed.js
@@ -1,5 +1,12 @@
/* global chrome, Popup */
+// Fill i18n in HTML
+window.onload = () => {
+ Popup.fill(document.body, (m) => {
+ return chrome.i18n.getMessage(m);
+ });
+};
+
const port = chrome.runtime.connect({
name: "popup"
});
@@ -11,8 +18,8 @@ port.onMessage.addListener((m) => {
if (missingFeature) {
popup.setEnabled(false);
popup.setActive(false);
- popup.setStatusText("Snowflake is off");
- popup.setStatusDesc("WebRTC feature is not detected.", true);
+ popup.setStatusText(chrome.i18n.getMessage('popupStatusOff'));
+ popup.setStatusDesc(chrome.i18n.getMessage('popupWebRTCOff'), true);
popup.hideButton();
return;
}
@@ -21,13 +28,17 @@ port.onMessage.addListener((m) => {
if (enabled) {
popup.setChecked(true);
- popup.setToggleText('Turn Off');
- popup.setStatusText(`${clients} client${(clients !== 1) ? 's' : ''} connected.`);
- popup.setStatusDesc(`Your snowflake has helped ${total} user${(total !== 1) ? 's' : ''} circumvent censorship in the last 24 hours.`);
+ popup.setToggleText(chrome.i18n.getMessage('popupTurnOff'));
+ if (clients > 0) {
+ popup.setStatusText(chrome.i18n.getMessage('popupStatusOn', String(clients)));
+ } else {
+ popup.setStatusText(chrome.i18n.getMessage('popupStatusReady'));
+ }
+ popup.setStatusDesc((total > 0) ? chrome.i18n.getMessage('popupDescOn', String(total)) : '');
} else {
popup.setChecked(false);
- popup.setToggleText('Turn On');
- popup.setStatusText("Snowflake is off");
+ popup.setToggleText(chrome.i18n.getMessage('popupTurnOn'));
+ popup.setStatusText(chrome.i18n.getMessage('popupStatusOff'));
popup.setStatusDesc("");
}
popup.setEnabled(enabled);
diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json
index 8863dbb..7317c67 100644
--- a/proxy/webext/manifest.json
+++ b/proxy/webext/manifest.json
@@ -2,7 +2,8 @@
"manifest_version": 2,
"name": "Snowflake",
"version": "0.0.9",
- "description": "Snowflake is a WebRTC pluggable transport for Tor.",
+ "description": "__MSG_appDesc__",
+ "default_locale": "en_US",
"background": {
"scripts": ["snowflake.js"],
"persistent": true
1
0
[translation/tor-launcher-properties] Update translations for tor-launcher-properties
by translation@torproject.org 15 Aug '19
by translation@torproject.org 15 Aug '19
15 Aug '19
commit ed40772d0f49e3c467eb635089c294a4620849e1
Author: Translation commit bot <translation(a)torproject.org>
Date: Thu Aug 15 20:50:11 2019 +0000
Update translations for tor-launcher-properties
---
nb/torlauncher.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nb/torlauncher.properties b/nb/torlauncher.properties
index af10a76d7..db8b1329f 100644
--- a/nb/torlauncher.properties
+++ b/nb/torlauncher.properties
@@ -67,7 +67,7 @@ torlauncher.bootstrapStatus.loading_status=Laster nettverkstatus
torlauncher.bootstrapStatus.loading_keys=Laster identitetsbekreftende sertifikater
torlauncher.bootstrapStatus.requesting_descriptors=Sender forespørsel om rutingsstafettoppsettsinformasjon
torlauncher.bootstrapStatus.loading_descriptors=Laster inn rutingsstafettoppsetts-informasjon
-torlauncher.bootstrapStatus.enough_dirinfo=Finished loading relay information
+torlauncher.bootstrapStatus.enough_dirinfo=Fullført lasting om relé informasjon
torlauncher.bootstrapStatus.ap_conn_pt=Bygger kretser: Kobler til bro
torlauncher.bootstrapStatus.ap_conn_done_pt=Bygger kretser: Koblet til bro
torlauncher.bootstrapStatus.ap_conn_proxy=Bygger kretser: Kobler til proxy
1
0