summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorintrigeri <intrigeri@boum.org>2015-03-18 08:42:41 +0000
committerintrigeri <intrigeri@boum.org>2015-03-18 08:42:41 +0000
commit13ad525706b83ae6d10efeaf257ea322dd5c8aba (patch)
tree5a20af40b81c6cf334c41015f52a5d4730e4a5f0
parent0550b1cd9dad4da633d38e1ddc93bd48aae9dffd (diff)
parentff9063e75d95029c454b763b4d6298dd802ec022 (diff)
Merge branch 'stable' into feature/8740-new-signing-key-phase-2feature/8740-new-signing-key-phase-2
Conflicts: features/step_definitions/checks.rb
-rwxr-xr-xconfig/chroot_local-hooks/19-install-tor-browser-AppArmor-profile18
-rwxr-xr-xconfig/chroot_local-hooks/52-update-rc.d1
-rwxr-xr-xconfig/chroot_local-includes/etc/init.d/tails-autotest-remote-shell75
-rwxr-xr-xconfig/chroot_local-includes/lib/live/config/9999-autotest11
-rwxr-xr-x[-rw-r--r--]config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell (renamed from config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py)6
-rw-r--r--debian/changelog6
-rw-r--r--features/images/PidginAccountManagerAddButton.pngbin0 -> 557 bytes
-rw-r--r--features/images/PidginAddAccountProtocolLabel.pngbin0 -> 2048 bytes
-rw-r--r--features/images/PidginAddAccountProtocolXMPP.pngbin0 -> 1345 bytes
-rw-r--r--features/images/PidginAddAccountWindow.pngbin0 -> 1433 bytes
-rw-r--r--features/images/PidginAddAccountXMPPAddButton.pngbin0 -> 550 bytes
-rw-r--r--features/images/PidginAddAccountXMPPAdvancedTab.pngbin0 -> 1280 bytes
-rw-r--r--features/images/PidginAddAccountXMPPConnectServer.pngbin0 -> 1639 bytes
-rw-r--r--features/images/PidginAddAccountXMPPDomain.pngbin0 -> 1151 bytes
-rw-r--r--features/images/PidginAddAccountXMPPPassword.pngbin0 -> 1489 bytes
-rw-r--r--features/images/PidginAddAccountXMPPRememberPassword.pngbin0 -> 2056 bytes
-rw-r--r--features/images/PidginAddAccountXMPPUsername.pngbin0 -> 1278 bytes
-rw-r--r--features/images/PidginAvailableStatus.pngbin0 -> 1759 bytes
-rw-r--r--features/images/PidginBuddiesMenu.pngbin0 -> 928 bytes
-rw-r--r--features/images/PidginBuddiesMenuJoinChat.pngbin0 -> 1312 bytes
-rw-r--r--features/images/PidginChat1UserInRoom.pngbin0 -> 571 bytes
-rw-r--r--features/images/PidginChat2UsersInRoom.pngbin0 -> 1156 bytes
-rw-r--r--features/images/PidginConversationMenu.pngbin0 -> 1389 bytes
-rw-r--r--features/images/PidginConversationMenuClearScrollback.pngbin0 -> 1931 bytes
-rw-r--r--features/images/PidginConversationOTRMenu.pngbin0 -> 1252 bytes
-rw-r--r--features/images/PidginConversationOTRUnverifiedSessionStarted.pngbin0 -> 622 bytes
-rw-r--r--features/images/PidginConversationWindowMenuBar.pngbin0 -> 3217 bytes
-rw-r--r--features/images/PidginCreateNewRoomAcceptDefaultsButton.pngbin0 -> 1763 bytes
-rw-r--r--features/images/PidginCreateNewRoomPrompt.pngbin0 -> 2218 bytes
-rw-r--r--features/images/PidginFriendExpectedAnswer.pngbin0 -> 733 bytes
-rw-r--r--features/images/PidginFriendOnline.pngbin0 -> 1096 bytes
-rw-r--r--features/images/PidginJoinChatButton.pngbin0 -> 597 bytes
-rw-r--r--features/images/PidginJoinChatRoomLabel.pngbin0 -> 874 bytes
-rw-r--r--features/images/PidginJoinChatServerLabel.pngbin0 -> 993 bytes
-rw-r--r--features/images/PidginJoinChatWindow.pngbin0 -> 1418 bytes
-rw-r--r--features/images/PidginOTRKeyGenPrompt.pngbin0 -> 2084 bytes
-rw-r--r--features/images/PidginOTRKeyGenPromptDoneButton.pngbin0 -> 1156 bytes
-rw-r--r--features/images/PidginOTRMenuStartSession.pngbin0 -> 1709 bytes
-rw-r--r--features/pidgin.feature42
-rwxr-xr-xfeatures/scripts/convertkey.py56
-rwxr-xr-xfeatures/scripts/otr-bot.py197
-rw-r--r--features/step_definitions/checks.rb11
-rw-r--r--features/step_definitions/common_steps.rb2
-rw-r--r--features/step_definitions/pidgin.rb225
-rw-r--r--features/support/config.rb3
-rw-r--r--features/support/helpers/chatbot_helper.rb60
-rw-r--r--features/support/helpers/ctcp_helper.rb125
-rw-r--r--features/support/helpers/misc_helpers.rb16
-rw-r--r--features/support/helpers/sikuli_helper.rb41
-rw-r--r--features/support/helpers/vm_helper.rb6
-rw-r--r--features/support/hooks.rb17
-rwxr-xr-xrun_test_suite49
-rw-r--r--wiki/src/contribute/release_process/test.mdwn30
-rw-r--r--wiki/src/contribute/release_process/test/automated_tests.mdwn9
-rw-r--r--wiki/src/contribute/release_process/test/setup.mdwn6
-rw-r--r--wiki/src/contribute/release_process/test/usage.mdwn50
56 files changed, 979 insertions, 83 deletions
diff --git a/config/chroot_local-hooks/19-install-tor-browser-AppArmor-profile b/config/chroot_local-hooks/19-install-tor-browser-AppArmor-profile
index 91fe2ab..4472f2f 100755
--- a/config/chroot_local-hooks/19-install-tor-browser-AppArmor-profile
+++ b/config/chroot_local-hooks/19-install-tor-browser-AppArmor-profile
@@ -15,13 +15,15 @@ toggle_src_APT_sources() {
case "$MODE" in
on)
- cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list \
- | sed --regexp-extended -e 's,^deb(\s+),deb-src\1,' \
- > "$TEMP_APT_SOURCES"
- ;;
+ cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list \
+ | grep --extended-regexp --line-regexp --invert-match \
+ 'deb\s+file:/root/local-packages\s+\./' \
+ | sed --regexp-extended -e 's,^deb(\s+),deb-src\1,' \
+ > "$TEMP_APT_SOURCES"
+ ;;
off)
- rm "$TEMP_APT_SOURCES"
- ;;
+ rm "$TEMP_APT_SOURCES"
+ ;;
esac
apt-get --yes update
@@ -33,8 +35,8 @@ install_torbrowser_AppArmor_profile() {
cd "$tmpdir"
apt-get source torbrowser-launcher/testing
install -m 0644 \
- torbrowser-launcher-*/apparmor/torbrowser.Browser.firefox \
- "$PROFILE"
+ torbrowser-launcher-*/apparmor/torbrowser.Browser.firefox \
+ "$PROFILE"
)
rm -r "$tmpdir"
}
diff --git a/config/chroot_local-hooks/52-update-rc.d b/config/chroot_local-hooks/52-update-rc.d
index 27f9148..17ea82b 100755
--- a/config/chroot_local-hooks/52-update-rc.d
+++ b/config/chroot_local-hooks/52-update-rc.d
@@ -3,6 +3,7 @@
set -e
CUSTOM_INITSCRIPTS="
+tails-autotest-remote-shell
tails-detect-virtualization
tails-kexec
tails-reconfigure-kexec
diff --git a/config/chroot_local-includes/etc/init.d/tails-autotest-remote-shell b/config/chroot_local-includes/etc/init.d/tails-autotest-remote-shell
new file mode 100755
index 0000000..d035f74
--- /dev/null
+++ b/config/chroot_local-includes/etc/init.d/tails-autotest-remote-shell
@@ -0,0 +1,75 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides: tails-autotest-remote-shell
+# Required-Start: mountkernfs $local_fs
+# Required-Stop:
+# Default-Start: 2 3 4 5
+# Default-Stop:
+# X-Start-Before: $x-display-manager gdm gdm3
+# Short-Description: Remote shell (over serial link) used in Tails test suite
+# Description: Remote shell (over serial link) used in Tails test suite
+### END INIT INFO
+
+# Author: Tails Developers <tails@boum.org>
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
+DESC="Remote shell (over serial link) used in Tails test suite"
+NAME="tails-autotest-remote-shell"
+SCRIPTNAME="/etc/init.d/${NAME}"
+DAEMON="/usr/local/lib/${NAME}"
+DAEMON_ARGS="/dev/ttyS0"
+
+# Exit if not run by Tails automated test suite. The if-construction
+# below may seem silly but we really want to only continue running
+# this script this if the expected kernel command-line option is
+# present. Fail safe, not open, and all that.
+if grep -qw "autotest_never_use_this_option" /proc/cmdline
+then
+ :
+else
+ exit 0
+fi
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+wait_until_remote_shell_is_listening()
+{
+ REMOTE_SHELL_STATE_FILE=/var/lib/live/autotest-remote-shell-running
+ until [ -e "${REMOTE_SHELL_STATE_FILE}" ]; do
+ sleep 1
+ done
+}
+
+do_start()
+{
+ start-stop-daemon \
+ --start \
+ --quiet \
+ --background \
+ --exec ${DAEMON} -- ${DAEMON_ARGS}
+ wait_until_remote_shell_is_listening
+}
+
+case "${1}" in
+ start)
+ [ "${VERBOSE}" != no ] && log_daemon_msg "${DESC}" "${NAME}"
+ do_start
+ [ "${VERBOSE}" != no ] && log_end_msg ${?}
+ ;;
+ restart|reload|stop|force-reload)
+ :
+ ;;
+ *)
+ echo "Usage: ${SCRIPTNAME} start" >&2
+ exit 1
+ ;;
+esac
+
+:
diff --git a/config/chroot_local-includes/lib/live/config/9999-autotest b/config/chroot_local-includes/lib/live/config/9999-autotest
deleted file mode 100755
index 508c8ce..0000000
--- a/config/chroot_local-includes/lib/live/config/9999-autotest
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-SCRIPT=/usr/local/sbin/autotest_remote_shell.py
-
-if grep -qw "autotest_never_use_this_option" /proc/cmdline; then
- # FIXME: more beautiful solution
- sed -i 's/^exit.*$//' /etc/rc.local
- echo "( while true ; do python ${SCRIPT} /dev/ttyS0 ; done ) &" >> \
- /etc/rc.local
- echo "exit 0" >> /etc/rc.local
-fi
diff --git a/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
index 77a5309..958d5bb 100644..100755
--- a/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py
+++ b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
@@ -41,6 +41,12 @@ def main():
dev = argv[1]
port = serial.Serial(port = dev, baudrate = 4000000)
port.open()
+
+ # Create a state file so other applications can know that the remote
+ # shell is operational.
+ state_file_path = "/var/lib/live/autotest-remote-shell-running"
+ open(state_file_path, "w").close()
+
while True:
try:
line = port.readline()
diff --git a/debian/changelog b/debian/changelog
index 3c2dac9..68d7a3f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+tails (1.3.1) UNRELEASED; urgency=medium
+
+ * Dummy entry.
+
+ -- Tails developers <tails@boum.org> Tue, 24 Feb 2015 21:39:38 +0100
+
tails (1.3) unstable; urgency=medium
* Major new features
diff --git a/features/images/PidginAccountManagerAddButton.png b/features/images/PidginAccountManagerAddButton.png
new file mode 100644
index 0000000..ac00ea3
--- /dev/null
+++ b/features/images/PidginAccountManagerAddButton.png
Binary files differ
diff --git a/features/images/PidginAddAccountProtocolLabel.png b/features/images/PidginAddAccountProtocolLabel.png
new file mode 100644
index 0000000..8c680c0
--- /dev/null
+++ b/features/images/PidginAddAccountProtocolLabel.png
Binary files differ
diff --git a/features/images/PidginAddAccountProtocolXMPP.png b/features/images/PidginAddAccountProtocolXMPP.png
new file mode 100644
index 0000000..2d42f0f
--- /dev/null
+++ b/features/images/PidginAddAccountProtocolXMPP.png
Binary files differ
diff --git a/features/images/PidginAddAccountWindow.png b/features/images/PidginAddAccountWindow.png
new file mode 100644
index 0000000..e6dc1f0
--- /dev/null
+++ b/features/images/PidginAddAccountWindow.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPAddButton.png b/features/images/PidginAddAccountXMPPAddButton.png
new file mode 100644
index 0000000..5dc1587
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPAddButton.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPAdvancedTab.png b/features/images/PidginAddAccountXMPPAdvancedTab.png
new file mode 100644
index 0000000..41de626
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPAdvancedTab.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPConnectServer.png b/features/images/PidginAddAccountXMPPConnectServer.png
new file mode 100644
index 0000000..99ebdde
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPConnectServer.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPDomain.png b/features/images/PidginAddAccountXMPPDomain.png
new file mode 100644
index 0000000..02b2568
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPDomain.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPPassword.png b/features/images/PidginAddAccountXMPPPassword.png
new file mode 100644
index 0000000..cb75b23
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPPassword.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPRememberPassword.png b/features/images/PidginAddAccountXMPPRememberPassword.png
new file mode 100644
index 0000000..dd28443
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPRememberPassword.png
Binary files differ
diff --git a/features/images/PidginAddAccountXMPPUsername.png b/features/images/PidginAddAccountXMPPUsername.png
new file mode 100644
index 0000000..a8a5189
--- /dev/null
+++ b/features/images/PidginAddAccountXMPPUsername.png
Binary files differ
diff --git a/features/images/PidginAvailableStatus.png b/features/images/PidginAvailableStatus.png
new file mode 100644
index 0000000..fb9f5b7
--- /dev/null
+++ b/features/images/PidginAvailableStatus.png
Binary files differ
diff --git a/features/images/PidginBuddiesMenu.png b/features/images/PidginBuddiesMenu.png
new file mode 100644
index 0000000..45c936e
--- /dev/null
+++ b/features/images/PidginBuddiesMenu.png
Binary files differ
diff --git a/features/images/PidginBuddiesMenuJoinChat.png b/features/images/PidginBuddiesMenuJoinChat.png
new file mode 100644
index 0000000..34e2f2e
--- /dev/null
+++ b/features/images/PidginBuddiesMenuJoinChat.png
Binary files differ
diff --git a/features/images/PidginChat1UserInRoom.png b/features/images/PidginChat1UserInRoom.png
new file mode 100644
index 0000000..e404554
--- /dev/null
+++ b/features/images/PidginChat1UserInRoom.png
Binary files differ
diff --git a/features/images/PidginChat2UsersInRoom.png b/features/images/PidginChat2UsersInRoom.png
new file mode 100644
index 0000000..cabc208
--- /dev/null
+++ b/features/images/PidginChat2UsersInRoom.png
Binary files differ
diff --git a/features/images/PidginConversationMenu.png b/features/images/PidginConversationMenu.png
new file mode 100644
index 0000000..0643c0c
--- /dev/null
+++ b/features/images/PidginConversationMenu.png
Binary files differ
diff --git a/features/images/PidginConversationMenuClearScrollback.png b/features/images/PidginConversationMenuClearScrollback.png
new file mode 100644
index 0000000..0ac2a11
--- /dev/null
+++ b/features/images/PidginConversationMenuClearScrollback.png
Binary files differ
diff --git a/features/images/PidginConversationOTRMenu.png b/features/images/PidginConversationOTRMenu.png
new file mode 100644
index 0000000..90570f8
--- /dev/null
+++ b/features/images/PidginConversationOTRMenu.png
Binary files differ
diff --git a/features/images/PidginConversationOTRUnverifiedSessionStarted.png b/features/images/PidginConversationOTRUnverifiedSessionStarted.png
new file mode 100644
index 0000000..ef0bd28
--- /dev/null
+++ b/features/images/PidginConversationOTRUnverifiedSessionStarted.png
Binary files differ
diff --git a/features/images/PidginConversationWindowMenuBar.png b/features/images/PidginConversationWindowMenuBar.png
new file mode 100644
index 0000000..0932542
--- /dev/null
+++ b/features/images/PidginConversationWindowMenuBar.png
Binary files differ
diff --git a/features/images/PidginCreateNewRoomAcceptDefaultsButton.png b/features/images/PidginCreateNewRoomAcceptDefaultsButton.png
new file mode 100644
index 0000000..34a90f3
--- /dev/null
+++ b/features/images/PidginCreateNewRoomAcceptDefaultsButton.png
Binary files differ
diff --git a/features/images/PidginCreateNewRoomPrompt.png b/features/images/PidginCreateNewRoomPrompt.png
new file mode 100644
index 0000000..ab0bab8
--- /dev/null
+++ b/features/images/PidginCreateNewRoomPrompt.png
Binary files differ
diff --git a/features/images/PidginFriendExpectedAnswer.png b/features/images/PidginFriendExpectedAnswer.png
new file mode 100644
index 0000000..ddb51a7
--- /dev/null
+++ b/features/images/PidginFriendExpectedAnswer.png
Binary files differ
diff --git a/features/images/PidginFriendOnline.png b/features/images/PidginFriendOnline.png
new file mode 100644
index 0000000..4909606
--- /dev/null
+++ b/features/images/PidginFriendOnline.png
Binary files differ
diff --git a/features/images/PidginJoinChatButton.png b/features/images/PidginJoinChatButton.png
new file mode 100644
index 0000000..e4f0747
--- /dev/null
+++ b/features/images/PidginJoinChatButton.png
Binary files differ
diff --git a/features/images/PidginJoinChatRoomLabel.png b/features/images/PidginJoinChatRoomLabel.png
new file mode 100644
index 0000000..3fbee48
--- /dev/null
+++ b/features/images/PidginJoinChatRoomLabel.png
Binary files differ
diff --git a/features/images/PidginJoinChatServerLabel.png b/features/images/PidginJoinChatServerLabel.png
new file mode 100644
index 0000000..6386617
--- /dev/null
+++ b/features/images/PidginJoinChatServerLabel.png
Binary files differ
diff --git a/features/images/PidginJoinChatWindow.png b/features/images/PidginJoinChatWindow.png
new file mode 100644
index 0000000..82ce19e
--- /dev/null
+++ b/features/images/PidginJoinChatWindow.png
Binary files differ
diff --git a/features/images/PidginOTRKeyGenPrompt.png b/features/images/PidginOTRKeyGenPrompt.png
new file mode 100644
index 0000000..194e044
--- /dev/null
+++ b/features/images/PidginOTRKeyGenPrompt.png
Binary files differ
diff --git a/features/images/PidginOTRKeyGenPromptDoneButton.png b/features/images/PidginOTRKeyGenPromptDoneButton.png
new file mode 100644
index 0000000..c48a953
--- /dev/null
+++ b/features/images/PidginOTRKeyGenPromptDoneButton.png
Binary files differ
diff --git a/features/images/PidginOTRMenuStartSession.png b/features/images/PidginOTRMenuStartSession.png
new file mode 100644
index 0000000..3be7de9
--- /dev/null
+++ b/features/images/PidginOTRMenuStartSession.png
Binary files differ
diff --git a/features/pidgin.feature b/features/pidgin.feature
index 83330ce..27a8034 100644
--- a/features/pidgin.feature
+++ b/features/pidgin.feature
@@ -13,6 +13,47 @@ Feature: Chatting anonymously using Pidgin
Then Pidgin has the expected accounts configured with random nicknames
And I save the state so the background can be restored next scenario
+ @check_tor_leaks
+ Scenario: Chatting with some friend over XMPP
+ When I start Pidgin through the GNOME menu
+ Then I see Pidgin's account manager window
+ When I create my XMPP account
+ And I close Pidgin's account manager window
+ Then Pidgin automatically enables my XMPP account
+ Given my XMPP friend goes online
+ When I start a conversation with my friend
+ And I say something to my friend
+ Then I receive a response from my friend
+
+ @check_tor_leaks
+ Scenario: Chatting with some friend over XMPP in a multi-user chat
+ When I start Pidgin through the GNOME menu
+ Then I see Pidgin's account manager window
+ When I create my XMPP account
+ And I close Pidgin's account manager window
+ Then Pidgin automatically enables my XMPP account
+ When I join some empty multi-user chat
+ And I clear the multi-user chat's scrollback
+ And my XMPP friend goes online and joins the multi-user chat
+ Then I can see that my friend joined the multi-user chat
+ And I say something to my friend in the multi-user chat
+ Then I receive a response from my friend in the multi-user chat
+
+ @check_tor_leaks
+ Scenario: Chatting with some friend over XMPP and with OTR
+ When I start Pidgin through the GNOME menu
+ Then I see Pidgin's account manager window
+ When I create my XMPP account
+ And I close Pidgin's account manager window
+ Then Pidgin automatically enables my XMPP account
+ Given my XMPP friend goes online
+ When I start a conversation with my friend
+ And I start an OTR session with my friend
+ Then Pidgin automatically generates an OTR key
+ And an OTR session was successfully started with my friend
+ When I say something to my friend
+ Then I receive a response from my friend
+
@check_tor_leaks
Scenario: Connecting to the #tails IRC channel with the pre-configured account
When I start Pidgin through the GNOME menu
@@ -26,6 +67,7 @@ Feature: Chatting anonymously using Pidgin
Then I see the Tails roadmap URL
When I click on the Tails roadmap URL
Then the Tor Browser has started and loaded the Tails roadmap
+ And the "irc.oftc.net" account only responds to PING and VERSION CTCP requests
Scenario: Adding a certificate to Pidgin
And I start Pidgin through the GNOME menu
diff --git a/features/scripts/convertkey.py b/features/scripts/convertkey.py
new file mode 100755
index 0000000..ba78675
--- /dev/null
+++ b/features/scripts/convertkey.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+# Copyright 2011 Kjell Braden <afflux@pentabarf.de>
+#
+# This file is part of the python-potr library.
+#
+# python-potr is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# any later version.
+#
+# python-potr is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from potr.compatcrypto.pycrypto import DSAKey
+
+def parse(tokens):
+ key = tokens.pop(0)[1:]
+
+ parsed = {key:{}}
+
+ while tokens:
+ token = tokens.pop(0)
+ if token.endswith(')'):
+ if token[:-1]:
+ val = token[:-1].strip('"')
+ if val.startswith('#') and val.endswith('#'):
+ val = int(val[1:-1], 16)
+ parsed[key] = val
+ return parsed, tokens
+ if token.startswith('('):
+ pdata, tokens = parse([token]+tokens)
+ parsed[key].update(pdata)
+
+ return parsed, []
+
+def convert(path):
+ with open(path, 'r') as f:
+ text = f.read().strip()
+ tokens = text.split()
+ oldkey = parse(tokens)[0]['privkeys']['account']
+
+ k = oldkey['private-key']['dsa']
+ newkey = DSAKey((k['y'],k['g'],k['p'],k['q'],k['x']), private=True)
+ print('Writing converted key for %s/%s to %s' % (oldkey['name'],
+ oldkey['protocol'], path+'2'))
+ with open(path+'3', 'wb') as f:
+ f.write(newkey.serializePrivateKey())
+
+if __name__ == '__main__':
+ import sys
+ convert(sys.argv[1])
diff --git a/features/scripts/otr-bot.py b/features/scripts/otr-bot.py
new file mode 100755
index 0000000..dad41c6
--- /dev/null
+++ b/features/scripts/otr-bot.py
@@ -0,0 +1,197 @@
+#!/usr/bin/python
+import sys
+import jabberbot
+import xmpp
+import potr
+from argparse import ArgumentParser
+
+class OtrContext(potr.context.Context):
+
+ def __init__(self, account, peer):
+ super(OtrContext, self).__init__(account, peer)
+
+ def getPolicy(self, key):
+ return True
+
+ def inject(self, msg, appdata = None):
+ mess = appdata["base_reply"]
+ mess.setBody(msg)
+ appdata["send_raw_message_fn"](mess)
+
+
+class BotAccount(potr.context.Account):
+
+ def __init__(self, jid, keyFilePath):
+ protocol = 'xmpp'
+ max_message_size = 10*1024
+ super(BotAccount, self).__init__(jid, protocol, max_message_size)
+ self.keyFilePath = keyFilePath
+
+ def loadPrivkey(self):
+ with open(self.keyFilePath, 'rb') as keyFile:
+ return potr.crypt.PK.parsePrivateKey(keyFile.read())[0]
+
+
+class OtrContextManager:
+
+ def __init__(self, jid, keyFilePath):
+ self.account = BotAccount(jid, keyFilePath)
+ self.contexts = {}
+
+ def start_context(self, other):
+ if not other in self.contexts:
+ self.contexts[other] = OtrContext(self.account, other)
+ return self.contexts[other]
+
+ def get_context_for_user(self, other):
+ return self.start_context(other)
+
+
+class OtrBot(jabberbot.JabberBot):
+
+ PING_FREQUENCY = 60
+
+ def __init__(self, account, password, otr_key_path, connect_server = None):
+ self.__connect_server = connect_server
+ self.__password = password
+ super(OtrBot, self).__init__(account, password)
+ self.__otr_manager = OtrContextManager(account, otr_key_path)
+ self.send_raw_message_fn = super(OtrBot, self).send_message
+ self.__default_otr_appdata = {
+ "send_raw_message_fn": self.send_raw_message_fn
+ }
+
+ def __otr_appdata_for_mess(self, mess):
+ appdata = self.__default_otr_appdata.copy()
+ appdata["base_reply"] = mess
+ return appdata
+
+ # Unfortunately Jabberbot's connect() is not very friendly to
+ # overriding in subclasses so we have to re-implement it
+ # completely (copy-paste mostly) in order to add support for using
+ # an XMPP "Connect Server".
+ def connect(self):
+ if not self.conn:
+ conn = xmpp.Client(self.jid.getDomain(), debug=[])
+ if self.__connect_server:
+ try:
+ conn_server, conn_port = self.__connect_server.split(":", 1)
+ except ValueError:
+ conn_server = self.__connect_server
+ conn_port = 5222
+ conres = conn.connect((conn_server, int(conn_port)))
+ else:
+ conres = conn.connect()
+ if not conres:
+ return None
+ authres = conn.auth(self.jid.getNode(), self.__password, self.res)
+ if not authres:
+ return None
+ self.conn = conn
+ self.conn.sendInitPresence()
+ self.roster = self.conn.Roster.getRoster()
+ for (handler, callback) in self.handlers:
+ self.conn.RegisterHandler(handler, callback)
+ return self.conn
+
+ # Wrap OTR encryption around Jabberbot's most low-level method for
+ # sending messages.
+ def send_message(self, mess):
+ body = str(mess.getBody())
+ user = str(mess.getTo().getStripped())
+ otrctx = self.__otr_manager.get_context_for_user(user)
+ if otrctx.state == potr.context.STATE_ENCRYPTED:
+ otrctx.sendMessage(potr.context.FRAGMENT_SEND_ALL, body,
+ appdata = self.__otr_appdata_for_mess(mess))
+ else:
+ self.send_raw_message_fn(mess)
+
+ # Wrap OTR decryption around Jabberbot's callback mechanism.
+ def callback_message(self, conn, mess):
+ body = str(mess.getBody())
+ user = str(mess.getFrom().getStripped())
+ otrctx = self.__otr_manager.get_context_for_user(user)
+ if mess.getType() == "chat":
+ try:
+ appdata = self.__otr_appdata_for_mess(mess.buildReply())
+ decrypted_body, tlvs = otrctx.receiveMessage(body,
+ appdata = appdata)
+ otrctx.processTLVs(tlvs)
+ except potr.context.NotEncryptedError:
+ otrctx.authStartV2(appdata = appdata)
+ return
+ except (potr.context.UnencryptedMessage, potr.context.NotOTRMessage):
+ decrypted_body = body
+ else:
+ decrypted_body = body
+ if decrypted_body == None:
+ return
+ if mess.getType() == "groupchat":
+ bot_prefix = self.jid.getNode() + ": "
+ if decrypted_body.startswith(bot_prefix):
+ decrypted_body = decrypted_body[len(bot_prefix):]
+ else:
+ return
+ mess.setBody(decrypted_body)
+ super(OtrBot, self).callback_message(conn, mess)
+
+ # Override Jabberbot quitting on keep alive failure.
+ def on_ping_timeout(self):
+ self.__lastping = None
+
+ @jabberbot.botcmd
+ def ping(self, mess, args):
+ """Why not just test it?"""
+ return "pong"
+
+ @jabberbot.botcmd
+ def say(self, mess, args):
+ """Unleash my inner parrot"""
+ return args
+
+ @jabberbot.botcmd
+ def clear_say(self, mess, args):
+ """Make me speak in the clear even if we're in an OTR chat"""
+ self.send_raw_message_fn(mess.buildReply(args))
+ return ""
+
+ @jabberbot.botcmd
+ def start_otr(self, mess, args):
+ """Make me *initiate* (but not refresh) an OTR session"""
+ if mess.getType() == "groupchat":
+ return
+ return "?OTRv2?"
+
+ @jabberbot.botcmd
+ def end_otr(self, mess, args):
+ """Make me gracefully end the OTR session if there is one"""
+ if mess.getType() == "groupchat":
+ return
+ user = str(mess.getFrom().getStripped())
+ self.__otr_manager.get_context_for_user(user).disconnect(appdata =
+ self.__otr_appdata_for_mess(mess.buildReply()))
+ return ""
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument("account",
+ help = "the user account, given as user@domain")
+ parser.add_argument("password",
+ help = "the user account's password")
+ parser.add_argument("otr_key_path",
+ help = "the path to the account's OTR key file")
+ parser.add_argument("-c", "--connect-server", metavar = 'ADDRESS',
+ help = "use a Connect Server, given as host[:port] " +
+ "(port defaults to 5222)")
+ parser.add_argument("-j", "--auto-join", nargs = '+', metavar = 'ROOMS',
+ help = "auto-join multi-user chatrooms on start")
+ args = parser.parse_args()
+ otr_bot_opt_args = dict()
+ if args.connect_server:
+ otr_bot_opt_args["connect_server"] = args.connect_server
+ otr_bot = OtrBot(args.account, args.password, args.otr_key_path,
+ **otr_bot_opt_args)
+ if args.auto_join:
+ for room in args.auto_join:
+ otr_bot.join_room(room)
+ otr_bot.serve_forever()
diff --git a/features/step_definitions/checks.rb b/features/step_definitions/checks.rb
index 17a1900..2d37a77 100644
--- a/features/step_definitions/checks.rb
+++ b/features/step_definitions/checks.rb
@@ -1,11 +1,12 @@
Then /^the shipped Tails (signing|Debian repository) key will be valid for the next (\d+) months$/ do |key_type, max_months|
next if @skip_steps_while_restoring_background
- if key_type == 'signing'
- sig_key_fingerprint = "A490D0F4D311A4153E2BB7CADBB802B258ACD84F"
+ case key_type
+ when 'signing'
+ sig_key_fingerprint = TAILS_SIGNING_KEY
cmd = 'gpg'
user = LIVE_USER
- elsif key_type == 'Debian repository'
- sig_key_fingerprint = "221F9A3C6FA3E09E182E060BC7988EA7A358D82E"
+ when 'Debian repository'
+ sig_key_fingerprint = TAILS_DEBIAN_REPO_KEY
cmd = 'apt-key adv'
user = 'root'
else
@@ -116,7 +117,7 @@ end
Then /^a screenshot is saved to the live user's home directory$/ do
next if @skip_steps_while_restoring_background
home = "/home/#{LIVE_USER}"
- try_for(3, :msg=> "No screenshot was created in #{home}") {
+ try_for(10, :msg=> "No screenshot was created in #{home}") {
!@vm.execute("find '#{home}' -name 'Screenshot*.png' -maxdepth 1").stdout.empty?
}
end
diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb
index 12cbc9c..9f6d89e 100644
--- a/features/step_definitions/common_steps.rb
+++ b/features/step_definitions/common_steps.rb
@@ -127,6 +127,8 @@ Given /^I capture all network traffic$/ do
# something external to the VM state.
@sniffer = Sniffer.new("sniffer", $vmnet)
@sniffer.capture
+ add_after_scenario_hook(@sniffer.method(:stop))
+ add_after_scenario_hook(@sniffer.method(:clear))
end
Given /^I set Tails to boot with options "([^"]*)"$/ do |options|
diff --git a/features/step_definitions/pidgin.rb b/features/step_definitions/pidgin.rb
index eb719ea..b962f33 100644
--- a/features/step_definitions/pidgin.rb
+++ b/features/step_definitions/pidgin.rb
@@ -1,5 +1,186 @@
+# Extracts the secrets for the XMMP account `account_name`.
+def xmpp_account(account_name, required_options = [])
+ begin
+ account = $config["Pidgin"]["Accounts"]["XMPP"][account_name]
+ check_keys = ["username", "domain", "password"] + required_options
+ for key in check_keys do
+ assert(account.has_key?(key))
+ assert_not_nil(account[key])
+ assert(!account[key].empty?)
+ end
+ rescue NoMethodError, Test::Unit::AssertionFailedError
+ raise(
+<<EOF
+Your Pidgin:Accounts:XMPP:#{account} is incorrect or missing from your local configuration file (#{LOCAL_CONFIG_FILE}). See wiki/src/contribute/release_process/test/usage.mdwn for the format.
+EOF
+)
+ end
+ return account
+end
+
+When /^I create my XMPP account$/ do
+ next if @skip_steps_while_restoring_background
+ account = xmpp_account("Tails_account")
+ @screen.click("PidginAccountManagerAddButton.png")
+ @screen.wait("PidginAddAccountWindow.png", 20)
+ @screen.click_mid_right_edge("PidginAddAccountProtocolLabel.png")
+ @screen.click("PidginAddAccountProtocolXMPP.png")
+ @screen.click_mid_right_edge("PidginAddAccountXMPPUsername.png")
+ @screen.type(account["username"])
+ @screen.click_mid_right_edge("PidginAddAccountXMPPDomain.png")
+ @screen.type(account["domain"])
+ @screen.click_mid_right_edge("PidginAddAccountXMPPPassword.png")
+ @screen.type(account["password"])
+ @screen.click("PidginAddAccountXMPPRememberPassword.png")
+ if account["connect_server"]
+ @screen.click("PidginAddAccountXMPPAdvancedTab.png")
+ @screen.click_mid_right_edge("PidginAddAccountXMPPConnectServer.png")
+ @screen.type(account["connect_server"])
+ end
+ @screen.click("PidginAddAccountXMPPAddButton.png")
+end
+
+Then /^Pidgin automatically enables my XMPP account$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window('Buddy List')
+ @screen.wait("PidginAvailableStatus.png", 120)
+end
+
+Given /^my XMPP friend goes online( and joins the multi-user chat)?$/ do |join_chat|
+ next if @skip_steps_while_restoring_background
+ account = xmpp_account("Friend_account", ["otr_key"])
+ bot_opts = account.select { |k, v| ["connect_server"].include?(k) }
+ if join_chat
+ bot_opts["auto_join"] = [@chat_room_jid]
+ end
+ @friend_name = account["username"]
+ @chatbot = ChatBot.new(account["username"] + "@" + account["domain"],
+ account["password"], account["otr_key"], bot_opts)
+ @chatbot.start
+ add_after_scenario_hook(@chatbot.method(:stop))
+ @vm.focus_window('Buddy List')
+ @screen.wait("PidginFriendOnline.png", 60)
+end
+
+When /^I start a conversation with my friend$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window('Buddy List')
+ # Clicking the middle, bottom of this image should query our
+ # friend, given it's the only subscribed user that's online, which
+ # we assume.
+ r = @screen.find("PidginFriendOnline.png")
+ bottom_left = r.getBottomLeft()
+ x = bottom_left.getX + r.getW/2
+ y = bottom_left.getY
+ @screen.doubleClick_point(x, y)
+ # Since Pidgin sets the window name to the contact, we have no good
+ # way to identify the conversation window. Let's just look for the
+ # expected menu bar.
+ @screen.wait("PidginConversationWindowMenuBar.png", 10)
+end
+
+And /^I say something to my friend( in the multi-user chat)?$/ do |multi_chat|
+ next if @skip_steps_while_restoring_background
+ msg = "ping" + Sikuli::Key.ENTER
+ if multi_chat
+ @vm.focus_window(@chat_room_jid.split("@").first)
+ msg = @friend_name + ": " + msg
+ else
+ @vm.focus_window(@friend_name)
+ end
+ @screen.type(msg)
+end
+
+Then /^I receive a response from my friend( in the multi-user chat)?$/ do |multi_chat|
+ next if @skip_steps_while_restoring_background
+ if multi_chat
+ @vm.focus_window(@chat_room_jid.split("@").first)
+ else
+ @vm.focus_window(@friend_name)
+ end
+ @screen.wait("PidginFriendExpectedAnswer.png", 20)
+end
+
+When /^I start an OTR session with my friend$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window(@friend_name)
+ @screen.click("PidginConversationOTRMenu.png")
+ @screen.hide_cursor
+ @screen.click("PidginOTRMenuStartSession.png")
+end
+
+Then /^Pidgin automatically generates an OTR key$/ do
+ next if @skip_steps_while_restoring_background
+ @screen.wait("PidginOTRKeyGenPrompt.png", 30)
+ @screen.wait_and_click("PidginOTRKeyGenPromptDoneButton.png", 30)
+end
+
+Then /^an OTR session was successfully started with my friend$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window(@friend_name)
+ @screen.wait("PidginConversationOTRUnverifiedSessionStarted.png", 10)
+end
+
+# The reason the chat must be empty is to guarantee that we don't mix
+# up messages/events from other users with the ones we expect from the
+# bot.
+When /^I join some empty multi-user chat$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window('Buddy List')
+ @screen.click("PidginBuddiesMenu.png")
+ @screen.wait_and_click("PidginBuddiesMenuJoinChat.png", 10)
+ @screen.wait_and_click("PidginJoinChatWindow.png", 10)
+ @screen.click_mid_right_edge("PidginJoinChatRoomLabel.png")
+ account = xmpp_account("Tails_account")
+ if account.has_key?("chat_room") && \
+ !account["chat_room"].nil? && \
+ !account["chat_room"].empty?
+ chat_room = account["chat_room"]
+ else
+ chat_room = random_alnum_string(10, 15)
+ end
+ @screen.type(chat_room)
+
+ # We will need the conference server later, when starting the bot.
+ @screen.click_mid_right_edge("PidginJoinChatServerLabel.png")
+ @screen.type("a", Sikuli::KeyModifier.CTRL)
+ @screen.type("c", Sikuli::KeyModifier.CTRL)
+ conference_server =
+ @vm.execute_successfully("xclip -o", LIVE_USER).stdout.chomp
+ @chat_room_jid = chat_room + "@" + conference_server
+
+ @screen.click("PidginJoinChatButton.png")
+ # The following will both make sure that the we joined the chat, and
+ # that it is empty. We'll also deal with the *potential* "Create New
+ # Room" prompt that Pidgin shows for some server configurations.
+ images = ["PidginCreateNewRoomPrompt.png",
+ "PidginChat1UserInRoom.png"]
+ image_found, _ = @screen.waitAny(images, 30)
+ if image_found == "PidginCreateNewRoomPrompt.png"
+ @screen.click("PidginCreateNewRoomAcceptDefaultsButton.png")
+ end
+ @vm.focus_window(@chat_room_jid)
+ @screen.wait("PidginChat1UserInRoom.png", 10)
+end
+
+# Since some servers save the scrollback, and sends it when joining,
+# it's safer to clear it so we do not get false positives from old
+# messages when looking for a particular response, or similar.
+When /^I clear the multi-user chat's scrollback$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window(@chat_room_jid)
+ @screen.click("PidginConversationMenu.png")
+ @screen.wait_and_click("PidginConversationMenuClearScrollback.png", 10)
+end
+
+Then /^I can see that my friend joined the multi-user chat$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.focus_window(@chat_room_jid)
+ @screen.wait("PidginChat2UsersInRoom.png", 60)
+end
+
def configured_pidgin_accounts
- accounts = []
+ accounts = Hash.new
xml = REXML::Document.new(@vm.file_content('$HOME/.purple/accounts.xml',
LIVE_USER))
xml.elements.each("account/account") do |e|
@@ -9,14 +190,14 @@ def configured_pidgin_accounts
port = e.elements["settings/setting[@name='port']"].text
nickname = e.elements["settings/setting[@name='username']"].text
real_name = e.elements["settings/setting[@name='realname']"].text
- accounts.push({
- 'name' => account_name,
- 'network' => network,
- 'protocol' => protocol,
- 'port' => port,
- 'nickname' => nickname,
- 'real_name' => real_name,
- })
+ accounts[network] = {
+ 'name' => account_name,
+ 'network' => network,
+ 'protocol' => protocol,
+ 'port' => port,
+ 'nickname' => nickname,
+ 'real_name' => real_name,
+ }
end
return accounts
@@ -52,7 +233,7 @@ Given /^Pidgin has the expected accounts configured with random nicknames$/ do
["irc.oftc.net", "prpl-irc", "6697"],
["127.0.0.1", "prpl-irc", "6668"],
]
- configured_pidgin_accounts.each() do |account|
+ configured_pidgin_accounts.values.each() do |account|
assert(account['nickname'] != "XXX_NICK_XXX", "Nickname was no randomised")
assert_equal(account['nickname'], account['real_name'],
"Nickname and real name are not identical: " +
@@ -99,20 +280,30 @@ When /^I activate the "([^"]+)" Pidgin account$/ do |account|
@screen.wait("PidginConnecting.png", 5)
end
-def focus_pidgin_buddy_list
- @vm.execute_successfully(
- "xdotool search --name 'Buddy List' windowactivate --sync", LIVE_USER
- )
-end
-
Then /^Pidgin successfully connects to the "([^"]+)" account$/ do |account|
next if @skip_steps_while_restoring_background
expected_channel_entry = chan_image(account, default_chan(account), 'roaster')
# Sometimes the OFTC welcome notice window pops up over the buddy list one...
- focus_pidgin_buddy_list
+ @vm.focus_window('Buddy List')
@screen.wait(expected_channel_entry, 60)
end
+Then /^the "([^"]*)" account only responds to PING and VERSION CTCP requests$/ do |irc_server|
+ next if @skip_steps_while_restoring_background
+ ctcp_cmds = [
+ "CLIENTINFO", "DATE", "ERRMSG", "FINGER", "PING", "SOURCE", "TIME",
+ "USERINFO", "VERSION"
+ ]
+ expected_ctcp_replies = {
+ "PING" => /^\d+$/,
+ "VERSION" => /^Purple IRC$/
+ }
+ spam_target = configured_pidgin_accounts[irc_server]["nickname"]
+ ctcp_check = CtcpChecker.new(irc_server, 6667, spam_target, ctcp_cmds,
+ expected_ctcp_replies)
+ ctcp_check.verify_ctcp_responses
+end
+
Then /^I can join the "([^"]+)" channel on "([^"]+)"$/ do |channel, account|
next if @skip_steps_while_restoring_background
@screen.doubleClick( chan_image(account, channel, 'roaster'))
diff --git a/features/support/config.rb b/features/support/config.rb
index f28cd26..1bdc74c 100644
--- a/features/support/config.rb
+++ b/features/support/config.rb
@@ -49,3 +49,6 @@ TOR_AUTHORITIES =
"154.35.32.5"
]
VM_XML_PATH = "#{Dir.pwd}/features/domains"
+
+TAILS_SIGNING_KEY = cmd_helper(". #{Dir.pwd}/config/amnesia; echo ${AMNESIA_DEV_KEYID}").tr(' ', '').chomp
+TAILS_DEBIAN_REPO_KEY = "221F9A3C6FA3E09E182E060BC7988EA7A358D82E"
diff --git a/features/support/helpers/chatbot_helper.rb b/features/support/helpers/chatbot_helper.rb
new file mode 100644
index 0000000..02a09c7
--- /dev/null
+++ b/features/support/helpers/chatbot_helper.rb
@@ -0,0 +1,60 @@
+require 'tempfile'
+
+class ChatBot
+
+ def initialize(account, password, otr_key, opts = Hash.new)
+ @account = account
+ @password = password
+ @otr_key = otr_key
+ @opts = opts
+ @pid = nil
+ @otr_key_file = nil
+ end
+
+ def start
+ @otr_key_file = Tempfile.new("otr_key.", $config["TMP_DIR"])
+ @otr_key_file << @otr_key
+ @otr_key_file.close
+
+ # XXX: Once #9066 we should remove the convertkey.py script from
+ # our tree and use the one bundled in python-potr instead.
+ cmd_helper("#{GIT_DIR}/features/scripts/convertkey.py #{@otr_key_file.path}")
+ cmd_helper("mv #{@otr_key_file.path}3 #{@otr_key_file.path}")
+
+ cmd = [
+ "#{GIT_DIR}/features/scripts/otr-bot.py",
+ @account,
+ @password,
+ @otr_key_file.path
+ ]
+ cmd += ["--connect-server", @opts["connect_server"]] if @opts["connect_server"]
+ cmd += ["--auto-join"] + @opts["auto_join"] if @opts["auto_join"]
+
+ job = IO.popen(cmd)
+ @pid = job.pid
+ end
+
+ def stop
+ @otr_key_file.delete
+ begin
+ Process.kill("TERM", @pid)
+ rescue
+ # noop
+ end
+ end
+
+ def active?
+ begin
+ ret = Process.kill(0, @pid)
+ rescue Errno::ESRCH => e
+ if e.message == "No such process"
+ return false
+ else
+ raise e
+ end
+ end
+ assert_equal(1, ret, "This shouldn't happen")
+ return true
+ end
+
+end
diff --git a/features/support/helpers/ctcp_helper.rb b/features/support/helpers/ctcp_helper.rb
new file mode 100644
index 0000000..f7699a9
--- /dev/null
+++ b/features/support/helpers/ctcp_helper.rb
@@ -0,0 +1,125 @@
+require 'net/irc'
+require 'timeout'
+
+class CtcpChecker < Net::IRC::Client
+
+ CTCP_SPAM_DELAY = 5
+
+ # `spam_target`: the nickname of the IRC user to CTCP spam.
+ # `ctcp_cmds`: the Array of CTCP commands to send.
+ # `expected_ctcp_replies`: Hash where the keys are the exact set of replies
+ # we expect, and their values a regex the reply data must match.
+ def initialize(host, port, spam_target, ctcp_cmds, expected_ctcp_replies)
+ @spam_target = spam_target
+ @ctcp_cmds = ctcp_cmds
+ @expected_ctcp_replies = expected_ctcp_replies
+ nickname = self.class.random_irc_nickname
+ opts = {
+ :nick => nickname,
+ :user => nickname,
+ :real => nickname,
+ }
+ opts[:logger] = Logger.new("/dev/null") if !$config["DEBUG"]
+ super(host, port, opts)
+ end
+
+ # Makes sure that only the expected CTCP replies are received.
+ def verify_ctcp_responses
+ @sent_ctcp_cmds = Set.new
+ @received_ctcp_replies = Set.new
+
+ # Give 60 seconds for connecting to the server and other overhead
+ # beyond the expected time to spam all CTCP commands.
+ expected_ctcp_spam_time = @ctcp_cmds.length * CTCP_SPAM_DELAY
+ timeout = expected_ctcp_spam_time + 60
+
+ begin
+ Timeout::timeout(timeout) do
+ start
+ end
+ rescue Timeout::Error
+ # Do nothing as we'll check for errors below.
+ ensure
+ finish
+ end
+
+ ctcp_cmds_not_sent = @ctcp_cmds - @sent_ctcp_cmds.to_a
+ expected_ctcp_replies_not_received =
+ @expected_ctcp_replies.keys - @received_ctcp_replies.to_a
+ if !ctcp_cmds_not_sent.empty? || !expected_ctcp_replies_not_received.empty?
+ raise "Failed to spam all CTCP commands and receive the expected " +
+ "replies within #{timeout} seconds.\n" +
+ (ctcp_cmds_not_sent.empty? ? "" :
+ "CTCP commands not sent: #{ctcp_cmds_not_sent}\n") +
+ (expected_ctcp_replies_not_received.empty? ? "" :
+ "Expected CTCP replies not received: " +
+ expected_ctcp_replies_not_received.to_s)
+ end
+
+ end
+
+ # Generate a random IRC nickname, in this case an alpha-numeric
+ # string with length 10 to 15. To make it legal, the first character
+ # is forced to be alpha.
+ def self.random_irc_nickname
+ random_alpha_string(1) + random_alnum_string(9, 14)
+ end
+
+ def spam(spam_target)
+ post(NOTICE, spam_target, "Hi! I'm gonna test your CTCP capabilities now.")
+ @ctcp_cmds.each do |cmd|
+ sleep CTCP_SPAM_DELAY
+ full_cmd = cmd
+ case cmd
+ when "PING"
+ full_cmd += " #{Time.now.to_i}"
+ when "ACTION"
+ full_cmd += " barfs on the floor."
+ when "ERRMSG"
+ full_cmd += " Pidgin should not respond to this."
+ end
+ post(PRIVMSG, spam_target, ctcp_encode(full_cmd))
+ @sent_ctcp_cmds << cmd
+ end
+ end
+
+ def on_rpl_welcome(m)
+ super
+ Thread.new { spam(@spam_target) }
+ end
+
+ def on_message(m)
+ if m.command == ERR_NICKNAMEINUSE
+ finish
+ new_nick = self.class.random_irc_nickname
+ @opts.marshal_load({
+ :nick => new_nick,
+ :user => new_nick,
+ :real => new_nick,
+ })
+ start
+ return
+ end
+
+ if m.ctcp? and /^:#{@spam_target}!/.match(m)
+ m.ctcps.each do |ctcp_reply|
+ reply_type, _, reply_data = ctcp_reply.partition(" ")
+ if @expected_ctcp_replies.has_key?(reply_type)
+ if @expected_ctcp_replies[reply_type].match(reply_data)
+ @received_ctcp_replies << reply_type
+ else
+ raise "Received expected CTCP reply '#{reply_type}' but with " +
+ "unexpected data '#{reply_data}' "
+ end
+ else
+ raise "Received unexpected CTCP reply '#{reply_type}' with " +
+ "data '#{reply_data}'"
+ end
+ end
+ end
+ if Set.new(@ctcp_cmds) == @sent_ctcp_cmds && \
+ Set.new(@expected_ctcp_replies.keys) == @received_ctcp_replies
+ finish
+ end
+ end
+end
diff --git a/features/support/helpers/misc_helpers.rb b/features/support/helpers/misc_helpers.rb
index d315e52..6c45319 100644
--- a/features/support/helpers/misc_helpers.rb
+++ b/features/support/helpers/misc_helpers.rb
@@ -111,3 +111,19 @@ def get_free_space(machine, path)
output = free.split("\n").last
return output.match(/[^\s]\s+[0-9]+\s+[0-9]+\s+([0-9]+)\s+.*/)[1].chomp.to_i
end
+
+def random_string_from_set(set, min_len, max_len)
+ len = (min_len..max_len).to_a.sample
+ len ||= min_len
+ (0..len-1).map { |n| set.sample }.join
+end
+
+def random_alpha_string(min_len, max_len = 0)
+ alpha_set = ('A'..'Z').to_a + ('a'..'z').to_a
+ random_string_from_set(alpha_set, min_len, max_len)
+end
+
+def random_alnum_string(min_len, max_len = 0)
+ alnum_set = ('A'..'Z').to_a + ('a'..'z').to_a + (0..9).to_a.map { |n| n.to_s }
+ random_string_from_set(alnum_set, min_len, max_len)
+end
diff --git a/features/support/helpers/sikuli_helper.rb b/features/support/helpers/sikuli_helper.rb
index b0ad6da..b087145 100644
--- a/features/support/helpers/sikuli_helper.rb
+++ b/features/support/helpers/sikuli_helper.rb
@@ -104,6 +104,18 @@ def sikuli_script_proxy.new(*args)
self.click(Sikuli::Location.new(x, y))
end
+ def s.doubleClick_point(x, y)
+ self.doubleClick(Sikuli::Location.new(x, y))
+ end
+
+ def s.click_mid_right_edge(pic)
+ r = self.find(pic)
+ top_right = r.getTopRight()
+ x = top_right.getX
+ y = top_right.getY + r.getH/2
+ self.click_point(x, y)
+ end
+
def s.wait_and_click(pic, time)
self.click(self.wait(pic, time))
end
@@ -120,6 +132,35 @@ def sikuli_script_proxy.new(*args)
self.hover(self.wait(pic, time))
end
+ def s.findAny(images)
+ images.each do |image|
+ begin
+ return [image, self.find(image)]
+ rescue FindFailed
+ # Ignore. We deal we'll throw an appropriate exception after
+ # having looped through all images and found none of them.
+ end
+ end
+ # If we've reached this point, none of the images could be found.
+ Rjb::throw('org.sikuli.script.FindFailed',
+ "can not find any of the images #{images} on the screen")
+ end
+
+ def s.waitAny(images, time)
+ Timeout::timeout(time) do
+ loop do
+ begin
+ return self.findAny(images)
+ rescue FindFailed
+ # Ignore. We want to retry until we timeout.
+ end
+ end
+ end
+ rescue Timeout::Error
+ Rjb::throw('org.sikuli.script.FindFailed',
+ "can not find any of the images #{images} on the screen")
+ end
+
def s.hover_point(x, y)
self.hover(Sikuli::Location.new(x, y))
end
diff --git a/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb
index 9cdec42..4e9a5c4 100644
--- a/features/support/helpers/vm_helper.rb
+++ b/features/support/helpers/vm_helper.rb
@@ -374,6 +374,12 @@ EOF
return execute("pidof -x -o '%PPID' " + process).stdout.chomp.split
end
+ def focus_window(window_title, user = LIVE_USER)
+ execute_successfully(
+ "xdotool search --name '#{window_title}' windowactivate --sync", user
+ )
+ end
+
def file_exist?(file)
execute("test -e '#{file}'").success?
end
diff --git a/features/support/hooks.rb b/features/support/hooks.rb
index 8069ef9..1c8e79c 100644
--- a/features/support/hooks.rb
+++ b/features/support/hooks.rb
@@ -19,6 +19,11 @@ def delete_all_snapshots
end
end
+def add_after_scenario_hook(fn, args = [])
+ @after_scenario_hook ||= Array.new
+ @after_scenario_hook << [fn, args]
+end
+
BeforeFeature('@product') do |feature|
if File.exist?($config["TMP_DIR"])
if !File.directory?($config["TMP_DIR"])
@@ -119,10 +124,6 @@ After('@product') do |scenario|
STDIN.gets
end
end
- if @sniffer
- @sniffer.stop
- @sniffer.clear
- end
@vm.destroy_and_undefine if @vm
end
@@ -167,10 +168,16 @@ After('@source') do
FileUtils.remove_entry_secure @git_clone
end
-
# Common
########
+After do
+ if @after_scenario_hook
+ @after_scenario_hook.each { |fn, args| fn.call(*args) }
+ end
+ @after_scenario_hook = Array.new
+end
+
BeforeFeature('@product', '@source') do |feature|
raise "Feature #{feature.file} is tagged both @product and @source, " +
"which is an impossible combination"
diff --git a/run_test_suite b/run_test_suite
index d173619..d9b59b6 100755
--- a/run_test_suite
+++ b/run_test_suite
@@ -1,7 +1,8 @@
-#!/bin/sh
+#!/bin/bash
set -e
set -u
+set -o pipefail
NAME=$(basename ${0})
@@ -34,6 +35,8 @@ Options for '@product' features:
--old-iso IMAGE For some '@product' features (e.g. usb_install) we need
an older version of Tails, which this options sets to
IMAGE.
+ --log-to-file FILE Save the output to the specified file, in addition to
+ sending it to stdout and stderr.
Note that '@source' features has no relevant options.
"
@@ -97,9 +100,28 @@ capture_session() {
-vcodec libvpx -y "${CAPTURE_FILE}" >/dev/null 2>&1 &
}
+remove_control_chars_from() {
+ local file="$1"
+ local tmpfile
+
+ # Sanity checks
+ [ -n "$file" ] || return 11
+ [ -r "$file" ] || return 13
+ [ -w "$(dirname "$file")" ] || return 17
+
+ # Remove control chars with `perl` and backspaces with `col`
+ tmpfile=$(mktemp)
+ cat "$file" \
+ | perl -pe 's/\e([^\[\]]|\[.*?[a-zA-Z]|\].*?\a)//g' \
+ | col -b \
+ > "$tmpfile"
+ mv "$tmpfile" "$file"
+}
+
# main script
CAPTURE_FILE=
+LOG_FILE=
VNC_VIEWER=
VNC_SERVER=
DEBUG=
@@ -110,7 +132,7 @@ TEMP_DIR=
TAILS_ISO=
OLD_TAILS_ISO=
-LONGOPTS="view,vnc-server-only,capture:,help,temp-dir:,keep-snapshots,retry-find,iso:,old-iso:,debug,pause-on-fail"
+LONGOPTS="view,vnc-server-only,capture:,help,temp-dir:,keep-snapshots,retry-find,iso:,old-iso:,debug,pause-on-fail,log-to-file:"
OPTS=$(getopt -o "" --longoptions $LONGOPTS -n "${NAME}" -- "$@")
eval set -- "$OPTS"
while [ $# -gt 0 ]; do
@@ -127,6 +149,10 @@ while [ $# -gt 0 ]; do
shift
CAPTURE_FILE="$1"
;;
+ --log-to-file)
+ shift
+ LOG_FILE="$1"
+ ;;
--debug)
export DEBUG="yes"
;;
@@ -186,8 +212,21 @@ export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64"
export SIKULI_HOME="/usr/share/java"
export DISPLAY=${TARGET_DISPLAY}
check_dependency cucumber
-if [ -z "${*}" ]; then
- cucumber --format ExtraHooks::Pretty features
+
+if [ -z "$@" ]; then
+ FEATURES="features"
+else
+ FEATURES="$@"
+fi
+
+CUCUMBER_COMMAND="cucumber --format ExtraHooks::Pretty \
+features/step_definitions features/support ${FEATURES}"
+
+if [ -z "$LOG_FILE" ]; then
+ $CUCUMBER_COMMAND
else
- cucumber --format ExtraHooks::Pretty features/step_definitions features/support ${*}
+ script --quiet --flush --return --command "$CUCUMBER_COMMAND" "$LOG_FILE" | cat
+ RET="$?"
+ remove_control_chars_from "$LOG_FILE"
+ exit "$RET"
fi
diff --git a/wiki/src/contribute/release_process/test.mdwn b/wiki/src/contribute/release_process/test.mdwn
index 85bd772..f651ac1 100644
--- a/wiki/src/contribute/release_process/test.mdwn
+++ b/wiki/src/contribute/release_process/test.mdwn
@@ -101,36 +101,6 @@ tracked by tickets prefixed with `todo/test_suite:`.
* Browsing (by IP) a HTTP or HTTPS server on the LAN should be possible.
* Browsing (by IP) a FTP server on the LAN should be possible.
-# Pidgin
-
-(automate: [[!tails_ticket 7820]])
-
-* Check that you can initiate an OTR conversation.
-* Check that XMPP is working with a new test profile.
- For example using Riseup:
- - Username: username
- - Domain: riseup.net
- - Connect server: 4cjw6cwpeaeppfqz.onion
- - Then try to create and connect to a new room:
- - Room: testing
- - Server: conference.riseup.net
- - Handle: username
-* Check that Pidgin doesn't leak too much information when replying to
- CTCP requests:
- * Start Tails, launch Pidgin, and join #tails.
- * Also join #tails from a client that supports CTCP commands
- properly, e.g. Konversation.
- * Try to send `/ctcp <Tails_account_nick> COMMAND` from the other client to Pidgin:
- - You should get no answer apart for the PING and VERSION commands
- ([[!tails_ticket 5823]]).
- - List of `/ctcp` commands, see [this page](http://www.wikkedwire.com/irccommands):
- - PING
- - VERSION
- - FINGER
- - USERINFO
- - CLIENTINFO
- - TIME
-
# Tor
* The version of Tor should be the latest stable one, which is the highest version number
diff --git a/wiki/src/contribute/release_process/test/automated_tests.mdwn b/wiki/src/contribute/release_process/test/automated_tests.mdwn
index e637935..e914bff 100644
--- a/wiki/src/contribute/release_process/test/automated_tests.mdwn
+++ b/wiki/src/contribute/release_process/test/automated_tests.mdwn
@@ -103,6 +103,15 @@ Requirements on the guest (the remote shell server):
firewall exceptions; actually we don't want any network traffic at
all from it, but this kind of follows from the previous requirement
any way)
+* must start before Tails Greeter. Since that's the first point of
+ user interaction in a Tails system (if we ignore the boot menu), it
+ seems like a good place to be able to assume that the remote shell
+ is running.
+
+Scripts:
+
+* [[!tails_gitweb config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell]]
+* [[!tails_gitweb config/chroot_local-includes/etc/init.d/tails-autotest-remote-shell]]
# The art of writing new product test cases
diff --git a/wiki/src/contribute/release_process/test/setup.mdwn b/wiki/src/contribute/release_process/test/setup.mdwn
index 9d34b8d..eac306a 100644
--- a/wiki/src/contribute/release_process/test/setup.mdwn
+++ b/wiki/src/contribute/release_process/test/setup.mdwn
@@ -24,9 +24,11 @@ wheezy-backports sources added:
libxslt1-dev tcpdump unclutter radvd x11-apps syslinux \
libcap2-bin devscripts libvirt-ruby ruby-rspec gawk ntp ovmf/testing \
ruby-json x11vnc xtightvncviewer ffmpeg libavcodec-extra-53 \
- libvpx1 dnsmasq-base openjdk-7-jre ruby-guestfs ruby-test-unit && \
+ libvpx1 dnsmasq-base openjdk-7-jre ruby-guestfs ruby-net-irc \
+ ruby-test-unit python-jabberbot && \
apt-get -t wheezy-backports install qemu-kvm qemu-system-x86 libvirt0 \
- libvirt-dev libvirt-bin seabios ruby-rjb ruby-packetfu cucumber && \
+ libvirt-dev libvirt-bin seabios ruby-rjb ruby-packetfu cucumber \
+ python-potr && \
service libvirtd restart
In addition, if `libguestfs` doesn't work by default you probably have
diff --git a/wiki/src/contribute/release_process/test/usage.mdwn b/wiki/src/contribute/release_process/test/usage.mdwn
index 9376cee..a1ebba5 100644
--- a/wiki/src/contribute/release_process/test/usage.mdwn
+++ b/wiki/src/contribute/release_process/test/usage.mdwn
@@ -112,3 +112,53 @@ bridges, for the `cert=... iat-mode=...` stuff, and the same for
This setting is required for `tor_bridges.feature` (requires types
`Bridge`, `Obfs2`, `Obfs3` and `Obfs4`) and `time_syncing.feature`
(requires type `Bridge` only).
+
+### Pidgin
+
+These secrets are required for some scenarios in
+`pidgin.feature`. Here's an example which explains the format:
+
+ Pidgin:
+ Accounts:
+ XMPP:
+ Tails_account:
+ username: "test"
+ domain: "jabber.org"
+ password: "opensesame"
+ Friend_account:
+ username: "friend"
+ domain: "jabber.org"
+ password: "trustno1"
+ otr_key: |
+ (privkeys
+ (account
+ (name friend)
+ (protocol xmpp)
+ (private-key
+ (dsa
+ [...]
+
+Note that the fields used in the above example show the *mandatory*
+fields.
+
+The XMPP account described by `Tails_account` (to be used in Tails'
+Pidgin) and `Friend_account` (run via a bot from the tester host) must
+be subscribed to each other but to no other XMPP account. Also, for the
+`Friend_account`, it's important that the `otr_key`'s `name` field is
+the same as `username`, and that the `protocol` field is `xmpp`.
+
+If a "Connect Server" is needed for any of the accounts, it can be set
+in the *optional* `connect_server` field.
+
+In case the `Tails_account`'s conference server doesn't allow creating
+arbitrary chat rooms, a specific one that is known to work can be set
+in the *optional* `chat_room` field. It should still be a room with a
+strange name that is highly likely to always be empty; otherwise the
+test will fail.
+
+XMPP services known to work well both for `Tails_account` and
+`Friend_account` are:
+* riseup.net (use `connect_server: xmpp.riseup.net`)
+* jabber.org (doesn't allow creating arbitrary chatrooms, so setting
+ `chat_room` may be needed)
+* jabber.ccc.de