summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Hands <phil@hands.com>2017-06-29 22:11:09 +0200
committerHolger Levsen <holger@layer-acht.org>2017-08-01 00:53:58 -0400
commita6f41c35e337db192e612ee6e1545fcae4c69ac7 (patch)
tree3d03ff353fee11bb00de060f73c6b4d040173e9a
parentc49069662ccf276a7b97f77524f3e2638c9fa152 (diff)
downloadjenkins.debian.net-a6f41c35e337db192e612ee6e1545fcae4c69ac7.tar.xz
lvc: grab updates from tails (01371c19bd..6ae59c49e5)
Signed-off-by: Holger Levsen <holger@layer-acht.org>
-rwxr-xr-xcucumber/bin/run_test_suite26
-rw-r--r--cucumber/features/config/defaults.yml4
-rw-r--r--cucumber/features/domains/cdrom.xml5
-rw-r--r--cucumber/features/domains/default.xml19
-rw-r--r--cucumber/features/domains/default_net.xml2
-rw-r--r--cucumber/features/domains/fs_share.xml6
-rwxr-xr-xcucumber/features/scripts/vm-execute6
-rw-r--r--cucumber/features/step_definitions/apt.rb132
-rw-r--r--cucumber/features/step_definitions/browser.rb100
-rw-r--r--cucumber/features/step_definitions/build.rb46
-rw-r--r--cucumber/features/step_definitions/checks.rb124
-rw-r--r--cucumber/features/step_definitions/common_steps.rb579
-rw-r--r--cucumber/features/step_definitions/dhcp.rb34
-rw-r--r--cucumber/features/step_definitions/electrum.rb18
-rw-r--r--cucumber/features/step_definitions/encryption.rb16
-rw-r--r--cucumber/features/step_definitions/firewall_leaks.rb33
-rw-r--r--cucumber/features/step_definitions/git.rb26
-rw-r--r--cucumber/features/step_definitions/icedove.rb94
-rw-r--r--cucumber/features/step_definitions/mac_spoofing.rb87
-rw-r--r--cucumber/features/step_definitions/pidgin.rb126
-rw-r--r--cucumber/features/step_definitions/root_access_control.rb3
-rw-r--r--cucumber/features/step_definitions/snapshots.rb15
-rw-r--r--cucumber/features/step_definitions/ssh.rb52
-rw-r--r--cucumber/features/step_definitions/time_syncing.rb16
-rw-r--r--cucumber/features/step_definitions/tor.rb152
-rw-r--r--cucumber/features/step_definitions/torified_browsing.rb8
-rw-r--r--cucumber/features/step_definitions/torified_gnupg.rb89
-rw-r--r--cucumber/features/step_definitions/torified_misc.rb16
-rw-r--r--cucumber/features/step_definitions/totem.rb23
-rw-r--r--cucumber/features/step_definitions/unsafe_browser.rb45
-rw-r--r--cucumber/features/step_definitions/untrusted_partitions.rb4
-rw-r--r--cucumber/features/step_definitions/usb.rb347
-rw-r--r--cucumber/features/support/config.rb18
-rw-r--r--cucumber/features/support/env.rb36
-rw-r--r--cucumber/features/support/extra_hooks.rb54
-rw-r--r--cucumber/features/support/helpers/dogtail.rb233
-rw-r--r--cucumber/features/support/helpers/exec_helper.rb90
-rw-r--r--cucumber/features/support/helpers/firewall_helper.rb187
-rw-r--r--cucumber/features/support/helpers/misc_helpers.rb139
-rw-r--r--cucumber/features/support/helpers/remote_shell.rb171
-rw-r--r--cucumber/features/support/helpers/sikuli_helper.rb32
-rw-r--r--cucumber/features/support/helpers/sniffing_helper.rb14
-rw-r--r--cucumber/features/support/helpers/storage_helper.rb39
-rw-r--r--cucumber/features/support/helpers/vm_helper.rb206
-rw-r--r--cucumber/features/support/hooks.rb53
45 files changed, 2174 insertions, 1351 deletions
diff --git a/cucumber/bin/run_test_suite b/cucumber/bin/run_test_suite
index 19128445..f7903639 100755
--- a/cucumber/bin/run_test_suite
+++ b/cucumber/bin/run_test_suite
@@ -20,13 +20,15 @@ libvirt-clients
libvirt-daemon-system
libvirt-dev
libvirt0
-openjdk-7-jre
+obfs4proxy
openssh-server
ovmf
+pry
python-jabberbot
python-potr
qemu-kvm
qemu-system-x86
+redir
ruby-guestfs
ruby-json
ruby-libvirt
@@ -38,6 +40,7 @@ ruby-rspec
ruby-test-unit
seabios
tcpdump
+tor
unclutter
virt-viewer
xvfb
@@ -59,9 +62,10 @@ Options for '@product' features:
encoding. Requires x264.
--capture-all Keep videos for all scenarios, including those that
succeed (implies --capture).
- --pause-on-fail On failure, pause test suite until pressing Enter. This is
- useful for investigating the state of the VM guest to see
- exactly why a test failed.
+ --interactive-debugging
+ On failure, pause test suite until pressing Enter. Also
+ offer the option to open an interactive Ruby shell (pry)
+ in the Cucumber world's context.
--keep-snapshots Don't ever delete any snapshots (including ones marked as
temporary). This can be a big time saver when debugging new
features.
@@ -74,7 +78,7 @@ Options for '@product' features:
(default is TMPDIR in the environment, and if unset,
/tmp/DebianToaster).
--view Shows the test session in a windows. Requires x11vnc
- and xtightvncviewer.
+ and tigervnc-viewer.
--vnc-server-only Starts a VNC server for the test session. Requires x11vnc.
--iso IMAGE Test '@product' features using IMAGE.
--old-iso IMAGE For some '@product' features (e.g. usb_install) we need
@@ -159,8 +163,8 @@ start_vnc_server() {
}
start_vnc_viewer() {
- check_dependencies xtightvncviewer
- xtightvncviewer -viewonly localhost:${VNC_SERVER_PORT} 1>/dev/null 2>&1 &
+ check_dependencies tigervnc-viewer
+ xtigervncviewer -nojpeg -viewonly localhost:${VNC_SERVER_PORT} 1>/dev/null 2>&1 &
}
capture_session() {
@@ -181,13 +185,13 @@ CAPTURE_ALL=
LOG_FILE=
VNC_VIEWER=
VNC_SERVER=
-PAUSE_ON_FAIL=
+INTERACTIVE_DEBUGGING=
KEEP_SNAPSHOTS=
SIKULI_RETRY_FINDFAILED=
ISO=
OLD_ISO=
-LONGOPTS="artifacts-base-uri:,view,vnc-server-only,capture,capture-all,help,tmpdir:,keep-snapshots,retry-find,iso:,old-iso:,pause-on-fail"
+LONGOPTS="artifacts-base-uri:,view,vnc-server-only,capture,capture-all,help,tmpdir:,keep-snapshots,retry-find,iso:,old-iso:,interactive-debugging"
OPTS=$(getopt -o "" --longoptions $LONGOPTS -n "${NAME}" -- "$@")
eval set -- "$OPTS"
while [ $# -gt 0 ]; do
@@ -213,8 +217,8 @@ while [ $# -gt 0 ]; do
export CAPTURE="yes"
export CAPTURE_ALL="yes"
;;
- --pause-on-fail)
- export PAUSE_ON_FAIL="yes"
+ --interactive-debugging)
+ export INTERACTIVE_DEBUGGING="yes"
;;
--keep-snapshots)
export KEEP_SNAPSHOTS="yes"
diff --git a/cucumber/features/config/defaults.yml b/cucumber/features/config/defaults.yml
index 9c312146..bd063073 100644
--- a/cucumber/features/config/defaults.yml
+++ b/cucumber/features/config/defaults.yml
@@ -1,7 +1,7 @@
CAPTURE: false
CAPTURE_ALL: false
+INTERACTIVE_DEBUGGING: false
MAX_NEW_TOR_CIRCUIT_RETRIES: 10
-PAUSE_ON_FAIL: false
SIKULI_RETRY_FINDFAILED: false
TMPDIR: "/tmp/DebianToaster"
@@ -33,4 +33,4 @@ Unsafe_SSH_private_key: |
NWema+bArbaF0rKVJpwvpkZWGcr6qRn94Ts0kJAzR+VIVTOjB9sVwdxjadwWHRs5
kKnpY0tnSF7hyVRwN7GOsNDJEaFjCW7k4+55D2ZNBy2iN3beW8CZ
-----END RSA PRIVATE KEY-----
-Unsafe_SSH_public_key: = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8xQ2BRQz+TK6jbqb5fDuKAbrOAYUwVtLe7yblK0awk6fvMuInw/kyaX9H7i105LqjBVThFplM+w1lkr8KViY4+GY28nTilUKGTYNnwABGD9MA2PeqMqzcP4x4puTVu3oSwDnmSAaxNSTLlOLxxzZadrbmOqNqAiLIzzbY8Yb2aYjr/MthHpAtSLM1pyJetEzdDhHixCQSt5WUd6ic8SIZSz3PHSzAKku08zlQhi17U9UeCTB4+xTq8zxSpVMr9XC0suAtqdeewwW7OrsNMBc+rj35dIU2rgmXFsQXr49Bdm9hnk15bTQars1Kk8/y6gevp/Un6YzHGczzmPNi1HH5 amnesia@amnesia"
+Unsafe_SSH_public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8xQ2BRQz+TK6jbqb5fDuKAbrOAYUwVtLe7yblK0awk6fvMuInw/kyaX9H7i105LqjBVThFplM+w1lkr8KViY4+GY28nTilUKGTYNnwABGD9MA2PeqMqzcP4x4puTVu3oSwDnmSAaxNSTLlOLxxzZadrbmOqNqAiLIzzbY8Yb2aYjr/MthHpAtSLM1pyJetEzdDhHixCQSt5WUd6ic8SIZSz3PHSzAKku08zlQhi17U9UeCTB4+xTq8zxSpVMr9XC0suAtqdeewwW7OrsNMBc+rj35dIU2rgmXFsQXr49Bdm9hnk15bTQars1Kk8/y6gevp/Un6YzHGczzmPNi1HH5 amnesia@amnesia"
diff --git a/cucumber/features/domains/cdrom.xml b/cucumber/features/domains/cdrom.xml
new file mode 100644
index 00000000..8bc3be7c
--- /dev/null
+++ b/cucumber/features/domains/cdrom.xml
@@ -0,0 +1,5 @@
+<disk type='file' device='cdrom'>
+ <driver name='qemu' type='raw'/>
+ <target dev='hdc' bus='sata'/>
+ <readonly/>
+</disk>
diff --git a/cucumber/features/domains/default.xml b/cucumber/features/domains/default.xml
index 040a5d8e..0966ef9a 100644
--- a/cucumber/features/domains/default.xml
+++ b/cucumber/features/domains/default.xml
@@ -1,10 +1,9 @@
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
- <name>DebianToaster</name>
<memory unit='KiB'>1310720</memory>
<currentMemory unit='KiB'>1310720</currentMemory>
- <vcpu>1</vcpu>
+ <vcpu>2</vcpu>
<os>
- <type arch='x86_64' machine='pc-i440fx-2.1'>hvm</type>
+ <type arch='x86_64' machine='pc-q35-2.8'>hvm</type>
<boot dev='cdrom'/>
</os>
<features>
@@ -21,19 +20,11 @@
<on_crash>restart</on_crash>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
- <disk type='file' device='cdrom'>
- <driver name='qemu' type='raw'/>
- <source file=''/>
- <target dev='hdc' bus='ide'/>
- <readonly/>
- </disk>
- <controller type='usb' index='0' model='ich9-ehci1'/>
- <controller type='usb' index='0' model='ich9-uhci1'>
- <master startport='0'/>
- </controller>
- <controller type='ide' index='0'/>
+ <controller type='usb' index='0' model='nec-xhci'/>
+ <controller type='sata' index='0'/>
<controller type='virtio-serial' index='0'/>
<interface type='network'>
+ <alias name='net0'/>
<!-- <mac address='52:54:00:ac:dd:ee'/> -->
<source network='lvcNET'/>
<model type='virtio'/>
diff --git a/cucumber/features/domains/default_net.xml b/cucumber/features/domains/default_net.xml
index 35a1c61e..e9a575df 100644
--- a/cucumber/features/domains/default_net.xml
+++ b/cucumber/features/domains/default_net.xml
@@ -1,11 +1,11 @@
<network>
- <name>lvcNET</name>
<forward mode='nat'/>
<bridge name='virbr10' stp='on' delay='0' />
<ip address='10.2.1.1' netmask='255.255.255.0'>
<dhcp>
<range start='10.2.1.2' end='10.2.1.254' />
<!-- <host mac="52:54:00:ac:dd:ee" name="amnesia" ip="10.2.1.2" /> -->
+ <!-- <host mac="52:54:00:11:22:33" name="amnesia" ip="10.2.1.3" /> -->
</dhcp>
</ip>
<ip family="ipv6" address="fc00::1" prefix="7" />
diff --git a/cucumber/features/domains/fs_share.xml b/cucumber/features/domains/fs_share.xml
deleted file mode 100644
index 718755ea..00000000
--- a/cucumber/features/domains/fs_share.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<filesystem type='mount' accessmode='passthrough'>
- <driver type='path' wrpolicy='immediate'/>
- <source dir=''/>
- <target dir=''/>
- <readonly/>
-</filesystem>
diff --git a/cucumber/features/scripts/vm-execute b/cucumber/features/scripts/vm-execute
index 79b6942b..f3d20f9f 100755
--- a/cucumber/features/scripts/vm-execute
+++ b/cucumber/features/scripts/vm-execute
@@ -2,7 +2,9 @@
require 'optparse'
begin
- require "#{`git rev-parse --show-toplevel`.chomp}/cucumber/features/support/helpers/exec_helper.rb"
+ require "#{`git rev-parse --show-toplevel`.chomp}/cucumber/features/support/helpers/remote_shell.rb"
+ require "#{`git rev-parse --show-toplevel`.chomp}/cucumber/features/support/helpers/misc_helpers.rb"
+
rescue LoadError => e
raise "This script must be run from within Tails' Git directory."
end
@@ -46,7 +48,7 @@ opt_parser = OptionParser.new do |opts|
end
opt_parser.parse!(ARGV)
cmd = ARGV.join(" ")
-c = VMCommand.new(FakeVM.new, cmd, cmd_opts)
+c = RemoteShell::ShellCommand.new(FakeVM.new, cmd, cmd_opts)
puts "Return status: #{c.returncode}"
puts "STDOUT:\n#{c.stdout}"
puts "STDERR:\n#{c.stderr}"
diff --git a/cucumber/features/step_definitions/apt.rb b/cucumber/features/step_definitions/apt.rb
index c69d2598..52ef9f7f 100644
--- a/cucumber/features/step_definitions/apt.rb
+++ b/cucumber/features/step_definitions/apt.rb
@@ -2,55 +2,123 @@ require 'uri'
Given /^the only hosts in APT sources are "([^"]*)"$/ do |hosts_str|
hosts = hosts_str.split(',')
- $vm.file_content("/etc/apt/sources.list /etc/apt/sources.list.d/*").chomp.each_line { |line|
+ apt_sources = $vm.execute_successfully(
+ "cat /etc/apt/sources.list /etc/apt/sources.list.d/*"
+ ).stdout.chomp
+ apt_sources.each_line do |line|
next if ! line.start_with? "deb"
source_host = URI(line.split[1]).host
if !hosts.include?(source_host)
raise "Bad APT source '#{line}'"
end
- }
+ end
+end
+
+Given /^no proposed-updates APT suite is enabled$/ do
+ apt_sources = $vm.execute_successfully(
+ 'cat /etc/apt/sources.list /etc/apt/sources.list.d/*'
+ ).stdout
+ assert_no_match(/\s\S+-proposed-updates\s/, apt_sources)
+end
+
+When /^I configure APT to use non-onion sources$/ do
+ script = <<-EOF
+ use strict;
+ use warnings FATAL => "all";
+ s{vwakviie2ienjx6t[.]onion}{ftp.us.debian.org};
+ s{sgvtcaew4bxjd7ln[.]onion}{security.debian.org};
+ s{sdscoq7snqtznauu[.]onion}{deb.torproject.org};
+ s{jenw7xbd6tf7vfhp[.]onion}{deb.tails.boum.org};
+EOF
+ # VMCommand:s cannot handle newlines, and they're irrelevant in the
+ # above perl script any way
+ script.delete!("\n")
+ $vm.execute_successfully(
+ "perl -pi -E '#{script}' /etc/apt/sources.list /etc/apt/sources.list.d/*"
+ )
end
When /^I update APT using apt$/ do
- Timeout::timeout(30*60) do
- $vm.execute_successfully("echo #{@sudo_password} | " +
- "sudo -S apt update", :user => LIVE_USER)
+ recovery_proc = Proc.new do
+ step 'I kill the process "apt"'
+ $vm.execute('rm -rf /var/lib/apt/lists/*')
+ end
+ retry_tor(recovery_proc) do
+ Timeout::timeout(15*60) do
+ $vm.execute_successfully("echo #{@sudo_password} | " +
+ "sudo -S apt update", :user => LIVE_USER)
+ end
end
end
-Then /^I should be able to install a package using apt$/ do
- package = "cowsay"
- Timeout::timeout(120) do
- $vm.execute_successfully("echo #{@sudo_password} | " +
- "sudo -S apt install #{package}",
- :user => LIVE_USER)
+Then /^I install "(.+)" using apt$/ do |package_name|
+ recovery_proc = Proc.new do
+ step 'I kill the process "apt"'
+ $vm.execute("apt purge #{package_name}")
+ end
+ retry_tor(recovery_proc) do
+ Timeout::timeout(2*60) do
+ $vm.execute_successfully("echo #{@sudo_password} | " +
+ "sudo -S apt install #{package_name}",
+ :user => LIVE_USER)
+ end
end
- step "package \"#{package}\" is installed"
end
-When /^I update APT using Synaptic$/ do
- @screen.click('SynapticReloadButton.png')
- @screen.wait('SynapticReloadPrompt.png', 20)
- @screen.waitVanish('SynapticReloadPrompt.png', 30*60)
+When /^I start Synaptic$/ do
+ step 'I start "Synaptic Package Manager" via GNOME Activities Overview'
+ deal_with_polkit_prompt(@sudo_password)
+ @synaptic = Dogtail::Application.new('synaptic')
+ # The seemingly spurious space is needed because that is how this
+ # frame is named...
+ @synaptic.child(
+ 'Synaptic Package Manager ', roleName: 'frame', recursive: false
+ )
end
-Then /^I should be able to install a package using Synaptic$/ do
- package = "cowsay"
- try_for(60) do
- @screen.wait_and_click('SynapticSearchButton.png', 10)
- @screen.wait_and_click('SynapticSearchWindow.png', 10)
+When /^I update APT using Synaptic$/ do
+ recovery_proc = Proc.new do
+ step 'I kill the process "synaptic"'
+ step "I start Synaptic"
+ end
+ retry_tor(recovery_proc) do
+ @synaptic.button('Reload').click
+ sleep 10 # It might take some time before APT starts downloading
+ try_for(15*60, :msg => "Took too much time to download the APT data") {
+ !$vm.has_process?("/usr/lib/apt/methods/tor+http")
+ }
+ assert_raise(RuntimeError) do
+ @synaptic.child(roleName: 'dialog', recursive: false)
+ .child('Error', roleName: 'icon', retry: false)
+ end
+ if !$vm.has_process?("synaptic")
+ raise "Synaptic process vanished, did it segfault again?"
+ end
end
- @screen.type(package + Sikuli::Key.ENTER)
- @screen.wait_and_double_click('SynapticCowsaySearchResult.png', 20)
- @screen.wait_and_click('SynapticApplyButton.png', 10)
- @screen.wait('SynapticApplyPrompt.png', 60)
- @screen.type(Sikuli::Key.ENTER)
- @screen.wait('SynapticChangesAppliedPrompt.png', 240)
- step "package \"#{package}\" is installed"
end
-When /^I start Synaptic$/ do
- step 'I start "Synaptic" via the GNOME "System" applications menu'
- deal_with_polkit_prompt('PolicyKitAuthPrompt.png', @sudo_password)
- @screen.wait('SynapticReloadButton.png', 30)
+Then /^I install "(.+)" using Synaptic$/ do |package_name|
+ recovery_proc = Proc.new do
+ step 'I kill the process "synaptic"'
+ $vm.execute("apt -y purge #{package_name}")
+ step "I start Synaptic"
+ end
+ retry_tor(recovery_proc) do
+ @synaptic.button('Search').click
+ find_dialog = @synaptic.dialog('Find')
+ find_dialog.child(roleName: 'text').typeText(package_name)
+ find_dialog.button('Search').click
+ package_list = @synaptic.child('Installed Version',
+ roleName: 'table column header').parent
+ package_entry = package_list.child(package_name, roleName: 'table cell')
+ package_entry.doubleClick
+ @synaptic.button('Apply').click
+ apply_prompt = nil
+ try_for(60) { apply_prompt = @synaptic.dialog('Summary'); true }
+ apply_prompt.button('Apply').click
+ try_for(4*60) do
+ @synaptic.child('Changes applied', roleName: 'frame', recursive: false)
+ true
+ end
+ end
end
diff --git a/cucumber/features/step_definitions/browser.rb b/cucumber/features/step_definitions/browser.rb
index 84ef1d35..68d1bca4 100644
--- a/cucumber/features/step_definitions/browser.rb
+++ b/cucumber/features/step_definitions/browser.rb
@@ -1,41 +1,28 @@
-Then /^I see the (Unsafe|I2P) Browser start notification and wait for it to close$/ do |browser_type|
- robust_notification_wait("#{browser_type}BrowserStartNotification.png", 60)
+Then /^the Unsafe Browser has started$/ do
+ @screen.wait("UnsafeBrowserHomepage.png", 360)
end
-Then /^the (Unsafe|I2P) Browser has started$/ do |browser_type|
- case browser_type
- when 'Unsafe'
- @screen.wait("UnsafeBrowserHomepage.png", 360)
- when 'I2P'
- step 'the I2P router console is displayed in I2P Browser'
- end
-end
-
-When /^I start the (Unsafe|I2P) Browser(?: through the GNOME menu)?$/ do |browser_type|
- step "I start \"#{browser_type}Browser\" via the GNOME \"Internet\" applications menu"
+When /^I start the Unsafe Browser(?: through the GNOME menu)?$/ do
+ step "I start \"Unsafe Browser\" via GNOME Activities Overview"
end
-When /^I successfully start the (Unsafe|I2P) Browser$/ do |browser_type|
- step "I start the #{browser_type} Browser"
- step "I see and accept the Unsafe Browser start verification" unless browser_type == 'I2P'
- step "I see the #{browser_type} Browser start notification and wait for it to close"
- step "the #{browser_type} Browser has started"
+When /^I successfully start the Unsafe Browser$/ do
+ step "I start the Unsafe Browser"
+ step "I see and accept the Unsafe Browser start verification"
+ step "I see the \"Starting the Unsafe Browser...\" notification after at most 60 seconds"
+ step "the Unsafe Browser has started"
end
-When /^I close the (?:Unsafe|I2P) Browser$/ do
+When /^I close the Unsafe Browser$/ do
@screen.type("q", Sikuli::KeyModifier.CTRL)
end
-Then /^I see the (Unsafe|I2P) Browser stop notification$/ do |browser_type|
- robust_notification_wait("#{browser_type}BrowserStopNotification.png", 60)
-end
-
def xul_application_info(application)
binary = $vm.execute_successfully(
'echo ${TBB_INSTALL}/firefox', :libs => 'tor-browser'
).stdout.chomp
address_bar_image = "BrowserAddressBar.png"
- unused_tbb_libs = ['libnssdbm3.so']
+ unused_tbb_libs = ['libnssdbm3.so', "libmozavcodec.so", "libmozavutil.so"]
case application
when "Tor Browser"
user = LIVE_USER
@@ -47,11 +34,6 @@ def xul_application_info(application)
cmd_regex = "#{binary} .* -profile /home/#{user}/\.unsafe-browser/profile\.default"
chroot = "/var/lib/unsafe-browser/chroot"
new_tab_button_image = "UnsafeBrowserNewTabButton.png"
- when "I2P Browser"
- user = "i2pbrowser"
- cmd_regex = "#{binary} .* -profile /home/#{user}/\.i2p-browser/profile\.default"
- chroot = "/var/lib/i2p-browser/chroot"
- new_tab_button_image = "I2PBrowserNewTabButton.png"
when "Tor Launcher"
user = "tor-launcher"
# We do not enable AppArmor confinement for the Tor Launcher.
@@ -100,19 +82,41 @@ When /^I open the address "([^"]*)" in the (.*)$/ do |address, browser|
@screen.type('v', Sikuli::KeyModifier.CTRL)
@screen.type(Sikuli::Key.ENTER)
end
- open_address.call
+ recovery_on_failure = Proc.new do
+ @screen.type(Sikuli::Key.ESC)
+ @screen.waitVanish('BrowserReloadButton.png', 3)
+ open_address.call
+ end
if browser == "Tor Browser"
- recovery_on_failure = Proc.new do
- @screen.type(Sikuli::Key.ESC)
- @screen.waitVanish('BrowserReloadButton.png', 3)
- open_address.call
- end
- retry_tor(recovery_on_failure) do
- @screen.wait('BrowserReloadButton.png', 120)
- end
+ retry_method = method(:retry_tor)
+ else
+ retry_method = Proc.new { |p, &b| retry_action(10, recovery_proc: p, &b) }
+ end
+ open_address.call
+ retry_method.call(recovery_on_failure) do
+ @screen.wait('BrowserReloadButton.png', 120)
end
end
+# This step is limited to the Tor Browser due to #7502 since dogtail
+# uses the same interface.
+Then /^"([^"]+)" has loaded in the Tor Browser$/ do |title|
+ if @language == 'German'
+ browser_name = 'Tor-Browser'
+ reload_action = 'Aktuelle Seite neu laden'
+ else
+ browser_name = 'Tor Browser'
+ reload_action = 'Reload current page'
+ end
+ expected_title = "#{title} - #{browser_name}"
+ try_for(60) { @torbrowser.child(expected_title, roleName: 'frame') }
+ # The 'Reload current page' button (graphically shown as a looping
+ # arrow) is only shown when a page has loaded, so once we see the
+ # expected title *and* this button has appeared, then we can be sure
+ # that the page has fully loaded.
+ try_for(60) { @torbrowser.child(reload_action, roleName: 'push button') }
+end
+
Then /^the (.*) has no plugins installed$/ do |browser|
step "I open the address \"about:plugins\" in the #{browser}"
step "I see \"TorBrowserNoPlugins.png\" after at most 30 seconds"
@@ -193,3 +197,23 @@ Then /^the file is saved to the default Tor Browser download directory$/ do
expected_path = "/home/#{LIVE_USER}/Tor Browser/#{@some_file}"
try_for(10) { $vm.file_exist?(expected_path) }
end
+
+When /^I open Tails homepage in the (.+)$/ do |browser|
+ step "I open the address \"https://tails.boum.org\" in the #{browser}"
+end
+
+Then /^Tails homepage loads in the Tor Browser$/ do
+ title = 'Tails - Privacy for anyone anywhere'
+ step "\"#{title}\" has loaded in the Tor Browser"
+end
+
+Then /^Tails homepage loads in the Unsafe Browser$/ do
+ @screen.wait('TailsHomepage.png', 60)
+end
+
+Then /^the Tor Browser shows the "([^"]+)" error$/ do |error|
+ page = @torbrowser.child("Problem loading page", roleName: "document frame")
+ headers = page.children(roleName: "heading")
+ found = headers.any? { |heading| heading.text == error }
+ raise "Could not find the '#{error}' error in the Tor Browser" unless found
+end
diff --git a/cucumber/features/step_definitions/build.rb b/cucumber/features/step_definitions/build.rb
index fd001ff4..e02edc62 100644
--- a/cucumber/features/step_definitions/build.rb
+++ b/cucumber/features/step_definitions/build.rb
@@ -1,4 +1,4 @@
-Given /^Tails ([[:alnum:].]+) has been released$/ do |version|
+Given /^Tails ([[:alnum:]~.]+) has been released$/ do |version|
create_git unless git_exists?
old_branch = current_branch
@@ -17,7 +17,7 @@ tails (#{version}) stable; urgency=low
END_OF_CHANGELOG
end
fatal_system "git commit --quiet debian/changelog -m 'Release #{version}'"
- fatal_system "git tag '#{version}'"
+ fatal_system "git tag '#{version.gsub('~', '-')}'"
if old_branch != 'stable'
fatal_system "git checkout --quiet '#{old_branch}'"
@@ -42,6 +42,31 @@ Given /^the last version mentioned in debian\/changelog is ([[:alnum:]~.]+)$/ do
end
end
+Given /^the last versions mentioned in debian\/changelog are ([[:alnum:]~.]+) and ([[:alnum:]~.]+)$/ do |version_a, version_b|
+ step "the last version mentioned in debian/changelog is #{version_a}"
+ step "the last version mentioned in debian/changelog is #{version_b}"
+end
+
+Given(/^no frozen APT snapshot is encoded in config\/APT_snapshots\.d$/) do
+ ['debian', 'debian-security', 'torproject'].map do |origin|
+ File.open("config/APT_snapshots.d/#{origin}/serial", 'w+') do |serial|
+ serial.write("latest\n")
+ end
+ end
+end
+
+Given(/^frozen APT snapshots are encoded in config\/APT_snapshots\.d$/) do
+ ['debian', 'torproject'].map do |origin|
+ File.open("config/APT_snapshots.d/#{origin}/serial", 'w+') do |serial|
+ serial.write("2016060602\n")
+ end
+ end
+ # We never freeze debian-security
+ File.open("config/APT_snapshots.d/debian-security/serial", 'w+') do |serial|
+ serial.write("latest\n")
+ end
+end
+
Given %r{I am working on the ([[:alnum:]./_-]+) base branch$} do |branch|
create_git unless git_exists?
@@ -54,6 +79,11 @@ Given %r{I am working on the ([[:alnum:]./_-]+) base branch$} do |branch|
end
end
+Given %r{^I checkout the ([[:alnum:]~.-]+) tag$} do |tag|
+ create_git unless git_exists?
+ fatal_system "git checkout --quiet #{tag}"
+end
+
Given %r{I am working on the ([[:alnum:]./_-]+) branch based on ([[:alnum:]./_-]+)$} do |branch, base|
create_git unless git_exists?
@@ -66,12 +96,12 @@ Given %r{I am working on the ([[:alnum:]./_-]+) branch based on ([[:alnum:]./_-]
end
end
-When /^I successfully run ([[:alnum:]-]+)$/ do |command|
+When /^I successfully run "?([[:alnum:] -]+)"?$/ do |command|
@output = `#{File.expand_path("../../../auto/scripts/#{command}", __FILE__)}`
raise StandardError.new("#{command} failed. Exit code: #{$?}") if $? != 0
end
-When /^I run ([[:alnum:]-]+)$/ do |command|
+When /^I run "?([[:alnum:] -]+)"?$/ do |command|
@output = `#{File.expand_path("../../../auto/scripts/#{command}", __FILE__)}`
@exit_code = $?.exitstatus
end
@@ -113,3 +143,11 @@ end
Given(/^the config\/base_branch file is empty$/) do
File.truncate('config/base_branch', 0)
end
+
+Then(/^I should see the ([[:alnum:].-]+) tagged snapshot$/) do |tag|
+ @output.should have_tagged_snapshot(tag)
+end
+
+Then(/^I should see a time\-based snapshot$/) do
+ @output.should have_time_based_snapshot()
+end
diff --git a/cucumber/features/step_definitions/checks.rb b/cucumber/features/step_definitions/checks.rb
index 423b8390..142141a8 100644
--- a/cucumber/features/step_definitions/checks.rb
+++ b/cucumber/features/step_definitions/checks.rb
@@ -35,10 +35,6 @@ Then /^the shipped (?:Debian repository key|OpenPGP key ([A-Z0-9]+)) will be val
end
end
-Then /^I double-click the Report an Error launcher on the desktop$/ do
- @screen.wait_and_double_click('DesktopReportAnError.png', 30)
-end
-
Then /^the live user has been setup by live\-boot$/ do
assert($vm.execute("test -e /var/lib/live/config/user-setup").success?,
"live-boot failed its user-setup")
@@ -69,20 +65,12 @@ Then /^the live user owns its home dir and it has normal permissions$/ do
end
Then /^no unexpected services are listening for network connections$/ do
- netstat_cmd = $vm.execute("netstat -ltupn")
- assert netstat_cmd.success?
- for line in netstat_cmd.stdout.chomp.split("\n") do
+ for line in $vm.execute_successfully("ss -ltupn").stdout.chomp.split("\n") do
splitted = line.split(/[[:blank:]]+/)
proto = splitted[0]
- if proto == "tcp"
- proc_index = 6
- elsif proto == "udp"
- proc_index = 5
- else
- next
- end
- laddr, lport = splitted[3].split(":")
- proc = splitted[proc_index].split("/")[1]
+ next unless ['tcp', 'udp'].include?(proto)
+ laddr, lport = splitted[4].split(":")
+ proc = /users:\(\("([^"]+)"/.match(splitted[6])[1]
# Services listening on loopback is not a threat
if /127(\.[[:digit:]]{1,3}){3}/.match(laddr).nil?
if SERVICES_EXPECTED_ON_ALL_IFACES.include? [proc, laddr, lport] or
@@ -101,61 +89,58 @@ When /^Tails has booted a 64-bit kernel$/ do
"Tails has not booted a 64-bit kernel.")
end
-Then /^there is no screenshot in the live user's Pictures directory$/ do
- pictures_directory = "/home/#{LIVE_USER}/Pictures"
- assert($vm.execute(
- "find '#{pictures_directory}' -name 'Screenshot*.png' -maxdepth 1"
- ).stdout.empty?,
- "Existing screenshots were found in the live user's Pictures directory.")
-end
-
-Then /^a screenshot is saved to the live user's Pictures directory$/ do
- pictures_directory = "/home/#{LIVE_USER}/Pictures"
- try_for(10, :msg=> "No screenshot was created in #{pictures_directory}") do
- !$vm.execute(
- "find '#{pictures_directory}' -name 'Screenshot*.png' -maxdepth 1"
- ).stdout.empty?
- end
-end
-
Then /^the VirtualBox guest modules are available$/ do
assert($vm.execute("modinfo vboxguest").success?,
"The vboxguest module is not available.")
end
-Given /^I setup a filesystem share containing a sample PDF$/ do
- shared_pdf_dir_on_host = "#{$config["TMPDIR"]}/shared_pdf_dir"
- @shared_pdf_dir_on_guest = "/tmp/shared_pdf_dir"
- FileUtils.mkdir_p(shared_pdf_dir_on_host)
- Dir.glob("#{MISC_FILES_DIR}/*.pdf") do |pdf_file|
- FileUtils.cp(pdf_file, shared_pdf_dir_on_host)
- end
- add_after_scenario_hook { FileUtils.rm_r(shared_pdf_dir_on_host) }
- $vm.add_share(shared_pdf_dir_on_host, @shared_pdf_dir_on_guest)
-end
-
Then /^the support documentation page opens in Tor Browser$/ do
- @screen.wait("SupportDocumentation#{@language}.png", 120)
-end
-
-Then /^MAT can clean some sample PDF file$/ do
- for pdf_on_host in Dir.glob("#{MISC_FILES_DIR}/*.pdf") do
- pdf_name = File.basename(pdf_on_host)
- pdf_on_guest = "/home/#{LIVE_USER}/#{pdf_name}"
- step "I copy \"#{@shared_pdf_dir_on_guest}/#{pdf_name}\" to \"#{pdf_on_guest}\" as user \"#{LIVE_USER}\""
- check_before = $vm.execute_successfully("mat --check '#{pdf_on_guest}'",
+ if @language == 'German'
+ expected_title = 'Tails - Hilfe & Support'
+ expected_heading = 'Die Dokumentation durchsuchen'
+ else
+ expected_title = 'Tails - Support'
+ expected_heading = 'Search the documentation'
+ end
+ step "\"#{expected_title}\" has loaded in the Tor Browser"
+ headings = @torbrowser
+ .child(expected_title, roleName: 'document frame')
+ .children(roleName: 'heading')
+ assert(
+ headings.any? { |heading| heading.text == expected_heading }
+ )
+end
+
+Given /^I plug and mount a USB drive containing a sample PNG$/ do
+ @png_dir = share_host_files(Dir.glob("#{MISC_FILES_DIR}/*.png"))
+end
+
+Then /^MAT can clean some sample PNG file$/ do
+ for png_on_host in Dir.glob("#{MISC_FILES_DIR}/*.png") do
+ png_name = File.basename(png_on_host)
+ png_on_guest = "/home/#{LIVE_USER}/#{png_name}"
+ step "I copy \"#{@png_dir}/#{png_name}\" to \"#{png_on_guest}\" as user \"#{LIVE_USER}\""
+ raw_check_cmd = "grep --quiet --fixed-strings --text " +
+ "'Created with GIMP' '#{png_on_guest}'"
+ assert($vm.execute(raw_check_cmd, user: LIVE_USER).success?,
+ 'The comment is not present in the PNG')
+ check_before = $vm.execute_successfully("mat --check '#{png_on_guest}'",
:user => LIVE_USER).stdout
- assert(check_before.include?("#{pdf_on_guest} is not clean"),
- "MAT failed to see that '#{pdf_on_host}' is dirty")
- $vm.execute_successfully("mat '#{pdf_on_guest}'", :user => LIVE_USER)
- check_after = $vm.execute_successfully("mat --check '#{pdf_on_guest}'",
+ assert(check_before.include?("#{png_on_guest} is not clean"),
+ "MAT failed to see that '#{png_on_host}' is dirty")
+ $vm.execute_successfully("mat '#{png_on_guest}'", :user => LIVE_USER)
+ check_after = $vm.execute_successfully("mat --check '#{png_on_guest}'",
:user => LIVE_USER).stdout
- assert(check_after.include?("#{pdf_on_guest} is clean"),
- "MAT failed to clean '#{pdf_on_host}'")
- $vm.execute_successfully("rm '#{pdf_on_guest}'")
+ assert(check_after.include?("#{png_on_guest} is clean"),
+ "MAT failed to clean '#{png_on_host}'")
+ assert($vm.execute(raw_check_cmd, user: LIVE_USER).failure?,
+ 'The comment is still present in the PNG')
+ $vm.execute_successfully("rm '#{png_on_guest}'")
end
end
+
+
Then /^AppArmor is enabled$/ do
assert($vm.execute("aa-status").success?, "AppArmor is not enabled")
end
@@ -184,13 +169,8 @@ def get_apparmor_status(pid)
end
Then /^the running process "(.+)" is confined with AppArmor in (complain|enforce) mode$/ do |process, mode|
- if process == 'i2p'
- $vm.execute_successfully('service i2p status')
- pid = $vm.file_content('/run/i2p/i2p.pid').chomp
- else
- assert($vm.has_process?(process), "Process #{process} not running.")
- pid = $vm.pidof(process)[0]
- end
+ assert($vm.has_process?(process), "Process #{process} not running.")
+ pid = $vm.pidof(process)[0]
assert_equal(mode, get_apparmor_status(pid))
end
@@ -238,12 +218,10 @@ Then /^tails-debugging-info is not susceptible to symlink attacks$/ do
end
When /^I disable all networking in the Tails Greeter$/ do
- begin
- @screen.click('TailsGreeterDisableAllNetworking.png')
- rescue FindFailed
- @screen.type(Sikuli::Key.PAGE_DOWN)
- @screen.click('TailsGreeterDisableAllNetworking.png')
- end
+ open_greeter_additional_settings()
+ @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
+ @screen.wait_and_click('TailsGreeterDisableAllNetworking.png', 10)
+ @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
end
Then /^the Tor Status icon tells me that Tor is( not)? usable$/ do |not_usable|
diff --git a/cucumber/features/step_definitions/common_steps.rb b/cucumber/features/step_definitions/common_steps.rb
index feec90e2..ca7604f8 100644
--- a/cucumber/features/step_definitions/common_steps.rb
+++ b/cucumber/features/step_definitions/common_steps.rb
@@ -8,24 +8,6 @@ def post_vm_start_hook
@screen.click_point(@screen.w-1, @screen.h/2)
end
-def activate_filesystem_shares
- # XXX-9p: First of all, filesystem shares cannot be mounted while we
- # do a snapshot save+restore, so unmounting+remounting them seems
- # like a good idea. However, the 9p modules get into a broken state
- # during the save+restore, so we also would like to unload+reload
- # them, but loading of 9pnet_virtio fails after a restore with
- # "probe of virtio2 failed with error -2" (in dmesg) which makes the
- # shares unavailable. Hence we leave this code commented for now.
- #for mod in ["9pnet_virtio", "9p"] do
- # $vm.execute("modprobe #{mod}")
- #end
-
- $vm.list_shares.each do |share|
- $vm.execute("mkdir -p #{share}")
- $vm.execute("mount -t 9p -o trans=virtio #{share} #{share}")
- end
-end
-
def context_menu_helper(top, bottom, menu_item)
try_for(60) do
t = @screen.wait(top, 10)
@@ -41,64 +23,12 @@ def context_menu_helper(top, bottom, menu_item)
end
end
-def deactivate_filesystem_shares
- $vm.list_shares.each do |share|
- $vm.execute("umount #{share}")
- end
-
- # XXX-9p: See XXX-9p above
- #for mod in ["9p", "9pnet_virtio"] do
- # $vm.execute("modprobe -r #{mod}")
- #end
-end
-
-# This helper requires that the notification image is the one shown in
-# the notification applet's list, not the notification pop-up.
-def robust_notification_wait(notification_image, time_to_wait)
- error_msg = "Didn't not manage to open the notification applet"
- wait_start = Time.now
- try_for(time_to_wait, :delay => 0, :msg => error_msg) do
- @screen.hide_cursor
- @screen.click("GnomeNotificationApplet.png")
- @screen.wait("GnomeNotificationAppletOpened.png", 10)
- end
-
- error_msg = "Didn't not see notification '#{notification_image}'"
- time_to_wait -= (Time.now - wait_start).ceil
- try_for(time_to_wait, :delay => 0, :msg => error_msg) do
- found = false
- entries = @screen.findAll("GnomeNotificationEntry.png")
- while(entries.hasNext) do
- entry = entries.next
- @screen.hide_cursor
- @screen.click(entry)
- close_entry = @screen.wait("GnomeNotificationEntryClose.png", 10)
- if @screen.exists(notification_image)
- found = true
- @screen.click(close_entry)
- break
- else
- @screen.click(entry)
- end
- end
- found
- end
-
- # Click anywhere to close the notification applet
- @screen.hide_cursor
- @screen.click("GnomeApplicationsMenu.png")
- @screen.hide_cursor
-end
-
def post_snapshot_restore_hook
# FIXME -- we've got a brain-damaged version of this, unlike Tails, so it breaks after restores at present
# that being the case, let's not worry until we actually miss the feature
#$vm.wait_until_remote_shell_is_up
post_vm_start_hook
- # XXX-9p: See XXX-9p above
- #activate_filesystem_shares
-
# debian-TODO: move to tor feature
# The guest's Tor's circuits' states are likely to get out of sync
# with the other relays, so we ensure that we have fresh circuits.
@@ -106,18 +36,10 @@ def post_snapshot_restore_hook
#if $vm.has_network?
# if $vm.execute("systemctl --quiet is-active tor@default.service").success?
# $vm.execute("systemctl stop tor@default.service")
- # $vm.execute("rm -f /var/log/tor/log")
# $vm.execute("systemctl --no-block restart tails-tor-has-bootstrapped.target")
# $vm.host_to_guest_time_sync
- # $vm.spawn("restart-tor")
+ # $vm.execute("systemctl start tor@default.service")
# wait_until_tor_is_working
- # if $vm.file_content('/proc/cmdline').include?(' i2p')
- # $vm.execute_successfully('/usr/local/sbin/tails-i2p stop')
- # # we "killall tails-i2p" to prevent multiple
- # # copies of the script from running
- # $vm.execute_successfully('killall tails-i2p')
- # $vm.spawn('/usr/local/sbin/tails-i2p start')
- # end
# end
#else
# $vm.host_to_guest_time_sync
@@ -137,10 +59,6 @@ Given /^a computer$/ do
$vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
end
-Given /^the computer has (\d+) ([[:alpha:]]+) of RAM$/ do |size, unit|
- $vm.set_ram_size(size, unit)
-end
-
Then /^the VM shuts down within (\d+) minutes$/ do |mins|
timeout = 60*mins.to_i
try_for(timeout, :msg => "VM is still running after #{timeout} seconds") do
@@ -156,7 +74,7 @@ Given /^the computer is set to boot from (.+?) drive$/ do |type|
$vm.set_disk_boot(JOB_NAME, type.downcase)
end
-Given /^I (temporarily )?create a (\d+) ([[:alpha:]]+) disk named "([^"]+)"$/ do |temporary, size, unit, name|
+Given /^I (temporarily )?create an? (\d+) ([[:alpha:]]+) disk named "([^"]+)"$/ do |temporary, size, unit, name|
$vm.storage.create_new_disk(name, {:size => size, :unit => unit,
:type => "qcow2"})
add_after_scenario_hook { $vm.storage.delete_volume(name) } if temporary
@@ -184,6 +102,11 @@ Given /^the network is unplugged$/ do
$vm.unplug_network
end
+Given /^the network connection is ready(?: within (\d+) seconds)?$/ do |timeout|
+ timeout ||= 30
+ try_for(timeout.to_i) { $vm.has_network? }
+end
+
Given /^the hardware clock is set to "([^"]*)"$/ do |time|
$vm.set_hardware_clock(DateTime.parse(time).to_time)
end
@@ -220,51 +143,43 @@ end
Given /^I start Tails( from DVD)?( with network unplugged)?( and I login)?$/ do |dvd_boot, network_unplugged, do_login|
step "the computer is set to boot from the Tails DVD" if dvd_boot
- if network_unplugged.nil?
- step "the network is plugged"
- else
+ if network_unplugged
step "the network is unplugged"
+ else
+ step "the network is plugged"
end
step "I start the computer"
step "the computer boots Tails"
if do_login
step "I log in to a new session"
- step "Tails seems to have booted normally"
- if network_unplugged.nil?
- step "Tor is ready"
+ if network_unplugged
step "all notifications have disappeared"
- step "available upgrades have been checked"
else
+ step "Tor is ready"
step "all notifications have disappeared"
+ step "available upgrades have been checked"
end
end
end
-Given /^I start Tails from (.+?) drive "(.+?)"(| with network unplugged)( and I login(| with(| read-only) persistence enabled))?$/ do |drive_type, drive_name, network_unplugged, do_login, persistence_on, persistence_ro|
+Given /^I start Tails from (.+?) drive "(.+?)"( with network unplugged)?( and I login( with persistence enabled)?)?$/ do |drive_type, drive_name, network_unplugged, do_login, persistence_on|
step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\""
- if network_unplugged.empty?
- step "the network is plugged"
- else
+ if network_unplugged
step "the network is unplugged"
+ else
+ step "the network is plugged"
end
step "I start the computer"
step "the computer boots Tails"
if do_login
- if ! persistence_on.empty?
- if persistence_ro.empty?
- step "I enable persistence"
- else
- step "I enable read-only persistence"
- end
- end
+ step "I enable persistence" if persistence_on
step "I log in to a new session"
- step "Tails seems to have booted normally"
- if network_unplugged.empty?
- step "Tor is ready"
+ if network_unplugged
step "all notifications have disappeared"
- step "available upgrades have been checked"
else
+ step "Tor is ready"
step "all notifications have disappeared"
+ step "available upgrades have been checked"
end
end
end
@@ -691,16 +606,16 @@ Given /^I should see a ([a-zA-Z]*) Login prompt$/ do |style|
@screen.waitAny(loginPrompt[style], 20 * 60)
end
-def bootsplash
+def boot_menu_cmdline_image
case @os_loader
when "UEFI"
- 'TailsBootSplashUEFI.png'
+ 'TailsBootMenuKernelCmdlineUEFI.png'
else
'd-i8_bootsplash.png'
end
end
-def bootsplash_tab_msg
+def boot_menu_tab_msg_image
case @os_loader
when "UEFI"
'TailsBootSplashTabMsgUEFI.png'
@@ -719,21 +634,12 @@ def bootsplash_tab_msg
end
Given /^the computer (re)?boots Tails$/ do |reboot|
-
- boot_timeout = 30
- # We need some extra time for memory wiping if rebooting
- boot_timeout += 90 if reboot
-
- @screen.wait(bootsplash, boot_timeout)
- @screen.wait(bootsplash_tab_msg, 10)
- @screen.type(Sikuli::Key.TAB)
- @screen.waitVanish(bootsplash_tab_msg, 1)
-
+ step "Tails is at the boot menu's cmdline" + (reboot ? ' after rebooting' : '')
@screen.type(" autotest_never_use_this_option blacklist=psmouse #{@boot_options}" +
Sikuli::Key.ENTER)
- @screen.wait("DebianLive#{version}Greeter.png", 5*60)
- @vm.wait_until_remote_shell_is_up
- activate_filesystem_shares
+ @screen.wait('TailsGreeter.png', 5*60)
+ $vm.wait_until_remote_shell_is_up
+ step 'I configure Tails to use a simulated Tor network'
end
Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
@@ -741,37 +647,60 @@ Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
when 'German'
@language = "German"
@screen.wait_and_click('TailsGreeterLanguage.png', 10)
- @screen.wait_and_click("TailsGreeterLanguage#{@language}.png", 10)
+ @screen.wait('TailsGreeterLanguagePopover.png', 10)
+ @screen.type(@language)
+ sleep(2) # Gtk needs some time to filter the results
+ @screen.type(Sikuli::Key.ENTER)
@screen.wait_and_click("TailsGreeterLoginButton#{@language}.png", 10)
when ''
@screen.wait_and_click('TailsGreeterLoginButton.png', 10)
else
raise "Unsupported language: #{lang}"
end
+ step 'Tails Greeter has applied all settings'
+ step 'the Tails desktop is ready'
end
-Given /^I set sudo password "([^"]*)"$/ do |password|
- @sudo_password = password
- next if @skip_steps_while_restoring_background
- #@screen.wait("TailsGreeterAdminPassword.png", 20)
+def open_greeter_additional_settings
+ @screen.click('TailsGreeterAddMoreOptions.png')
+ @screen.wait('TailsGreeterAdditionalSettingsDialog.png', 10)
+end
+
+Given /^I open Tails Greeter additional settings dialog$/ do
+ open_greeter_additional_settings()
+end
+
+Given /^I enable the specific Tor configuration option$/ do
+ open_greeter_additional_settings()
+ @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
+ @screen.wait_and_click("TailsGreeterSpecificTorConfiguration.png", 10)
+ @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
+end
+
+Given /^I set an administration password$/ do
+ open_greeter_additional_settings()
+ @screen.wait_and_click("TailsGreeterAdminPassword.png", 20)
@screen.type(@sudo_password)
@screen.type(Sikuli::Key.TAB)
@screen.type(@sudo_password)
+ @screen.type(Sikuli::Key.ENTER)
end
-Given /^Tails Greeter has dealt with the sudo password$/ do
- f1 = "/etc/sudoers.d/tails-greeter"
- f2 = "#{f1}-no-password-lecture"
- try_for(20) {
- $vm.execute("test -e '#{f1}' -o -e '#{f2}'").success?
+Given /^Tails Greeter has applied all settings$/ do
+ # I.e. it is done with PostLogin, which is ensured to happen before
+ # a logind session is opened for LIVE_USER.
+ try_for(120) {
+ $vm.execute_successfully("loginctl").stdout
+ .match(/^\s*\S+\s+\d+\s+#{LIVE_USER}\s+seat\d+\s+\S+\s*$/) != nil
}
end
Given /^the Tails desktop is ready$/ do
desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
- # We wait for the Florence icon to be displayed to ensure reliable systray icon clicking.
- @screen.wait("GnomeSystrayFlorence.png", 180)
@screen.wait(desktop_started_picture, 180)
+ # We wait for the Florence icon to be displayed to ensure reliable systray icon clicking.
+ @screen.wait("GnomeSystrayFlorence.png", 30)
+ @screen.wait("DesktopTailsDocumentation.png", 30)
# Disable screen blanking since we sometimes need to wait long
# enough for it to activate, which can mess with Sikuli wait():ing
# for some image.
@@ -779,14 +708,22 @@ Given /^the Tails desktop is ready$/ do
'gsettings set org.gnome.desktop.session idle-delay 0',
:user => LIVE_USER
)
+ # We need to enable the accessibility toolkit for dogtail.
+ $vm.execute_successfully(
+ 'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
+ :user => LIVE_USER,
+ )
end
-Then /^Tails seems to have booted normally$/ do
- step "the Tails desktop is ready"
-end
-
-When /^I see the 'Tor is ready' notification$/ do
- robust_notification_wait('TorIsReadyNotification.png', 300)
+When /^I see the "(.+)" notification(?: after at most (\d+) seconds)?$/ do |title, timeout|
+ timeout = timeout ? timeout.to_i : nil
+ gnome_shell = Dogtail::Application.new('gnome-shell')
+ notification_list = gnome_shell.child(
+ 'No Notifications', roleName: 'label', showingOnly: false
+ ).parent.parent
+ try_for(timeout) do
+ notification_list.child?(title, roleName: 'label', showingOnly: false)
+ end
end
Given /^Tor is ready$/ do
@@ -803,43 +740,63 @@ Given /^Tor has built a circuit$/ do
end
Given /^the time has synced$/ do
- ["/var/run/tordate/done", "/var/run/htpdate/success"].each do |file|
+ ["/run/tordate/done", "/run/htpdate/success"].each do |file|
try_for(300) { $vm.execute("test -e #{file}").success? }
end
end
Given /^available upgrades have been checked$/ do
try_for(300) {
- $vm.execute("test -e '/var/run/tails-upgrader/checked_upgrades'").success?
+ $vm.execute("test -e '/run/tails-upgrader/checked_upgrades'").success?
}
end
-Given /^the Tor Browser has started$/ do
- tor_browser_picture = "TorBrowserWindow.png"
- @screen.wait(tor_browser_picture, 60)
+When /^I start the Tor Browser( in offline mode)?$/ do |offline|
+ step 'I start "Tor Browser" via GNOME Activities Overview'
+ if offline
+ offline_prompt = Dogtail::Application.new('zenity')
+ .dialog('Tor is not ready')
+ offline_prompt.button('Start Tor Browser').click
+ end
+ step "the Tor Browser has started#{offline}"
+ if offline
+ step 'the Tor Browser shows the "The proxy server is refusing connections" error'
+ end
end
-Given /^the Tor Browser (?:has started and )?load(?:ed|s) the (startup page|Tails roadmap)$/ do |page|
+Given /^the Tor Browser has started( in offline mode)?$/ do |offline|
+ try_for(60) do
+ @torbrowser = Dogtail::Application.new('Firefox')
+ @torbrowser.child?(roleName: 'frame', recursive: false)
+ end
+end
+
+Given /^the Tor Browser loads the (startup page|Tails roadmap)$/ do |page|
case page
when "startup page"
- picture = "TorBrowserStartupPage.png"
+ title = 'Tails - News'
when "Tails roadmap"
- picture = "TorBrowserTailsRoadmap.png"
+ title = 'Roadmap - Tails - RiseupLabs Code Repository'
else
raise "Unsupported page: #{page}"
end
- step "the Tor Browser has started"
- @screen.wait(picture, 120)
+ step "\"#{title}\" has loaded in the Tor Browser"
end
-Given /^the Tor Browser has started in offline mode$/ do
- @screen.wait("TorBrowserOffline.png", 60)
+When /^I request a new identity using Torbutton$/ do
+ @screen.wait_and_click('TorButtonIcon.png', 30)
+ @screen.wait_and_click('TorButtonNewIdentity.png', 30)
+end
+
+When /^I acknowledge Torbutton's New Identity confirmation prompt$/ do
+ @screen.wait('GnomeQuestionDialogIcon.png', 30)
+ step 'I type "y"'
end
Given /^I add a bookmark to eff.org in the Tor Browser$/ do
url = "https://www.eff.org"
step "I open the address \"#{url}\" in the Tor Browser"
- @screen.wait("TorBrowserOffline.png", 5)
+ step 'the Tor Browser shows the "The proxy server is refusing connections" error'
@screen.type("d", Sikuli::KeyModifier.CTRL)
@screen.wait("TorBrowserBookmarkPrompt.png", 10)
@screen.type(url + Sikuli::Key.ENTER)
@@ -851,24 +808,18 @@ Given /^the Tor Browser has a bookmark to eff.org$/ do
end
Given /^all notifications have disappeared$/ do
- next if not(@screen.exists("GnomeNotificationApplet.png"))
- @screen.click("GnomeNotificationApplet.png")
- @screen.wait("GnomeNotificationAppletOpened.png", 10)
- begin
- entries = @screen.findAll("GnomeNotificationEntry.png")
- while(entries.hasNext) do
- entry = entries.next
- @screen.hide_cursor
- @screen.click(entry)
- @screen.wait_and_click("GnomeNotificationEntryClose.png", 10)
+ # These magic coordinates always locates GNOME's clock in the top
+ # bar, which when clicked opens the calendar.
+ x, y = 512, 10
+ gnome_shell = Dogtail::Application.new('gnome-shell')
+ retry_action(10, recovery_proc: Proc.new { @screen.type(Sikuli::Key.ESC) }) do
+ @screen.click_point(x, y)
+ unless gnome_shell.child?('No Notifications', roleName: 'label')
+ @screen.click('GnomeCloseAllNotificationsButton.png')
end
- rescue FindFailed
- # No notifications, so we're good to go.
+ gnome_shell.child?('No Notifications', roleName: 'label')
end
- @screen.hide_cursor
- # Click anywhere to close the notification applet
- @screen.click("GnomeApplicationsMenu.png")
- @screen.hide_cursor
+ @screen.type(Sikuli::Key.ESC)
end
Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, image, time|
@@ -890,24 +841,31 @@ Then /^I (do not )?see the "([^"]*)" screen, after at most (\d+) seconds$/ do |n
end
Then /^all Internet traffic has only flowed through Tor$/ do
- leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
- :accepted_hosts => get_all_tor_nodes)
- leaks.assert_no_leaks
+ allowed_hosts = allowed_hosts_under_tor_enforcement
+ assert_all_connections(@sniffer.pcap_file) do |c|
+ allowed_hosts.include?({ address: c.daddr, port: c.dport })
+ end
end
Given /^I enter the sudo password in the pkexec prompt$/ do
step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
end
-def deal_with_polkit_prompt (image, password)
+def deal_with_polkit_prompt(password, opts = {})
+ opts[:expect_success] ||= true
+ image = 'PolicyKitAuthPrompt.png'
@screen.wait(image, 60)
@screen.type(password)
@screen.type(Sikuli::Key.ENTER)
- @screen.waitVanish(image, 10)
+ if opts[:expect_success]
+ @screen.waitVanish(image, 20)
+ else
+ @screen.wait('PolicyKitAuthFailure.png', 20)
+ end
end
Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
- deal_with_polkit_prompt('PolicyKitAuthPrompt.png', password)
+ deal_with_polkit_prompt(password)
end
Given /^process "([^"]+)" is (not )?running$/ do |process, not_running|
@@ -939,19 +897,17 @@ Given /^I kill the process "([^"]+)"$/ do |process|
}
end
-Then /^Tails eventually shuts down$/ do
- nr_gibs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
- timeout = nr_gibs_of_ram*5*60
- try_for(timeout, :msg => "VM is still running after #{timeout} seconds") do
- ! $vm.is_running?
+Then /^Tails eventually (shuts down|restarts)$/ do |mode|
+ try_for(3*60) do
+ if mode == 'restarts'
+ @screen.find('TailsGreeter.png')
+ true
+ else
+ ! $vm.is_running?
+ end
end
end
-Then /^Tails eventually restarts$/ do
- nr_gibs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
- @screen.wait('TailsBootSplash.png', nr_gibs_of_ram*5*60)
-end
-
Given /^I shutdown Tails and wait for the computer to power off$/ do
$vm.spawn("poweroff")
step 'Tails eventually shuts down'
@@ -960,6 +916,11 @@ end
When /^I request a shutdown using the emergency shutdown applet$/ do
@screen.hide_cursor
@screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
+ # Sometimes the next button too fast, before the menu has settled
+ # down to its final size and the icon we want to click is in its
+ # final position. dogtail might allow us to fix that, but given how
+ # rare this problem is, it's not worth the effort.
+ step 'I wait 5 seconds'
@screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10)
end
@@ -970,57 +931,38 @@ end
When /^I request a reboot using the emergency shutdown applet$/ do
@screen.hide_cursor
@screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
+ # See comment on /^I request a shutdown using the emergency shutdown applet$/
+ # that explains why we need to wait.
+ step 'I wait 5 seconds'
@screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10)
end
-Given /^package "([^"]+)" is installed$/ do |package|
+Given /^the package "([^"]+)" is installed$/ do |package|
assert($vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?,
"Package '#{package}' is not installed")
end
-When /^I start the Tor Browser$/ do
- step 'I start "TorBrowser" via the GNOME "Internet" applications menu'
-end
-
-When /^I request a new identity using Torbutton$/ do
- @screen.wait_and_click('TorButtonIcon.png', 30)
- @screen.wait_and_click('TorButtonNewIdentity.png', 30)
-end
-
-When /^I acknowledge Torbutton's New Identity confirmation prompt$/ do
- @screen.wait('GnomeQuestionDialogIcon.png', 30)
- step 'I type "y"'
-end
-
-When /^I start the Tor Browser in offline mode$/ do
- step "I start the Tor Browser"
- @screen.wait_and_click("TorBrowserOfflinePrompt.png", 10)
- @screen.click("TorBrowserOfflinePromptStart.png")
-end
-
-Given /^I add a wired DHCP NetworkManager connection called "([^"]+)"$/ do |con_name|
- con_content = <<EOF
-[802-3-ethernet]
-duplex=full
-
+Given /^I add a ([a-z0-9.]+ |)wired DHCP NetworkManager connection called "([^"]+)"$/ do |version, con_name|
+ if version and version == '2.x'
+ con_content = <<EOF
[connection]
id=#{con_name}
-uuid=bbc60668-1be0-11e4-a9c6-2f1ce0e75bf1
-type=802-3-ethernet
-timestamp=1395406011
-
-[ipv6]
-method=auto
-
-[ipv4]
-method=auto
+uuid=b04afa94-c3a1-41bf-aa12-1a743d964162
+interface-name=eth0
+type=ethernet
EOF
- con_content.split("\n").each do |line|
- $vm.execute("echo '#{line}' >> /tmp/NM.#{con_name}")
+ con_file = "/etc/NetworkManager/system-connections/#{con_name}"
+ $vm.file_overwrite(con_file, con_content)
+ $vm.execute_successfully("chmod 600 '#{con_file}'")
+ $vm.execute_successfully("nmcli connection load '#{con_file}'")
+ elsif version and version == '3.x'
+ raise "Unsupported version '#{version}'"
+ else
+ $vm.execute_successfully(
+ "nmcli connection add con-name #{con_name} " + \
+ "type ethernet autoconnect yes ifname eth0"
+ )
end
- con_file = "/etc/NetworkManager/system-connections/#{con_name}"
- $vm.execute("install -m 0600 '/tmp/NM.#{con_name}' '#{con_file}'")
- $vm.execute_successfully("nmcli connection load '#{con_file}'")
try_for(10) {
nm_con_list = $vm.execute("nmcli --terse --fields NAME connection show").stdout
nm_con_list.split("\n").include? "#{con_name}"
@@ -1035,8 +977,8 @@ Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
end
When /^I start and focus GNOME Terminal$/ do
- step 'I start "Terminal" via the GNOME "Utilities" applications menu'
- @screen.wait('GnomeTerminalWindow.png', 20)
+ step 'I start "GNOME Terminal" via GNOME Activities Overview'
+ @screen.wait('GnomeTerminalWindow.png', 40)
end
When /^I run "([^"]+)" in GNOME Terminal$/ do |command|
@@ -1091,57 +1033,12 @@ Then /^persistence for "([^"]+)" is (|not )enabled$/ do |app, enabled|
end
end
-def gnome_app_menu_click_helper(click_me, verify_me = nil)
- try_for(30) do
- @screen.hide_cursor
- # The sensitivity for submenus to open by just hovering past them
- # is extremely high, and may result in the wrong one
- # opening. Hence we better avoid hovering over undesired submenus
- # entirely by "approaching" the menu strictly horizontally.
- r = @screen.wait(click_me, 10)
- @screen.hover_point(@screen.w, r.getY)
- @screen.click(r)
- @screen.wait(verify_me, 10) if verify_me
- return
- end
-end
-
-Given /^I start "([^"]+)" via the GNOME "([^"]+)" applications menu$/ do |app, submenu|
- menu_button = "GnomeApplicationsMenu.png"
- sub_menu_entry = "GnomeApplications" + submenu + ".png"
- application_entry = "GnomeApplications" + app + ".png"
- try_for(120) do
- begin
- gnome_app_menu_click_helper(menu_button, sub_menu_entry)
- gnome_app_menu_click_helper(sub_menu_entry, application_entry)
- gnome_app_menu_click_helper(application_entry)
- rescue Exception => e
- # Close menu, if still open
- @screen.type(Sikuli::Key.ESC)
- raise e
- end
- true
- end
-end
-
-Given /^I start "([^"]+)" via the GNOME "([^"]+)"\/"([^"]+)" applications menu$/ do |app, submenu, subsubmenu|
- menu_button = "GnomeApplicationsMenu.png"
- sub_menu_entry = "GnomeApplications" + submenu + ".png"
- sub_sub_menu_entry = "GnomeApplications" + subsubmenu + ".png"
- application_entry = "GnomeApplications" + app + ".png"
- try_for(120) do
- begin
- gnome_app_menu_click_helper(menu_button, sub_menu_entry)
- gnome_app_menu_click_helper(sub_menu_entry, sub_sub_menu_entry)
- gnome_app_menu_click_helper(sub_sub_menu_entry, application_entry)
- gnome_app_menu_click_helper(application_entry)
- rescue Exception => e
- # Close menu, if still open
- @screen.type(Sikuli::Key.ESC)
- raise e
- end
- true
- end
+Given /^I start "([^"]+)" via GNOME Activities Overview$/ do |app_name|
+ @screen.wait('GnomeApplicationsMenu.png', 10)
+ $vm.execute_successfully('xdotool key Super', user: LIVE_USER)
+ @screen.wait('GnomeActivitiesOverview.png', 10)
+ @screen.type(app_name)
+ @screen.type(Sikuli::Key.ENTER, Sikuli::KeyModifier.CTRL)
end
When /^I type "([^"]+)"$/ do |string|
@@ -1198,8 +1095,14 @@ When /^(no|\d+) application(?:s?) (?:is|are) playing audio(?:| after (\d+) secon
assert_equal(nb.to_i, pulseaudio_sink_inputs)
end
-When /^I double-click on the "Tails documentation" link on the Desktop$/ do
- @screen.wait_and_double_click("DesktopTailsDocumentationIcon.png", 10)
+When /^I double-click on the (Tails documentation|Report an Error) launcher on the desktop$/ do |launcher|
+ image = 'Desktop' + launcher.split.map { |s| s.capitalize } .join + '.png'
+ info = xul_application_info('Tor Browser')
+ # Sometimes the double-click is lost (#12131).
+ retry_action(10) do
+ @screen.wait_and_double_click(image, 10) if $vm.execute("pgrep --uid #{info[:user]} --full --exact '#{info[:cmd_regex]}'").failure?
+ step 'the Tor Browser has started'
+ end
end
When /^I click the blocked video icon$/ do
@@ -1265,9 +1168,9 @@ When /^I can print the current page as "([^"]+[.]pdf)" to the (default downloads
end
Given /^a web server is running on the LAN$/ do
- web_server_ip_addr = $vmnet.bridge_ip_addr
- web_server_port = 8000
- @web_server_url = "http://#{web_server_ip_addr}:#{web_server_port}"
+ @web_server_ip_addr = $vmnet.bridge_ip_addr
+ @web_server_port = 8000
+ @web_server_url = "http://#{@web_server_ip_addr}:#{@web_server_port}"
web_server_hello_msg = "Welcome to the LAN web server!"
# I've tested ruby Thread:s, fork(), etc. but nothing works due to
@@ -1282,14 +1185,15 @@ Given /^a web server is running on the LAN$/ do
require "webrick"
STDOUT.reopen("/dev/null", "w")
STDERR.reopen("/dev/null", "w")
- server = WEBrick::HTTPServer.new(:BindAddress => "#{web_server_ip_addr}",
- :Port => #{web_server_port},
+ server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
+ :Port => #{@web_server_port},
:DocumentRoot => "/dev/null")
server.mount_proc("/") do |req, res|
res.body = "#{web_server_hello_msg}"
end
server.start
EOF
+ add_lan_host(@web_server_ip_addr, @web_server_port)
proc = IO.popen(['ruby', '-e', code])
try_for(10, :msg => "It seems the LAN web server failed to start") do
Process.kill(0, proc.pid) == 1
@@ -1365,8 +1269,7 @@ When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"(?: after at m
end
Then /^I force Tor to use a new circuit$/ do
- debug_log("Forcing new Tor circuit...")
- $vm.execute_successfully('tor_control_send "signal NEWNYM"', :libs => 'tor')
+ force_new_tor_circuit
end
When /^I eject the boot medium$/ do
@@ -1374,7 +1277,7 @@ When /^I eject the boot medium$/ do
dev_type = device_info(dev)['ID_TYPE']
case dev_type
when 'cd'
- $vm.remove_cdrom
+ $vm.eject_cdrom
when 'disk'
boot_disk_name = $vm.disk_name(dev)
$vm.unplug_drive(boot_disk_name)
@@ -1382,3 +1285,103 @@ When /^I eject the boot medium$/ do
raise "Unsupported medium type '#{dev_type}' for boot device '#{dev}'"
end
end
+
+Given /^Tails is fooled to think it is running version (.+)$/ do |version|
+ $vm.execute_successfully(
+ "sed -i " +
+ "'s/^TAILS_VERSION_ID=.*$/TAILS_VERSION_ID=\"#{version}\"/' " +
+ "/etc/os-release"
+ )
+end
+
+Then /^Tails is running version (.+)$/ do |version|
+ v1 = $vm.execute_successfully('tails-version').stdout.split.first
+ assert_equal(version, v1, "The version doesn't match tails-version's output")
+ v2 = $vm.file_content('/etc/os-release')
+ .scan(/TAILS_VERSION_ID="(#{version})"/).flatten.first
+ assert_equal(version, v2, "The version doesn't match /etc/os-release")
+end
+
+def share_host_files(files)
+ files = [files] if files.class == String
+ assert_equal(Array, files.class)
+ disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
+ # Let's add some extra space for filesysten overhead etc.
+ disk_size += [convert_to_bytes(1, 'MiB'), (disk_size * 0.10).ceil].max
+ disk = random_alpha_string(10)
+ step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
+ step "I create a gpt partition labeled \"#{disk}\" with an ext4 " +
+ "filesystem on disk \"#{disk}\""
+ $vm.storage.guestfs_disk_helper(disk) do |g, _|
+ partition = g.list_partitions().first
+ g.mount(partition, "/")
+ files.each { |f| g.upload(f, "/" + File.basename(f)) }
+ end
+ step "I plug USB drive \"#{disk}\""
+ mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
+ dev = $vm.disk_dev(disk)
+ partition = dev + '1'
+ $vm.execute_successfully("mount #{partition} #{mount_dir}")
+ $vm.execute_successfully("chmod -R a+rX '#{mount_dir}'")
+ return mount_dir
+end
+
+def mount_USB_drive(disk, fs_options = {})
+ fs_options[:encrypted] ||= false
+ @tmp_usb_drive_mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
+ dev = $vm.disk_dev(disk)
+ partition = dev + '1'
+ if fs_options[:encrypted]
+ password = fs_options[:password]
+ assert_not_nil(password)
+ luks_mapping = "#{disk}_unlocked"
+ $vm.execute_successfully(
+ "echo #{password} | " +
+ "cryptsetup luksOpen #{partition} #{luks_mapping}"
+ )
+ $vm.execute_successfully(
+ "mount /dev/mapper/#{luks_mapping} #{@tmp_usb_drive_mount_dir}"
+ )
+ else
+ $vm.execute_successfully("mount #{partition} #{@tmp_usb_drive_mount_dir}")
+ end
+ @tmp_filesystem_disk = disk
+ @tmp_filesystem_options = fs_options
+ @tmp_filesystem_partition = partition
+ return @tmp_usb_drive_mount_dir
+end
+
+When(/^I plug and mount a (\d+) MiB USB drive with an? (.*)$/) do |size_MiB, fs|
+ disk_size = convert_to_bytes(size_MiB.to_i, 'MiB')
+ disk = random_alpha_string(10)
+ step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
+ step "I create a gpt partition labeled \"#{disk}\" with " +
+ "an #{fs} on disk \"#{disk}\""
+ step "I plug USB drive \"#{disk}\""
+ fs_options = {}
+ fs_options[:filesystem] = /(.*) filesystem/.match(fs)[1]
+ if /\bencrypted with password\b/.match(fs)
+ fs_options[:encrypted] = true
+ fs_options[:password] = /encrypted with password "([^"]+)"/.match(fs)[1]
+ end
+ mount_dir = mount_USB_drive(disk, fs_options)
+ @tmp_filesystem_size_b = convert_to_bytes(
+ avail_space_in_mountpoint_kB(mount_dir),
+ 'KB'
+ )
+end
+
+When(/^I mount the USB drive again$/) do
+ mount_USB_drive(@tmp_filesystem_disk, @tmp_filesystem_options)
+end
+
+When(/^I umount the USB drive$/) do
+ $vm.execute_successfully("umount #{@tmp_usb_drive_mount_dir}")
+ if @tmp_filesystem_options[:encrypted]
+ $vm.execute_successfully("cryptsetup luksClose #{@tmp_filesystem_disk}_unlocked")
+ end
+end
+
+When /^Tails system time is magically synchronized$/ do
+ $vm.host_to_guest_time_sync
+end
diff --git a/cucumber/features/step_definitions/dhcp.rb b/cucumber/features/step_definitions/dhcp.rb
index ef4d9e15..3c834224 100644
--- a/cucumber/features/step_definitions/dhcp.rb
+++ b/cucumber/features/step_definitions/dhcp.rb
@@ -1,19 +1,23 @@
Then /^the hostname should not have been leaked on the network$/ do
- hostname = $vm.execute("hostname").stdout.chomp
- packets = PacketFu::PcapFile.new.file_to_array(:filename => @sniffer.pcap_file)
- packets.each do |p|
- # if PacketFu::TCPPacket.can_parse?(p)
- # ipv4_tcp_packets << PacketFu::TCPPacket.parse(p)
- if PacketFu::IPPacket.can_parse?(p)
- payload = PacketFu::IPPacket.parse(p).payload
- elsif PacketFu::IPv6Packet.can_parse?(p)
- payload = PacketFu::IPv6Packet.parse(p).payload
- else
- @sniffer.save_pcap_file
- raise "Found something in the pcap file that either is non-IP, or cannot be parsed"
- end
- if payload.match(hostname)
- raise "Hostname leak detected"
+ begin
+ hostname = $vm.execute("hostname").stdout.chomp
+ packets = PacketFu::PcapFile.new.file_to_array(filename: @sniffer.pcap_file)
+ packets.each do |p|
+ # if PacketFu::TCPPacket.can_parse?(p)
+ # ipv4_tcp_packets << PacketFu::TCPPacket.parse(p)
+ if PacketFu::IPPacket.can_parse?(p)
+ payload = PacketFu::IPPacket.parse(p).payload
+ elsif PacketFu::IPv6Packet.can_parse?(p)
+ payload = PacketFu::IPv6Packet.parse(p).payload
+ else
+ raise "Found something in the pcap file that either is non-IP, or cannot be parsed"
+ end
+ if payload.match(hostname)
+ raise "Hostname leak detected"
+ end
end
+ rescue Exception => e
+ save_failure_artifact("Network capture", @sniffer.pcap_file)
+ raise e
end
end
diff --git a/cucumber/features/step_definitions/electrum.rb b/cucumber/features/step_definitions/electrum.rb
index 447983d4..eaeb22aa 100644
--- a/cucumber/features/step_definitions/electrum.rb
+++ b/cucumber/features/step_definitions/electrum.rb
@@ -1,12 +1,12 @@
Then /^I start Electrum through the GNOME menu$/ do
- step "I start \"Electrum\" via the GNOME \"Internet\" applications menu"
+ step "I start \"Electrum Bitcoin Wallet\" via GNOME Activities Overview"
end
When /^a bitcoin wallet is (|not )present$/ do |existing|
wallet = "/home/#{LIVE_USER}/.electrum/wallets/default_wallet"
case existing
when ""
- step "the file \"#{wallet}\" exists after at most 10 seconds"
+ step "the file \"#{wallet}\" exists after at most 30 seconds"
when "not "
step "the file \"#{wallet}\" does not exist"
else
@@ -17,20 +17,22 @@ end
When /^I create a new bitcoin wallet$/ do
@screen.wait("ElectrumNoWallet.png", 10)
@screen.wait_and_click("ElectrumNextButton.png", 10)
+ @screen.wait("ElectrumCreateNewSeed.png", 10)
+ @screen.wait_and_click("ElectrumNextButton.png", 10)
@screen.wait("ElectrumWalletGenerationSeed.png", 15)
@screen.wait_and_click("ElectrumWalletSeedTextbox.png", 15)
@screen.type('a', Sikuli::KeyModifier.CTRL) # select wallet seed
@screen.type('c', Sikuli::KeyModifier.CTRL) # copy seed to clipboard
seed = $vm.get_clipboard
@screen.wait_and_click("ElectrumNextButton.png", 15)
- @screen.wait("ElectrumWalletSeedTextbox.png", 15)
+ @screen.wait("ElectrumSeedVerificationPrompt.png", 15)
+ @screen.wait_and_click("ElectrumWalletSeedTextbox.png", 15)
@screen.type(seed) # Confirm seed
@screen.wait_and_click("ElectrumNextButton.png", 10)
- @screen.wait_and_click("ElectrumEncryptWallet.png", 10)
+ @screen.wait("ElectrumEncryptWallet.png", 10)
+ @screen.type(Sikuli::Key.TAB) # focus first password field
@screen.type("asdf" + Sikuli::Key.TAB) # set password
@screen.type("asdf" + Sikuli::Key.TAB) # confirm password
- @screen.type(Sikuli::Key.ENTER)
- @screen.wait("ElectrumConnectServer.png", 20)
@screen.wait_and_click("ElectrumNextButton.png", 10)
@screen.wait("ElectrumPreferencesButton.png", 30)
end
@@ -39,8 +41,8 @@ Then /^I see a warning that Electrum is not persistent$/ do
@screen.wait('GnomeQuestionDialogIcon.png', 30)
end
-Then /^I am prompted to create a new wallet$/ do
- @screen.wait('ElectrumNoWallet.png', 60)
+Then /^I am prompted to configure Electrum$/ do
+ @screen.wait("ElectrumNoWallet.png", 60)
end
Then /^I see the main Electrum client window$/ do
diff --git a/cucumber/features/step_definitions/encryption.rb b/cucumber/features/step_definitions/encryption.rb
index 9f7f1b96..3b20a5b4 100644
--- a/cucumber/features/step_definitions/encryption.rb
+++ b/cucumber/features/step_definitions/encryption.rb
@@ -23,16 +23,16 @@ Given /^I generate an OpenPGP key named "([^"]+)" with password "([^"]+)"$/ do |
Passphrase: #{pwd}
%commit
EOF
- gpg_key_recipie.split("\n").each do |line|
- $vm.execute("echo '#{line}' >> /tmp/gpg_key_recipie", :user => LIVE_USER)
- end
- c = $vm.execute("gpg --batch --gen-key < /tmp/gpg_key_recipie",
+ recipe_path = '/tmp/gpg_key_recipe'
+ $vm.file_overwrite(recipe_path, gpg_key_recipie)
+ $vm.execute("chown #{LIVE_USER}:#{LIVE_USER} #{recipe_path}")
+ c = $vm.execute("gpg --batch --gen-key < #{recipe_path}",
:user => LIVE_USER)
assert(c.success?, "Failed to generate OpenPGP key:\n#{c.stderr}")
end
When /^I type a message into gedit$/ do
- step 'I start "Gedit" via the GNOME "Accessories" applications menu'
+ step 'I start "gedit" via GNOME Activities Overview'
@screen.wait_and_click("GeditWindow.png", 20)
# We don't have a good visual indicator for when we can continue. Without the
# sleep we may start typing in the gedit window far too soon, causing
@@ -60,7 +60,7 @@ def gedit_copy_all_text
context_menu_helper('GeditWindow.png', 'GeditStatusBar.png', 'GeditCopy.png')
end
-def paste_into_a_new_tab
+def gedit_paste_into_a_new_tab
@screen.wait_and_click("GeditNewTab.png", 20)
context_menu_helper('GeditWindow.png', 'GeditStatusBar.png', 'GeditPaste.png')
end
@@ -74,7 +74,7 @@ def encrypt_sign_helper
sleep 5
yield
maybe_deal_with_pinentry
- paste_into_a_new_tab
+ gedit_paste_into_a_new_tab
end
def decrypt_verify_helper(icon)
@@ -129,5 +129,5 @@ When /^I symmetrically encrypt the message with password "([^"]+)"$/ do |pwd|
seahorse_menu_click_helper('GpgAppletIconNormal.png', 'GpgAppletEncryptPassphrase.png')
maybe_deal_with_pinentry # enter password
maybe_deal_with_pinentry # confirm password
- paste_into_a_new_tab
+ gedit_paste_into_a_new_tab
end
diff --git a/cucumber/features/step_definitions/firewall_leaks.rb b/cucumber/features/step_definitions/firewall_leaks.rb
index 942d00b8..0cd94cca 100644
--- a/cucumber/features/step_definitions/firewall_leaks.rb
+++ b/cucumber/features/step_definitions/firewall_leaks.rb
@@ -1,29 +1,6 @@
-Then(/^the firewall leak detector has detected (.*?) leaks$/) do |type|
- leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
- :accepted_hosts => get_all_tor_nodes)
- case type.downcase
- when 'ipv4 tcp'
- if leaks.ipv4_tcp_leaks.empty?
- leaks.save_pcap_file
- raise "Couldn't detect any IPv4 TCP leaks"
- end
- when 'ipv4 non-tcp'
- if leaks.ipv4_nontcp_leaks.empty?
- leaks.save_pcap_file
- raise "Couldn't detect any IPv4 non-TCP leaks"
- end
- when 'ipv6'
- if leaks.ipv6_leaks.empty?
- leaks.save_pcap_file
- raise "Couldn't detect any IPv6 leaks"
- end
- when 'non-ip'
- if leaks.nonip_leaks.empty?
- leaks.save_pcap_file
- raise "Couldn't detect any non-IP leaks"
- end
- else
- raise "Incorrect packet type '#{type}'"
+Then(/^the firewall leak detector has detected leaks$/) do
+ assert_raise(FirewallAssertionFailedError) do
+ step 'all Internet traffic has only flowed through Tor'
end
end
@@ -40,12 +17,12 @@ Given(/^I disable Tails' firewall$/) do
end
When(/^I do a TCP DNS lookup of "(.*?)"$/) do |host|
- lookup = $vm.execute("host -T #{host} #{SOME_DNS_SERVER}", :user => LIVE_USER)
+ lookup = $vm.execute("host -T -t A #{host} #{SOME_DNS_SERVER}", :user => LIVE_USER)
assert(lookup.success?, "Failed to resolve #{host}:\n#{lookup.stdout}")
end
When(/^I do a UDP DNS lookup of "(.*?)"$/) do |host|
- lookup = $vm.execute("host #{host} #{SOME_DNS_SERVER}", :user => LIVE_USER)
+ lookup = $vm.execute("host -t A #{host} #{SOME_DNS_SERVER}", :user => LIVE_USER)
assert(lookup.success?, "Failed to resolve #{host}:\n#{lookup.stdout}")
end
diff --git a/cucumber/features/step_definitions/git.rb b/cucumber/features/step_definitions/git.rb
index bf6f869d..bd8fcf7d 100644
--- a/cucumber/features/step_definitions/git.rb
+++ b/cucumber/features/step_definitions/git.rb
@@ -1,3 +1,29 @@
+When /^I clone the Git repository "([\S]+)" in GNOME Terminal$/ do |repo|
+ repo_directory = /[\S]+\/([\S]+)(\.git)?$/.match(repo)[1]
+ assert(!$vm.directory_exist?("/home/#{LIVE_USER}/#{repo_directory}"))
+
+ recovery_proc = Proc.new do
+ $vm.execute("rm -rf /home/#{LIVE_USER}/#{repo_directory}",
+ :user => LIVE_USER)
+ step 'I kill the process "git"'
+ @screen.type('clear' + Sikuli::Key.ENTER)
+ end
+
+ retry_tor(recovery_proc) do
+ step "I run \"git clone #{repo}\" in GNOME Terminal"
+ m = /^(https?|git):\/\//.match(repo)
+ unless m
+ step 'I verify the SSH fingerprint for the Git repository'
+ end
+ try_for(180, :msg => 'Git process took too long') {
+ !$vm.has_process?('/usr/bin/git')
+ }
+ Dogtail::Application.new('gnome-terminal-server')
+ .child('Terminal', roleName: 'terminal')
+ .text['Unpacking objects: 100%']
+ end
+end
+
Then /^the Git repository "([\S]+)" has been cloned successfully$/ do |repo|
assert($vm.directory_exist?("/home/#{LIVE_USER}/#{repo}/.git"))
assert($vm.file_exist?("/home/#{LIVE_USER}/#{repo}/.git/config"))
diff --git a/cucumber/features/step_definitions/icedove.rb b/cucumber/features/step_definitions/icedove.rb
deleted file mode 100644
index d3672895..00000000
--- a/cucumber/features/step_definitions/icedove.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-Then /^Icedove has started$/ do
- step 'process "icedove" is running within 30 seconds'
- @screen.wait('IcedoveMainWindow.png', 60)
-end
-
-When /^I have not configured an email account$/ do
- icedove_prefs = $vm.file_content("/home/#{LIVE_USER}/.icedove/profile.default/prefs.js").chomp
- assert(!icedove_prefs.include?('mail.accountmanager.accounts'))
-end
-
-Then /^I am prompted to setup an email account$/ do
- $vm.focus_window('Mail Account Setup')
- @screen.wait('IcedoveMailAccountSetup.png', 30)
-end
-
-Then /^IMAP is the default protocol$/ do
- $vm.focus_window('Mail Account Setup')
- @screen.wait('IcedoveProtocolIMAP.png', 10)
-end
-
-Then /^I cancel setting up an email account$/ do
- $vm.focus_window('Mail Account Setup')
- @screen.type(Sikuli::Key.ESC)
- @screen.waitVanish('IcedoveMailAccountSetup.png', 10)
-end
-
-Then /^I open Icedove's Add-ons Manager$/ do
- $vm.focus_window('Icedove')
- @screen.wait_and_click('MozillaMenuButton.png', 10)
- @screen.wait_and_click('IcedoveToolsMenuAddOns.png', 10)
- @screen.wait('MozillaAddonsManagerExtensions.png', 30)
-end
-
-Then /^I click the extensions tab$/ do
- @screen.wait_and_click('MozillaAddonsManagerExtensions.png', 10)
-end
-
-Then /^I see that Adblock is not installed in Icedove$/ do
- if @screen.exists('MozillaExtensionsAdblockPlus.png')
- raise 'Adblock should not be enabled within Icedove'
- end
-end
-
-When /^I go into Enigmail's preferences$/ do
- $vm.focus_window('Icedove')
- @screen.type("a", Sikuli::KeyModifier.ALT)
- @screen.wait_and_click('IcedoveEnigmailPreferences.png', 10)
- @screen.wait('IcedoveEnigmailPreferencesWindow.png', 10)
- @screen.click('IcedoveEnigmailExpertSettingsButton.png')
- @screen.wait('IcedoveEnigmailKeyserverTab.png', 10)
-end
-
-When /^I click Enigmail's keyserver tab$/ do
- @screen.wait_and_click('IcedoveEnigmailKeyserverTab.png', 10)
-end
-
-Then /^I see that Enigmail is configured to use the correct keyserver$/ do
- @screen.wait('IcedoveEnigmailKeyserver.png', 10)
-end
-
-Then /^I click Enigmail's advanced tab$/ do
- @screen.wait_and_click('IcedoveEnigmailAdvancedTab.png', 10)
-end
-
-Then /^I see that Enigmail is configured to use the correct SOCKS proxy$/ do
- @screen.click('IcedoveEnigmailAdvancedParameters.png')
- @screen.type(Sikuli::Key.END)
- @screen.wait('IcedoveEnigmailProxy.png', 10)
-end
-
-Then /^I see that Torbirdy is configured to use Tor$/ do
- @screen.wait('IcedoveTorbirdyEnabled.png', 10)
-end
-
-When /^I open Torbirdy's preferences$/ do
- step "I open Icedove's Add-ons Manager"
- step 'I click the extensions tab'
- @screen.wait_and_click('MozillaExtensionsTorbirdy.png', 10)
- @screen.type(Sikuli::Key.TAB) # Select 'More' link
- @screen.type(Sikuli::Key.TAB) # Select 'Preferences' button
- @screen.type(Sikuli::Key.SPACE) # Press 'Preferences' button
- @screen.wait('GnomeQuestionDialogIcon.png', 10)
- @screen.type(Sikuli::Key.ENTER)
-end
-
-When /^I test Torbirdy's proxy settings$/ do
- @screen.wait('IcedoveTorbirdyPreferencesWindow.png', 10)
- @screen.click('IcedoveTorbirdyTestProxySettingsButton.png')
- @screen.wait('IcedoveTorbirdyCongratulationsTab.png', 180)
-end
-
-Then /^Torbirdy's proxy test is successful$/ do
- @screen.wait('IcedoveTorbirdyCongratulationsTab.png', 180)
-end
diff --git a/cucumber/features/step_definitions/mac_spoofing.rb b/cucumber/features/step_definitions/mac_spoofing.rb
index a4aa8714..260b28fd 100644
--- a/cucumber/features/step_definitions/mac_spoofing.rb
+++ b/cucumber/features/step_definitions/mac_spoofing.rb
@@ -5,51 +5,51 @@ def all_ethernet_nics
end
When /^I disable MAC spoofing in Tails Greeter$/ do
+ open_greeter_additional_settings()
@screen.wait_and_click("TailsGreeterMACSpoofing.png", 30)
+ @screen.wait_and_click("TailsGreeterDisableMACSpoofing.png", 10)
+ @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
end
-Then /^the network device has (its default|a spoofed) MAC address configured$/ do |mode|
+Then /^the (\d+)(?:st|nd|rd|th) network device has (its real|a spoofed) MAC address configured$/ do |dev_nr, mode|
is_spoofed = (mode == "a spoofed")
- nic = "eth0"
- assert_equal([nic], all_ethernet_nics,
- "We only expected NIC #{nic} but these are present: " +
- all_ethernet_nics.join(", "))
- nic_real_mac = $vm.real_mac
+ alias_name = "net#{dev_nr.to_i - 1}"
+ nic_real_mac = $vm.real_mac(alias_name)
+ nic = "eth#{dev_nr.to_i - 1}"
nic_current_mac = $vm.execute_successfully(
"get_current_mac_of_nic #{nic}", :libs => 'hardware'
).stdout.chomp
- if is_spoofed
- if nic_real_mac == nic_current_mac
- save_pcap_file
- raise "The MAC address was expected to be spoofed but wasn't"
- end
- else
- if nic_real_mac != nic_current_mac
- save_pcap_file
- raise "The MAC address is spoofed but was expected to not be"
+ begin
+ if is_spoofed
+ if nic_real_mac == nic_current_mac
+ raise "The MAC address was expected to be spoofed but wasn't"
+ end
+ else
+ if nic_real_mac != nic_current_mac
+ raise "The MAC address is spoofed but was expected to not be"
+ end
end
+ rescue Exception => e
+ save_failure_artifact("Network capture", @sniffer.pcap_file)
+ raise e
end
end
-Then /^the real MAC address was (not )?leaked$/ do |mode|
- is_leaking = mode.nil?
- leaks = FirewallLeakCheck.new(@sniffer.pcap_file)
- mac_leaks = leaks.mac_leaks
- if is_leaking
- if !mac_leaks.include?($vm.real_mac)
- save_pcap_file
- raise "The real MAC address was expected to leak but didn't. We " +
- "observed the following MAC addresses: #{mac_leaks}"
- end
- else
- if mac_leaks.include?($vm.real_mac)
- save_pcap_file
- raise "The real MAC address was leaked but was expected not to. We " +
- "observed the following MAC addresses: #{mac_leaks}"
+Then /^no network device leaked the real MAC address$/ do
+ macs = $vm.all_real_macs
+ assert_all_connections(@sniffer.pcap_file) do |c|
+ macs.all? do |mac|
+ not [c.mac_saddr, c.mac_daddr].include?(mac)
end
end
end
+Then /^some network device leaked the real MAC address$/ do
+ assert_raise(FirewallAssertionFailedError) do
+ step 'no network device leaked the real MAC address'
+ end
+end
+
Given /^macchanger will fail by not spoofing and always returns ([\S]+)$/ do |mode|
$vm.execute_successfully("mv /usr/bin/macchanger /usr/bin/macchanger.orig")
$vm.execute_successfully("ln -s /bin/#{mode} /usr/bin/macchanger")
@@ -76,14 +76,6 @@ EOF
$vm.execute_successfully("chmod a+rx /sbin/modprobe")
end
-When /^see the "Network card disabled" notification$/ do
- robust_notification_wait("MACSpoofNetworkCardDisabled.png", 60)
-end
-
-When /^see the "All networking disabled" notification$/ do
- robust_notification_wait("MACSpoofNetworkingDisabled.png", 60)
-end
-
Then /^(\d+|no) network interface(?:s)? (?:is|are) enabled$/ do |expected_nr_nics|
# note that "no".to_i => 0 in Ruby.
expected_nr_nics = expected_nr_nics.to_i
@@ -106,3 +98,22 @@ Then /^the MAC spoofing panic mode disabled networking$/ do
end
end
end
+
+When /^I hotplug a network device( and wait for it to be initialized)?$/ do |wait|
+ initial_nr_nics = wait ? all_ethernet_nics.size : nil
+ xml = <<-EOF
+ <interface type='network'>
+ <alias name='net1'/>
+ <mac address='52:54:00:11:22:33'/>
+ <source network='TailsToasterNet'/>
+ <model type='virtio'/>
+ <link state='up'/>
+ </interface>
+ EOF
+ $vm.plug_device(xml)
+ if wait
+ try_for(20) do
+ all_ethernet_nics.size >= initial_nr_nics + 1
+ end
+ end
+end
diff --git a/cucumber/features/step_definitions/pidgin.rb b/cucumber/features/step_definitions/pidgin.rb
index 3f5ed931..43949b68 100644
--- a/cucumber/features/step_definitions/pidgin.rb
+++ b/cucumber/features/step_definitions/pidgin.rb
@@ -28,26 +28,26 @@ def wait_and_focus(img, time = 10, window)
end
def focus_pidgin_irc_conversation_window(account)
- if account == 'I2P'
- # After connecting to Irc2P messages are sent from services. Most of the
- # time the services will send their messages right away. If there's lag we
- # may in fact join the channel _before_ the message is received. We'll look
- # for a message from InfoServ first then default to looking for '#i2p'
- try_for(20) do
- begin
- $vm.focus_window('InfoServ')
- rescue ExecutionFailedInVM
- $vm.focus_window('#i2p')
- end
- end
- else
- account = account.sub(/^irc\./, '')
- try_for(20) do
- $vm.focus_window(".*#{Regexp.escape(account)}$")
- end
+ account = account.sub(/^irc\./, '')
+ try_for(20) do
+ $vm.focus_window(".*#{Regexp.escape(account)}$")
end
end
+def pidgin_dbus_call(method, *args)
+ dbus_send(
+ 'im.pidgin.purple.PurpleService',
+ '/im/pidgin/purple/PurpleObject',
+ "im.pidgin.purple.PurpleInterface.#{method}",
+ *args, user: LIVE_USER
+ )
+end
+
+def pidgin_account_connected?(account, prpl_protocol)
+ account_id = pidgin_dbus_call('PurpleAccountsFind', account, prpl_protocol)
+ pidgin_dbus_call('PurpleAccountIsConnected', account_id) == 1
+end
+
When /^I create my XMPP account$/ do
account = xmpp_account("Tails_account")
@screen.click("PidginAccountManagerAddButton.png")
@@ -74,6 +74,11 @@ When /^I create my XMPP account$/ do
end
Then /^Pidgin automatically enables my XMPP account$/ do
+ account = xmpp_account("Tails_account")
+ jid = account["username"] + '@' + account["domain"]
+ try_for(3*60) do
+ pidgin_account_connected?(jid, 'prpl-jabber')
+ end
$vm.focus_window('Buddy List')
@screen.wait("PidginAvailableStatus.png", 60*3)
end
@@ -109,8 +114,9 @@ When /^I start a conversation with my friend$/ do
@screen.wait("PidginConversationWindowMenuBar.png", 10)
end
-And /^I say something to my friend( in the multi-user chat)?$/ do |multi_chat|
- msg = "ping" + Sikuli::Key.ENTER
+And /^I say (.*) to my friend( in the multi-user chat)?$/ do |msg, multi_chat|
+ msg = "ping" if msg == "something"
+ msg = msg + Sikuli::Key.ENTER
if multi_chat
$vm.focus_window(@chat_room_jid.split("@").first)
msg = @friend_name + ": " + msg
@@ -126,7 +132,12 @@ Then /^I receive a response from my friend( in the multi-user chat)?$/ do |multi
else
$vm.focus_window(@friend_name)
end
- @screen.wait("PidginFriendExpectedAnswer.png", 20)
+ try_for(60) do
+ if @screen.exists('PidginServerMessage.png')
+ @screen.click('PidginDialogCloseButton.png')
+ end
+ @screen.find('PidginFriendExpectedAnswer.png')
+ end
end
When /^I start an OTR session with my friend$/ do
@@ -203,15 +214,26 @@ end
def configured_pidgin_accounts
accounts = Hash.new
- xml = REXML::Document.new($vm.file_content('$HOME/.purple/accounts.xml',
- LIVE_USER))
+ xml = REXML::Document.new(
+ $vm.file_content("/home/#{LIVE_USER}/.purple/accounts.xml")
+ )
xml.elements.each("account/account") do |e|
account = e.elements["name"].text
account_name, network = account.split("@")
protocol = e.elements["protocol"].text
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
+ username_element = e.elements["settings/setting[@name='username']"]
+ realname_elemenet = e.elements["settings/setting[@name='realname']"]
+ if username_element
+ nickname = username_element.text
+ else
+ nickname = nil
+ end
+ if realname_elemenet
+ real_name = realname_elemenet.text
+ else
+ real_name = nil
+ end
accounts[network] = {
'name' => account_name,
'network' => network,
@@ -227,34 +249,25 @@ end
def chan_image (account, channel, image)
images = {
- 'irc.oftc.net' => {
- '#tails' => {
- 'roster' => 'PidginTailsChannelEntry',
+ 'conference.riseup.net' => {
+ 'tails' => {
'conversation_tab' => 'PidginTailsConversationTab',
'welcome' => 'PidginTailsChannelWelcome',
}
},
- 'I2P' => {
- '#i2p' => {
- 'roster' => 'PidginI2PChannelEntry',
- 'conversation_tab' => 'PidginI2PConversationTab',
- 'welcome' => 'PidginI2PChannelWelcome',
- }
- }
}
return images[account][channel][image] + ".png"
end
def default_chan (account)
chans = {
- 'irc.oftc.net' => '#tails',
- 'I2P' => '#i2p',
+ 'conference.riseup.net' => 'tails',
}
return chans[account]
end
def pidgin_otr_keys
- return $vm.file_content('$HOME/.purple/otr.private_key', LIVE_USER)
+ return $vm.file_content("/home/#{LIVE_USER}/.purple/otr.private_key")
end
Given /^Pidgin has the expected accounts configured with random nicknames$/ do
@@ -278,10 +291,6 @@ Given /^Pidgin has the expected accounts configured with random nicknames$/ do
"#{expected}")
end
-When /^I start Pidgin through the GNOME menu$/ do
- step 'I start "Pidgin" via the GNOME "Internet" applications menu'
-end
-
When /^I open Pidgin's account manager window$/ do
@screen.wait_and_click('PidginMenuAccounts.png', 20)
@screen.wait_and_click('PidginMenuManageAccounts.png', 20)
@@ -293,7 +302,13 @@ When /^I see Pidgin's account manager window$/ do
end
When /^I close Pidgin's account manager window$/ do
- @screen.wait_and_click("PidginAccountManagerCloseButton.png", 10)
+ @screen.wait_and_click("PidginDialogCloseButton.png", 10)
+end
+
+When /^I close Pidgin$/ do
+ $vm.focus_window('Buddy List')
+ @screen.type("q", Sikuli::KeyModifier.CTRL)
+ @screen.waitVanish('PidginAvailableStatus.png', 10)
end
When /^I (de)?activate the "([^"]+)" Pidgin account$/ do |deactivate, account|
@@ -331,8 +346,7 @@ Then /^Pidgin successfully connects to the "([^"]+)" account$/ do |account|
deactivate_and_activate_pidgin_account(account)
end
end
- retrier_method = account == 'I2P' ? method(:retry_i2p) : method(:retry_tor)
- retrier_method.call(recovery_on_failure) do
+ retry_tor(recovery_on_failure) do
begin
$vm.focus_window('Buddy List')
rescue ExecutionFailedInVM
@@ -363,10 +377,22 @@ Then /^the "([^"]*)" account only responds to PING and VERSION CTCP requests$/ d
ctcp_check.verify_ctcp_responses
end
-Then /^I can join the "([^"]+)" channel on "([^"]+)"$/ do |channel, account|
- @screen.doubleClick( chan_image(account, channel, 'roster'))
+Then /^I can join the( pre-configured)? "([^"]+)" channel on "([^"]+)"$/ do |preconfigured, channel, account|
+ if preconfigured
+ @screen.doubleClick(chan_image(account, channel, 'roster'))
+ focus_pidgin_irc_conversation_window(account)
+ else
+ $vm.focus_window('Buddy List')
+ @screen.wait_and_click("PidginBuddiesMenu.png", 20)
+ @screen.wait_and_click("PidginBuddiesMenuJoinChat.png", 10)
+ @screen.wait_and_click("PidginJoinChatWindow.png", 10)
+ @screen.click_mid_right_edge("PidginJoinChatRoomLabel.png")
+ @screen.type(channel)
+ @screen.click("PidginJoinChatButton.png")
+ @chat_room_jid = channel + "@" + account
+ $vm.focus_window(@chat_room_jid)
+ end
@screen.hide_cursor
- focus_pidgin_irc_conversation_window(account)
try_for(60) do
begin
@screen.wait_and_click(chan_image(account, channel, 'conversation_tab'), 5)
@@ -405,7 +431,7 @@ end
def pidgin_add_certificate_from (cert_file)
# Here, we need a certificate that is not already in the NSS database
- step "I copy \"/usr/share/ca-certificates/spi-inc.org/spi-cacert-2008.crt\" to \"#{cert_file}\" as user \"amnesia\""
+ step "I copy \"/usr/share/ca-certificates/mozilla/CNNIC_ROOT.crt\" to \"#{cert_file}\" as user \"amnesia\""
$vm.focus_window('Buddy List')
@screen.wait_and_click('PidginToolsMenu.png', 10)
@@ -453,6 +479,9 @@ end
When /^I see the Tails roadmap URL$/ do
try_for(60) do
+ if @screen.exists('PidginServerMessage.png')
+ @screen.click('PidginDialogCloseButton.png')
+ end
begin
@screen.find('PidginTailsRoadmapUrl.png')
rescue FindFailed => e
@@ -464,4 +493,5 @@ end
When /^I click on the Tails roadmap URL$/ do
@screen.click('PidginTailsRoadmapUrl.png')
+ try_for(60) { @torbrowser = Dogtail::Application.new('Firefox') }
end
diff --git a/cucumber/features/step_definitions/root_access_control.rb b/cucumber/features/step_definitions/root_access_control.rb
index ff1bdfcc..8362342d 100644
--- a/cucumber/features/step_definitions/root_access_control.rb
+++ b/cucumber/features/step_definitions/root_access_control.rb
@@ -34,8 +34,7 @@ end
Then /^I should not be able to run a command as root with pkexec and the standard passwords$/ do
step "I run \"pkexec touch /root/pkexec-test\" in GNOME Terminal"
['', 'live', 'amnesia'].each do |password|
- step "I enter the \"#{password}\" password in the pkexec prompt"
- @screen.wait('PolicyKitAuthFailure.png', 20)
+ deal_with_polkit_prompt(password, expect_success: false)
end
@screen.type(Sikuli::Key.ESC)
@screen.wait('PolicyKitAuthCompleteFailure.png', 20)
diff --git a/cucumber/features/step_definitions/snapshots.rb b/cucumber/features/step_definitions/snapshots.rb
index 74c60d20..16e59a4b 100644
--- a/cucumber/features/step_definitions/snapshots.rb
+++ b/cucumber/features/step_definitions/snapshots.rb
@@ -6,7 +6,7 @@ def checkpoints
:parent_checkpoint => nil,
:steps => [
'I create a 8 GiB disk named "'+JOB_NAME+'"',
- 'I plug ide drive "'+JOB_NAME+'"',
+ 'I plug sata drive "'+JOB_NAME+'"',
]
}
@@ -16,7 +16,7 @@ def checkpoints
:parent_checkpoint => nil,
:steps => [
'I create a 64 GiB disk named "'+JOB_NAME+'"',
- 'I plug ide drive "'+JOB_NAME+'"',
+ 'I plug sata drive "'+JOB_NAME+'"',
]
}
@@ -54,7 +54,7 @@ def checkpoints
'I allow reboot after the install is complete',
'I wait for the reboot',
'I power off the computer',
- 'the computer is set to boot from ide drive',
+ 'the computer is set to boot from sata drive',
]
}
end
@@ -85,12 +85,12 @@ def reach_checkpoint(name)
post_snapshot_restore_hook
end
debug_log(scenario_indent + "Checkpoint: #{checkpoint_description}",
- :color => :white)
+ color: :white, timestamp: false)
step_action = "Given"
if parent_checkpoint
parent_description = checkpoints[parent_checkpoint][:description]
debug_log(step_indent + "#{step_action} #{parent_description}",
- :color => :green)
+ color: :green, timestamp: false)
step_action = "And"
end
steps.each do |s|
@@ -99,10 +99,11 @@ def reach_checkpoint(name)
rescue Exception => e
debug_log(scenario_indent +
"Step failed while creating checkpoint: #{s}",
- :color => :red)
+ color: :red, timestamp: false)
raise e
end
- debug_log(step_indent + "#{step_action} #{s}", :color => :green)
+ debug_log(step_indent + "#{step_action} #{s}",
+ color: :green, timestamp: false)
step_action = "And"
end
$vm.save_snapshot(name)
diff --git a/cucumber/features/step_definitions/ssh.rb b/cucumber/features/step_definitions/ssh.rb
index 038b2977..1fd0efaf 100644
--- a/cucumber/features/step_definitions/ssh.rb
+++ b/cucumber/features/step_definitions/ssh.rb
@@ -60,6 +60,7 @@ end
Given /^I (?:am prompted to )?verify the SSH fingerprint for the (?:Git|SSH) (?:repository|server)$/ do
@screen.wait("SSHFingerprint.png", 60)
+ sleep 1 # brief pause to ensure that the following keystrokes do not get lost
@screen.type('yes' + Sikuli::Key.ENTER)
end
@@ -75,6 +76,7 @@ Given /^an SSH server is running on the LAN$/ do
@sshd_server_host = $vmnet.bridge_ip_addr
sshd = SSHServer.new(@sshd_server_host, @sshd_server_port)
sshd.start
+ add_lan_host(@sshd_server_host, @sshd_server_port)
add_after_scenario_hook { sshd.stop }
end
@@ -94,8 +96,17 @@ When /^I connect to an SSH server on the (Internet|LAN)$/ do |location|
cmd = "ssh #{@ssh_username}@#{@ssh_host} #{ssh_port_suffix}"
step 'process "ssh" is not running'
- step "I run \"#{cmd}\" in GNOME Terminal"
- step 'process "ssh" is running within 10 seconds'
+
+ recovery_proc = Proc.new do
+ step 'I kill the process "ssh"' if $vm.has_process?("ssh")
+ step 'I run "clear" in GNOME Terminal'
+ end
+
+ retry_tor(recovery_proc) do
+ step "I run \"#{cmd}\" in GNOME Terminal"
+ step 'process "ssh" is running within 10 seconds'
+ step 'I verify the SSH fingerprint for the SSH server'
+ end
end
Then /^I have sucessfully logged into the SSH server$/ do
@@ -104,19 +115,42 @@ end
Then /^I connect to an SFTP server on the Internet$/ do
read_and_validate_ssh_config "SFTP"
+
@sftp_port ||= 22
@sftp_port = @sftp_port.to_s
- step 'I start "Files" via the GNOME "Accessories" applications menu'
- @screen.wait_and_click("GnomeFilesConnectToServer.png", 10)
- @screen.wait("GnomeConnectToServerWindow.png", 10)
- @screen.type("sftp://" + @sftp_username + "@" + @sftp_host + ":" + @sftp_port)
- @screen.wait_and_click("GnomeConnectToServerConnectButton.png", 10)
+
+ recovery_proc = Proc.new do
+ step 'I kill the process "ssh"'
+ step 'I kill the process "nautilus"'
+ end
+
+ retry_tor(recovery_proc) do
+ step 'I start "Nautilus" via GNOME Activities Overview'
+ nautilus = Dogtail::Application.new('nautilus')
+ nautilus.child(roleName: 'frame')
+ nautilus.child('Other Locations', roleName: 'label').click
+ connect_bar = nautilus.child('Connect to Server', roleName: 'label').parent
+ connect_bar
+ .child(roleName: 'filler', recursive: false)
+ .child(roleName: 'text', recursive: false)
+ .text = "sftp://" + @sftp_username + "@" + @sftp_host + ":" + @sftp_port
+ connect_bar.button('Connect', recursive: false).click
+ step "I verify the SSH fingerprint for the SFTP server"
+ end
end
Then /^I verify the SSH fingerprint for the SFTP server$/ do
- @screen.wait_and_click("GnomeSSHVerificationConfirm.png", 60)
+ try_for(30) do
+ Dogtail::Application.new('gnome-shell').child?('Log In Anyway')
+ end
+ # Here we'd like to click on the button using Dogtail, but something
+ # is buggy so let's just use the keyboard.
+ @screen.type(Sikuli::Key.ENTER)
end
Then /^I successfully connect to the SFTP server$/ do
- @screen.wait("GnomeSSHSuccess.png", 60)
+ try_for(60) do
+ Dogtail::Application.new('nautilus')
+ .child?("#{@sftp_username} on #{@sftp_host}")
+ end
end
diff --git a/cucumber/features/step_definitions/time_syncing.rb b/cucumber/features/step_definitions/time_syncing.rb
index 319fb521..d1b81073 100644
--- a/cucumber/features/step_definitions/time_syncing.rb
+++ b/cucumber/features/step_definitions/time_syncing.rb
@@ -47,23 +47,23 @@ Then /^Tails clock is less than (\d+) minutes incorrect$/ do |max_diff_mins|
puts "Time was #{diff} seconds off"
end
-Then /^the system clock is just past Tails' build date$/ do
+Then /^the system clock is just past Tails' source date$/ do
system_time_str = $vm.execute_successfully('date').to_s
system_time = DateTime.parse(system_time_str).to_time
- build_time_cmd = 'sed -n -e "1s/^.* - \([0-9]\+\)$/\1/p;q" ' +
- '/etc/amnesia/version'
- build_time_str = $vm.execute_successfully(build_time_cmd).to_s
- build_time = DateTime.parse(build_time_str).to_time
- diff = system_time - build_time # => in seconds
+ source_time_cmd = 'sed -n -e "1s/^.* - \([0-9]\+\)$/\1/p;q" ' +
+ '/etc/amnesia/version'
+ source_time_str = $vm.execute_successfully(source_time_cmd).to_s
+ source_time = DateTime.parse(source_time_str).to_time
+ diff = system_time - source_time # => in seconds
# Half an hour should be enough to boot Tails on any reasonable
# hardware and VM setup.
max_diff = 30*60
assert(diff > 0,
"The system time (#{system_time}) is before the Tails " +
- "build date (#{build_time})")
+ "source date (#{source_time})")
assert(diff <= max_diff,
"The system time (#{system_time}) is more than #{max_diff} seconds " +
- "past the build date (#{build_time})")
+ "past the source date (#{source_time})")
end
Then /^Tails' hardware clock is close to the host system's time$/ do
diff --git a/cucumber/features/step_definitions/tor.rb b/cucumber/features/step_definitions/tor.rb
index ac12fd4c..04852f76 100644
--- a/cucumber/features/step_definitions/tor.rb
+++ b/cucumber/features/step_definitions/tor.rb
@@ -90,7 +90,7 @@ Then /^the firewall is configured to only allow the (.+) users? to connect direc
"The following rule has an unexpected destination:\n" +
rule.to_s)
state_cond = try_xml_element_text(rule, "conditions/state/state")
- next if state_cond == "RELATED,ESTABLISHED"
+ next if state_cond == "ESTABLISHED"
assert_not_nil(rule.elements['conditions/owner/uid-owner'])
rule.elements.each('conditions/owner/uid-owner') do |owner|
uid = owner.text.to_i
@@ -184,7 +184,7 @@ def firewall_has_dropped_packet_to?(proto, host, port)
$vm.execute("journalctl --dmesg --output=cat | grep -qP '#{regex}'").success?
end
-When /^I open an untorified (TCP|UDP|ICMP) connections to (\S*)(?: on port (\d+))? that is expected to fail$/ do |proto, host, port|
+When /^I open an untorified (TCP|UDP|ICMP) connection to (\S*)(?: on port (\d+))?$/ do |proto, host, port|
assert(!firewall_has_dropped_packet_to?(proto, host, port),
"A #{proto} packet to #{host}" +
(port.nil? ? "" : ":#{port}") +
@@ -195,11 +195,11 @@ When /^I open an untorified (TCP|UDP|ICMP) connections to (\S*)(?: on port (\d+)
case proto
when "TCP"
assert_not_nil(port)
- cmd = "echo | netcat #{host} #{port}"
+ cmd = "echo | nc.traditional #{host} #{port}"
user = LIVE_USER
when "UDP"
assert_not_nil(port)
- cmd = "echo | netcat -u #{host} #{port}"
+ cmd = "echo | nc.traditional -u #{host} #{port}"
user = LIVE_USER
when "ICMP"
cmd = "ping -c 5 #{host}"
@@ -243,34 +243,38 @@ def stream_isolation_info(application)
case application
when "htpdate"
{
- :grep_monitor_expr => '/curl\>',
+ :grep_monitor_expr => 'users:(("curl"',
:socksport => 9062
}
- when "tails-security-check", "tails-upgrade-frontend-wrapper"
- # We only grep connections with ESTABLISHED state since `perl`
- # is also used by monkeysphere's validation agent, which LISTENs
+ when "tails-security-check"
{
- :grep_monitor_expr => '\<ESTABLISHED\>.\+/perl\>',
+ :grep_monitor_expr => 'users:(("tails-security-"',
+ :socksport => 9062
+ }
+ when "tails-upgrade-frontend-wrapper"
+ {
+ :grep_monitor_expr => 'users:(("tails-iuk-get-u"',
:socksport => 9062
}
when "Tor Browser"
{
- :grep_monitor_expr => '/firefox\>',
- :socksport => 9150
+ :grep_monitor_expr => 'users:(("firefox"',
+ :socksport => 9150,
+ :controller => true,
}
when "Gobby"
{
- :grep_monitor_expr => '/gobby\>',
+ :grep_monitor_expr => 'users:(("gobby-0.5"',
:socksport => 9050
}
when "SSH"
{
- :grep_monitor_expr => '/\(connect-proxy\|ssh\)\>',
+ :grep_monitor_expr => 'users:(("\(nc\|ssh\)"',
:socksport => 9050
}
when "whois"
{
- :grep_monitor_expr => '/whois\>',
+ :grep_monitor_expr => 'users:(("whois"',
:socksport => 9050
}
else
@@ -279,26 +283,28 @@ def stream_isolation_info(application)
end
When /^I monitor the network connections of (.*)$/ do |application|
- @process_monitor_log = "/tmp/netstat.log"
+ @process_monitor_log = "/tmp/ss.log"
info = stream_isolation_info(application)
$vm.spawn("while true; do " +
- " netstat -taupen | grep \"#{info[:grep_monitor_expr]}\"; " +
+ " ss -taupen | grep '#{info[:grep_monitor_expr]}'; " +
" sleep 0.1; " +
"done > #{@process_monitor_log}")
end
Then /^I see that (.+) is properly stream isolated$/ do |application|
- expected_port = stream_isolation_info(application)[:socksport]
+ info = stream_isolation_info(application)
+ expected_ports = [info[:socksport]]
+ expected_ports << 9051 if info[:controller]
assert_not_nil(@process_monitor_log)
log_lines = $vm.file_content(@process_monitor_log).split("\n")
assert(log_lines.size > 0,
"Couldn't see any connection made by #{application} so " \
"something is wrong")
log_lines.each do |line|
- addr_port = line.split(/\s+/)[4]
- assert_equal("127.0.0.1:#{expected_port}", addr_port,
- "#{application} should use SocksPort #{expected_port} but " \
- "was seen connecting to #{addr_port}")
+ ip_port = line.split(/\s+/)[5]
+ assert(expected_ports.map { |port| "127.0.0.1:#{port}" }.include?(ip_port),
+ "#{application} should only connect to #{expected_ports} but " \
+ "was seen connecting to #{ip_port}")
end
end
@@ -308,7 +314,7 @@ end
And /^I re-run htpdate$/ do
$vm.execute_successfully("service htpdate stop && " \
- "rm -f /var/run/htpdate/* && " \
+ "rm -f /run/htpdate/* && " \
"systemctl --no-block start htpdate.service")
step "the time has synced"
end
@@ -318,18 +324,22 @@ And /^I re-run tails-upgrade-frontend-wrapper$/ do
end
When /^I connect Gobby to "([^"]+)"$/ do |host|
- @screen.wait("GobbyWindow.png", 30)
- @screen.wait("GobbyWelcomePrompt.png", 10)
- @screen.click("GnomeCloseButton.png")
- @screen.wait("GobbyWindow.png", 10)
+ gobby = Dogtail::Application.new('gobby-0.5')
+ gobby.child('Welcome to Gobby', roleName: 'label')
+ gobby.button('Close').click
# This indicates that Gobby has finished initializing itself
# (generating DH parameters, etc.) -- before, the UI is not responsive
# and our CTRL-t is lost.
- @screen.wait("GobbyFailedToShareDocuments.png", 30)
+ gobby.child('Failed to share documents', roleName: 'label')
+ gobby.menu('File').click
+ gobby.menuItem('Connect to Server...').click
@screen.type("t", Sikuli::KeyModifier.CTRL)
- @screen.wait("GobbyConnectPrompt.png", 10)
- @screen.type(host + Sikuli::Key.ENTER)
- @screen.wait("GobbyConnectionComplete.png", 60)
+ connect_dialog = gobby.dialog('Connect to Server')
+ connect_dialog.child('', roleName: 'text').typeText(host)
+ connect_dialog.button('Connect').click
+ # This looks for the live user's presence entry in the chat, which
+ # will only be shown if the connection succeeded.
+ try_for(60) { gobby.child(LIVE_USER, roleName: 'table cell'); true }
end
When /^the Tor Launcher autostarts$/ do
@@ -337,35 +347,47 @@ When /^the Tor Launcher autostarts$/ do
end
When /^I configure some (\w+) pluggable transports in Tor Launcher$/ do |bridge_type|
- bridge_type.downcase!
- bridge_type.capitalize!
- begin
- @bridges = $config["Tor"]["Transports"][bridge_type]
- assert_not_nil(@bridges)
- assert(!@bridges.empty?)
- rescue NoMethodError, Test::Unit::AssertionFailedError
- raise(
-<<EOF
-It seems no '#{bridge_type}' pluggable transports are defined in your local configuration file (#{LOCAL_CONFIG_FILE}). See wiki/src/contribute/release_process/test/usage.mdwn for the format.
-EOF
-)
- end
- @bridge_hosts = []
- for bridge in @bridges do
- @bridge_hosts << bridge["ipv4_address"]
- end
-
@screen.wait_and_click('TorLauncherConfigureButton.png', 10)
@screen.wait('TorLauncherBridgePrompt.png', 10)
@screen.wait_and_click('TorLauncherYesRadioOption.png', 10)
@screen.wait_and_click('TorLauncherNextButton.png', 10)
@screen.wait_and_click('TorLauncherBridgeList.png', 10)
- for bridge in @bridges do
- bridge_line = bridge_type.downcase + " " +
- bridge["ipv4_address"] + ":" +
- bridge["ipv4_port"].to_s
- bridge_line += " " + bridge["fingerprint"].to_s if bridge["fingerprint"]
- bridge_line += " " + bridge["extra"].to_s if bridge["extra"]
+ @bridge_hosts = []
+ chutney_src_dir = "#{GIT_DIR}/submodules/chutney"
+ bridge_dirs = Dir.glob(
+ "#{$config['TMPDIR']}/chutney-data/nodes/*#{bridge_type}/"
+ )
+ bridge_dirs.each do |bridge_dir|
+ address = $vmnet.bridge_ip_addr
+ port = nil
+ fingerprint = nil
+ extra = nil
+ if bridge_type == 'bridge'
+ open(bridge_dir + "/torrc") do |f|
+ port = f.grep(/^OrPort\b/).first.split.last
+ end
+ else
+ # This is the pluggable transport case. While we could set a
+ # static port via ServerTransportListenAddr we instead let it be
+ # picked randomly so an already used port is not picked --
+ # Chutney already has issues with that for OrPort selection.
+ pt_re = /Registered server transport '#{bridge_type}' at '[^']*:(\d+)'/
+ open(bridge_dir + "/notice.log") do |f|
+ pt_lines = f.grep(pt_re)
+ port = pt_lines.last.match(pt_re)[1]
+ end
+ if bridge_type == 'obfs4'
+ open(bridge_dir + "/pt_state/obfs4_bridgeline.txt") do |f|
+ extra = f.readlines.last.chomp.sub(/^.* cert=/, 'cert=')
+ end
+ end
+ end
+ open(bridge_dir + "/fingerprint") do |f|
+ fingerprint = f.read.chomp.split.last
+ end
+ @bridge_hosts << { address: address, port: port.to_i }
+ bridge_line = bridge_type + " " + address + ":" + port
+ [fingerprint, extra].each { |e| bridge_line += " " + e.to_s if e }
@screen.type(bridge_line + Sikuli::Key.ENTER)
end
@screen.wait_and_click('TorLauncherNextButton.png', 10)
@@ -378,25 +400,7 @@ end
When /^all Internet traffic has only flowed through the configured pluggable transports$/ do
assert_not_nil(@bridge_hosts, "No bridges has been configured via the " +
"'I configure some ... bridges in Tor Launcher' step")
- leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
- :accepted_hosts => @bridge_hosts)
- leaks.assert_no_leaks
-end
-
-Then /^the Tor binary is configured to use the expected Tor authorities$/ do
- tor_auths = Set.new
- tor_binary_orport_strings = $vm.execute_successfully(
- "strings /usr/bin/tor | grep -E 'orport=[0-9]+'").stdout.chomp.split("\n")
- tor_binary_orport_strings.each do |potential_auth_string|
- auth_regex = /^\S+ orport=\d+( bridge)?( no-v2)?( v3ident=[A-Z0-9]{40})? ([0-9\.]+):\d+( [A-Z0-9]{4}){10}$/
- m = auth_regex.match(potential_auth_string)
- if m
- auth_ipv4_addr = m[4]
- tor_auths << auth_ipv4_addr
- end
+ assert_all_connections(@sniffer.pcap_file) do |c|
+ @bridge_hosts.include?({ address: c.daddr, port: c.dport })
end
- expected_tor_auths = Set.new(TOR_AUTHORITIES)
- assert_equal(expected_tor_auths, tor_auths,
- "The Tor binary does not have the expected Tor authorities " +
- "configured")
end
diff --git a/cucumber/features/step_definitions/torified_browsing.rb b/cucumber/features/step_definitions/torified_browsing.rb
index c8f3ff1d..76760789 100644
--- a/cucumber/features/step_definitions/torified_browsing.rb
+++ b/cucumber/features/step_definitions/torified_browsing.rb
@@ -1,5 +1,5 @@
-When /^no traffic has flowed to the LAN$/ do
- leaks = FirewallLeakCheck.new(@sniffer.pcap_file, :ignore_lan => false)
- assert(not(leaks.ipv4_tcp_leaks.include?(@lan_host)),
- "Traffic was sent to LAN host #{@lan_host}")
+Then /^no traffic was sent to the web server on the LAN$/ do
+ assert_no_connections(@sniffer.pcap_file) do |c|
+ c.daddr == @web_server_ip_addr and c.dport == @web_server_port
+ end
end
diff --git a/cucumber/features/step_definitions/torified_gnupg.rb b/cucumber/features/step_definitions/torified_gnupg.rb
index 4b4cc040..f5f61cef 100644
--- a/cucumber/features/step_definitions/torified_gnupg.rb
+++ b/cucumber/features/step_definitions/torified_gnupg.rb
@@ -1,3 +1,5 @@
+require 'resolv'
+
class OpenPGPKeyserverCommunicationError < StandardError
end
@@ -20,7 +22,7 @@ def start_or_restart_seahorse
if @withgpgapplet
seahorse_menu_click_helper('GpgAppletIconNormal.png', 'GpgAppletManageKeys.png')
else
- step 'I start "Seahorse" via the GNOME "Utilities" applications menu'
+ step 'I start "Passwords and Keys" via GNOME Activities Overview'
end
step 'Seahorse has opened'
end
@@ -43,6 +45,18 @@ When /^the "([^"]+)" OpenPGP key is not in the live user's public keyring$/ do |
"The '#{keyid}' key is in the live user's public keyring.")
end
+def setup_onion_keyserver
+ resolver = Resolv::DNS.new
+ keyservers = resolver.getaddresses('pool.sks-keyservers.net').select do |addr|
+ addr.class == Resolv::IPv4
+ end
+ onion_keyserver_address = keyservers.sample
+ hkp_port = 11371
+ @onion_keyserver_job = chutney_onionservice_redir(
+ onion_keyserver_address, hkp_port
+ )
+end
+
When /^I fetch the "([^"]+)" OpenPGP key using the GnuPG CLI( without any signatures)?$/ do |keyid, without|
# Make keyid an instance variable so we can reference it in the Seahorse
# keysyncing step.
@@ -52,7 +66,7 @@ When /^I fetch the "([^"]+)" OpenPGP key using the GnuPG CLI( without any signat
else
importopts = ''
end
- retry_tor do
+ retry_tor(Proc.new { setup_onion_keyserver }) do
@gnupg_recv_key_res = $vm.execute_successfully(
"timeout 120 gpg --batch #{importopts} --recv-key '#{@fetched_openpgp_keyid}'",
:user => LIVE_USER)
@@ -74,11 +88,6 @@ When /^the Seahorse operation is successful$/ do
$vm.has_process?('seahorse')
end
-When /^GnuPG uses the configured keyserver$/ do
- assert(@gnupg_recv_key_res.stderr[CONFIGURED_KEYSERVER_HOSTNAME],
- "GnuPG's stderr did not mention keyserver #{CONFIGURED_KEYSERVER_HOSTNAME}")
-end
-
When /^the "([^"]+)" key is in the live user's public keyring(?: after at most (\d) seconds)?$/ do |keyid, delay|
delay = 10 unless delay
try_for(delay.to_i, :msg => "The '#{keyid}' key is not in the live user's public keyring") {
@@ -87,7 +96,7 @@ When /^the "([^"]+)" key is in the live user's public keyring(?: after at most (
}
end
-When /^I start Seahorse( via the Tails OpenPGP Applet)?$/ do |withgpgapplet|
+When /^I start Seahorse( via the OpenPGP Applet)?$/ do |withgpgapplet|
@withgpgapplet = !!withgpgapplet
start_or_restart_seahorse
end
@@ -108,7 +117,8 @@ end
Then /^I synchronize keys in Seahorse$/ do
recovery_proc = Proc.new do
- # The versions of Seahorse in Wheezy and Jessie will abort with a
+ setup_onion_keyserver
+ # The version of Seahorse in Jessie will abort with a
# segmentation fault whenever there's any sort of network error while
# syncing keys. This will usually happens after clicking away the error
# message. This does not appear to be a problem in Stretch.
@@ -151,7 +161,7 @@ Then /^I synchronize keys in Seahorse$/ do
end
end
-When /^I fetch the "([^"]+)" OpenPGP key using Seahorse( via the Tails OpenPGP Applet)?$/ do |keyid, withgpgapplet|
+When /^I fetch the "([^"]+)" OpenPGP key using Seahorse( via the OpenPGP Applet)?$/ do |keyid, withgpgapplet|
step "I start Seahorse#{withgpgapplet}"
def change_of_status?(keyid)
@@ -166,6 +176,7 @@ When /^I fetch the "([^"]+)" OpenPGP key using Seahorse( via the Tails OpenPGP A
end
recovery_proc = Proc.new do
+ setup_onion_keyserver
@screen.click('GnomeCloseButton.png') if @screen.exists('GnomeCloseButton.png')
@screen.type("w", Sikuli::KeyModifier.CTRL)
end
@@ -198,11 +209,55 @@ When /^I fetch the "([^"]+)" OpenPGP key using Seahorse( via the Tails OpenPGP A
end
end
-Then /^Seahorse is configured to use the correct keyserver$/ do
- @gnome_keyservers = YAML.load($vm.execute_successfully('gsettings get org.gnome.crypto.pgp keyservers',
- :user => LIVE_USER).stdout)
- assert_equal(1, @gnome_keyservers.count, 'Seahorse should only have one keyserver configured.')
- # Seahorse doesn't support hkps so that part of the domain is stripped out.
- # We also insert hkp:// to the beginning of the domain.
- assert_equal(CONFIGURED_KEYSERVER_HOSTNAME.sub('hkps.', 'hkp://'), @gnome_keyservers[0])
+Given /^(GnuPG|Seahorse) is configured to use Chutney's onion keyserver$/ do |app|
+ setup_onion_keyserver unless @onion_keyserver_job
+ _, _, onion_address, onion_port = chutney_onionservice_info
+ case app
+ when 'GnuPG'
+ # Validate the shipped configuration ...
+ server = /keyserver\s+(\S+)$/.match($vm.file_content("/home/#{LIVE_USER}/.gnupg/dirmngr.conf"))[1]
+ assert_equal(
+ "hkp://#{CONFIGURED_KEYSERVER_HOSTNAME}", server,
+ "GnuPG's dirmngr does not use the correct keyserver"
+ )
+ # ... before replacing it
+ $vm.execute_successfully(
+ "sed -i 's/#{CONFIGURED_KEYSERVER_HOSTNAME}/#{onion_address}:#{onion_port}/' " +
+ "'/home/#{LIVE_USER}/.gnupg/dirmngr.conf'"
+ )
+ when 'Seahorse'
+ # Validate the shipped configuration ...
+ @gnome_keyservers = YAML.load(
+ $vm.execute_successfully(
+ 'gsettings get org.gnome.crypto.pgp keyservers',
+ user: LIVE_USER
+ ).stdout
+ )
+ assert_equal(1, @gnome_keyservers.count,
+ 'Seahorse should only have one keyserver configured.')
+ assert_equal(
+ 'hkp://' + CONFIGURED_KEYSERVER_HOSTNAME, @gnome_keyservers[0],
+ "GnuPG's dirmngr does not use the correct keyserver"
+ )
+ # ... before replacing it
+ $vm.execute_successfully(
+ "gsettings set org.gnome.crypto.pgp keyservers \"['hkp://#{onion_address}:#{onion_port}']\"",
+ user: LIVE_USER
+ )
+ end
+end
+
+Then /^GnuPG's dirmngr uses the configured keyserver$/ do
+ _, _, onion_keyserver_address, _ = chutney_onionservice_info
+ dirmngr_request = $vm.execute_successfully(
+ 'gpg-connect-agent --dirmngr "keyserver --hosttable" /bye', user: LIVE_USER
+ )
+ server = dirmngr_request.stdout.chomp.lines[1].split[4]
+ server = /keyserver\s+(\S+)$/.match(
+ $vm.file_content("/home/#{LIVE_USER}/.gnupg/dirmngr.conf")
+ )[1]
+ assert_equal(
+ "hkp://#{onion_keyserver_address}:5858", server,
+ "GnuPG's dirmngr does not use the correct keyserver"
+ )
end
diff --git a/cucumber/features/step_definitions/torified_misc.rb b/cucumber/features/step_definitions/torified_misc.rb
index 7112776a..7ccdb227 100644
--- a/cucumber/features/step_definitions/torified_misc.rb
+++ b/cucumber/features/step_definitions/torified_misc.rb
@@ -1,3 +1,5 @@
+require 'resolv'
+
When /^I query the whois directory service for "([^"]+)"$/ do |domain|
retry_tor do
@vm_execute_res = $vm.execute("whois '#{domain}'", :user => LIVE_USER)
@@ -9,10 +11,18 @@ When /^I query the whois directory service for "([^"]+)"$/ do |domain|
end
end
-When /^I wget "([^"]+)" to stdout(?:| with the '([^']+)' options)$/ do |url, options|
- arguments = "-O - '#{url}'"
- arguments = "#{options} #{arguments}" if options
+When /^I wget "([^"]+)" to stdout(?:| with the '([^']+)' options)$/ do |target, options|
retry_tor do
+ if target == "some Tails mirror"
+ host = 'dl.amnesia.boum.org'
+ address = Resolv.new.getaddresses(host).sample
+ puts "Resolved #{host} to #{address}"
+ url = "http://#{address}/tails/stable/"
+ else
+ url = target
+ end
+ arguments = "-O - '#{url}'"
+ arguments = "#{options} #{arguments}" if options
@vm_execute_res = $vm.execute("wget #{arguments}", :user => LIVE_USER)
if @vm_execute_res.failure?
raise "wget:ing #{url} with options #{options} failed with:\n" +
diff --git a/cucumber/features/step_definitions/totem.rb b/cucumber/features/step_definitions/totem.rb
index 72698dde..a5b88d14 100644
--- a/cucumber/features/step_definitions/totem.rb
+++ b/cucumber/features/step_definitions/totem.rb
@@ -1,23 +1,24 @@
Given /^I create sample videos$/ do
- @shared_video_dir_on_host = "#{$config["TMPDIR"]}/shared_video_dir"
- @shared_video_dir_on_guest = "/tmp/shared_video_dir"
- FileUtils.mkdir_p(@shared_video_dir_on_host)
- add_after_scenario_hook { FileUtils.rm_r(@shared_video_dir_on_host) }
+ @video_dir_on_host = "#{$config["TMPDIR"]}/video_dir"
+ FileUtils.mkdir_p(@video_dir_on_host)
+ add_after_scenario_hook { FileUtils.rm_r(@video_dir_on_host) }
fatal_system("avconv -loop 1 -t 30 -f image2 " +
- "-i 'features/images/TailsBootSplash.png' " +
+ "-i 'features/images/USBTailsLogo.png' " +
"-an -vcodec libx264 -y " +
'-filter:v "crop=in_w-mod(in_w\,2):in_h-mod(in_h\,2)" ' +
- "'#{@shared_video_dir_on_host}/video.mp4' >/dev/null 2>&1")
+ "'#{@video_dir_on_host}/video.mp4' >/dev/null 2>&1")
end
-Given /^I setup a filesystem share containing sample videos$/ do
- $vm.add_share(@shared_video_dir_on_host, @shared_video_dir_on_guest)
+Given /^I plug and mount a USB drive containing sample videos$/ do
+ @video_dir_on_guest = share_host_files(
+ Dir.glob("#{@video_dir_on_host}/*")
+ )
end
Given /^I copy the sample videos to "([^"]+)" as user "([^"]+)"$/ do |destination, user|
- for video_on_host in Dir.glob("#{@shared_video_dir_on_host}/*.mp4") do
+ for video_on_host in Dir.glob("#{@video_dir_on_host}/*.mp4") do
video_name = File.basename(video_on_host)
- src_on_guest = "#{@shared_video_dir_on_guest}/#{video_name}"
+ src_on_guest = "#{@video_dir_on_guest}/#{video_name}"
dst_on_guest = "#{destination}/#{video_name}"
step "I copy \"#{src_on_guest}\" to \"#{dst_on_guest}\" as user \"amnesia\""
end
@@ -32,7 +33,7 @@ When /^I close Totem$/ do
end
Then /^I can watch a WebM video over HTTPs$/ do
- test_url = 'https://webm.html5.org/test.webm'
+ test_url = 'https://tails.boum.org/lib/test_suite/test.webm'
recovery_on_failure = Proc.new do
step 'I close Totem'
end
diff --git a/cucumber/features/step_definitions/unsafe_browser.rb b/cucumber/features/step_definitions/unsafe_browser.rb
index b8c04983..160279ca 100644
--- a/cucumber/features/step_definitions/unsafe_browser.rb
+++ b/cucumber/features/step_definitions/unsafe_browser.rb
@@ -1,6 +1,11 @@
-When /^I see and accept the Unsafe Browser start verification$/ do
+When /^I see and accept the Unsafe Browser start verification(?:| in the "([^"]+)" locale)$/ do |locale|
@screen.wait('GnomeQuestionDialogIcon.png', 30)
- @screen.type(Sikuli::Key.ESC)
+ if ['ar_EG.utf8', 'fa_IR'].include?(locale)
+ # Take into account button ordering in RTL languages
+ @screen.type(Sikuli::Key.LEFT + Sikuli::Key.ENTER)
+ else
+ @screen.type(Sikuli::Key.RIGHT + Sikuli::Key.ENTER)
+ end
end
def supported_torbrowser_languages
@@ -8,7 +13,8 @@ def supported_torbrowser_languages
File.read(localization_descriptions).split("\n").map do |line|
# The line will be of the form "xx:YY:..." or "xx-YY:YY:..."
first, second = line.sub('-', '_').split(':')
- candidates = ["#{first}_#{second}.utf8", "#{first}.utf8",
+ candidates = ["#{first}_#{second}.UTF-8", "#{first}_#{second}.utf8",
+ "#{first}.UTF-8", "#{first}.utf8",
"#{first}_#{second}", first]
when_not_found = Proc.new { raise "Could not find a locale for '#{line}'" }
candidates.find(when_not_found) do |candidate|
@@ -19,12 +25,12 @@ end
Then /^I start the Unsafe Browser in the "([^"]+)" locale$/ do |loc|
step "I run \"LANG=#{loc} LC_ALL=#{loc} sudo unsafe-browser\" in GNOME Terminal"
- step "I see and accept the Unsafe Browser start verification"
+ step "I see and accept the Unsafe Browser start verification in the \"#{loc}\" locale"
end
Then /^the Unsafe Browser works in all supported languages$/ do
failed = Array.new
- supported_torbrowser_languages.each do |lang|
+ supported_torbrowser_languages.sample(3).each do |lang|
step "I start the Unsafe Browser in the \"#{lang}\" locale"
begin
step "the Unsafe Browser has started"
@@ -85,7 +91,7 @@ Then /^the Unsafe Browser has only Firefox's default bookmarks configured$/ do
assert_equal(5, mozilla_uris_counter,
"Unexpected number (#{mozilla_uris_counter}) of mozilla " \
"bookmarks")
- assert_equal(3, places_uris_counter,
+ assert_equal(2, places_uris_counter,
"Unexpected number (#{places_uris_counter}) of places " \
"bookmarks")
@screen.type(Sikuli::Key.F4, Sikuli::KeyModifier.ALT)
@@ -108,7 +114,7 @@ Then /^I can start the Unsafe Browser again$/ do
end
Then /^I cannot configure the Unsafe Browser to use any local proxies$/ do
- socks_proxy = 'c' # Alt+c for socks proxy
+ socks_proxy = 'C' # Alt+Shift+c for socks proxy
no_proxy = 'y' # Alt+y for no proxy
proxies = [[no_proxy, nil, nil]]
socksport_lines =
@@ -120,7 +126,7 @@ Then /^I cannot configure the Unsafe Browser to use any local proxies$/ do
proxies.each do |proxy_type, proxy_host, proxy_port|
@screen.hide_cursor
- # Open proxy settings and select manual proxy configuration
+ # Open proxy settings
@screen.click('UnsafeBrowserMenuButton.png')
@screen.wait_and_click('UnsafeBrowserPreferencesButton.png', 10)
@screen.wait_and_click('UnsafeBrowserAdvancedSettingsButton.png', 10)
@@ -129,20 +135,25 @@ Then /^I cannot configure the Unsafe Browser to use any local proxies$/ do
@screen.click(hit) if hit == 'UnsafeBrowserNetworkTab.png'
@screen.wait_and_click('UnsafeBrowserNetworkTabSettingsButton.png', 10)
@screen.wait_and_click('UnsafeBrowserProxySettingsWindow.png', 10)
- @screen.type("m", Sikuli::KeyModifier.ALT)
- # Configure the proxy
- @screen.type(proxy_type, Sikuli::KeyModifier.ALT) # Select correct proxy type
- @screen.type(proxy_host + Sikuli::Key.TAB + proxy_port) if proxy_type != no_proxy
+ # Ensure the desired proxy configuration
+ if proxy_type == no_proxy
+ @screen.type(proxy_type, Sikuli::KeyModifier.ALT)
+ @screen.wait('UnsafeBrowserNoProxySelected.png', 10)
+ else
+ @screen.type("M", Sikuli::KeyModifier.ALT)
+ @screen.type(proxy_type, Sikuli::KeyModifier.ALT)
+ @screen.type(proxy_host + Sikuli::Key.TAB + proxy_port)
+ end
# Close settings
@screen.click('UnsafeBrowserProxySettingsOkButton.png')
@screen.waitVanish('UnsafeBrowserProxySettingsWindow.png', 10)
# Test that the proxy settings work as they should
- step "I open the address \"https://check.torproject.org\" in the Unsafe Browser"
+ step 'I open Tails homepage in the Unsafe Browser'
if proxy_type == no_proxy
- @screen.wait('UnsafeBrowserTorCheckFail.png', 60)
+ step 'Tails homepage loads in the Unsafe Browser'
else
@screen.wait('UnsafeBrowserProxyRefused.png', 60)
end
@@ -162,7 +173,11 @@ Then /^the Unsafe Browser has no proxy configured$/ do
end
Then /^the Unsafe Browser complains that no DNS server is configured$/ do
- @screen.wait("UnsafeBrowserDNSError.png", 30)
+ assert_not_nil(
+ Dogtail::Application.new('zenity')
+ .child(roleName: 'label')
+ .text['No DNS server was obtained']
+ )
end
Then /^I configure the Unsafe Browser to check for updates more frequently$/ do
diff --git a/cucumber/features/step_definitions/untrusted_partitions.rb b/cucumber/features/step_definitions/untrusted_partitions.rb
index 43453b2f..603c8b4f 100644
--- a/cucumber/features/step_definitions/untrusted_partitions.rb
+++ b/cucumber/features/step_definitions/untrusted_partitions.rb
@@ -27,7 +27,7 @@ Given /^I create an? ([[:alnum:]]+) partition( labeled "([^"]+)")? with an? ([[:
$vm.storage.disk_mkpartfs(name, parttype, fstype, opts)
end
-Given /^I cat an ISO of the Tails image to disk "([^"]+)"$/ do |name|
+Given /^I write the Tails ISO image to disk "([^"]+)"$/ do |name|
src_disk = {
:path => TAILS_ISO,
:opts => {
@@ -55,7 +55,7 @@ end
Then /^Tails Greeter has( not)? detected a persistence partition$/ do |no_persistence|
expecting_persistence = no_persistence.nil?
@screen.find('TailsGreeter.png')
- found_persistence = ! @screen.exists('TailsGreeterPersistence.png').nil?
+ found_persistence = ! @screen.exists('TailsGreeterPersistencePassphrase.png').nil?
assert_equal(expecting_persistence, found_persistence,
"Persistence is unexpectedly#{no_persistence} enabled")
end
diff --git a/cucumber/features/step_definitions/usb.rb b/cucumber/features/step_definitions/usb.rb
index 76f94d2f..e030f68e 100644
--- a/cucumber/features/step_definitions/usb.rb
+++ b/cucumber/features/step_definitions/usb.rb
@@ -48,6 +48,14 @@ def persistent_volumes_mountpoints
$vm.execute("ls -1 -d /live/persistence/*_unlocked/").stdout.chomp.split
end
+def recover_from_upgrader_failure
+ $vm.execute('killall tails-upgrade-frontend tails-upgrade-frontend-wrapper zenity')
+ # Remove unnecessary sleep for retry
+ $vm.execute_successfully('sed -i "/^sleep 30$/d" ' +
+ '/usr/local/bin/tails-upgrade-frontend-wrapper')
+ $vm.spawn('tails-upgrade-frontend-wrapper', user: LIVE_USER)
+end
+
Given /^I clone USB drive "([^"]+)" to a new USB drive "([^"]+)"$/ do |from, to|
$vm.storage.clone_to_new_disk(from, to)
end
@@ -65,66 +73,105 @@ Given /^the computer is set to boot in UEFI mode$/ do
@os_loader = 'UEFI'
end
+def tails_installer_selected_device
+ @installer.child('Target Device:', roleName: 'label').parent
+ .child('', roleName: 'combo box', recursive: false).name
+end
+
+def tails_installer_is_device_selected?(name)
+ device = $vm.disk_dev(name)
+ tails_installer_selected_device[/#{device}\d*$/]
+end
+
+def tails_installer_match_status(pattern)
+ @installer.child('', roleName: 'text').text[pattern]
+end
+
class UpgradeNotSupported < StandardError
end
def usb_install_helper(name)
- @screen.wait('USBTailsLogo.png', 10)
- if @screen.exists("USBCannotUpgrade.png")
+ if tails_installer_match_status(/It is impossible to upgrade the device .+ #{$vm.disk_dev(name)}\d* /)
raise UpgradeNotSupported
end
- @screen.wait_and_click('USBCreateLiveUSB.png', 10)
- @screen.wait('USBCreateLiveUSBConfirmWindow.png', 10)
- @screen.wait_and_click('USBCreateLiveUSBConfirmYes.png', 10)
- @screen.wait('USBInstallationComplete.png', 30*60)
+ assert(tails_installer_is_device_selected?(name))
+ begin
+ @installer.button('Install Tails').click
+ @installer.child('Question', roleName: 'alert').button('Yes').click
+ try_for(30*60) do
+ @installer
+ .child('Information', roleName: 'alert')
+ .child('Installation complete!', roleName: 'label')
+ true
+ end
+ rescue FindFailed => e
+ path = $vm.execute_successfully('ls -1 /tmp/tails-installer-*').stdout.chomp
+ debug_log("Tails Installer debug log:\n" + $vm.file_content(path))
+ raise e
+ end
end
-When /^I start Tails Installer$/ do
- step 'I start "TailsInstaller" via the GNOME "Tails" applications menu'
- @screen.wait('USBCloneAndInstall.png', 30)
+When /^I start Tails Installer in "([^"]+)" mode$/ do |mode|
+ step 'I run "export DEBUG=1 ; tails-installer-launcher" in GNOME Terminal'
+ installer_launcher = Dogtail::Application.new('tails-installer-launcher')
+ .child('Tails Installer', roleName: 'frame')
+ # Sometimes Dogtail will find the button and click it before it is
+ # shown (searchShowingOnly is not perfect) which generally means
+ # clicking somewhere on the Terminal => the click is lost *and* the
+ # installer does no go to the foreground. So let's wait a bit extra.
+ sleep 3
+ installer_launcher.button(mode).click
+ @installer = Dogtail::Application.new('tails-installer')
+ @installer.child('Tails Installer', roleName: 'frame')
+ # ... and something similar (for consecutive steps) again.
+ sleep 3
+ $vm.focus_window('Tails Installer')
end
-When /^I start Tails Installer in "([^"]+)" mode$/ do |mode|
- step 'I start Tails Installer'
- case mode
- when 'Clone & Install'
- @screen.wait_and_click('USBCloneAndInstall.png', 10)
- when 'Clone & Upgrade'
- @screen.wait_and_click('USBCloneAndUpgrade.png', 10)
- when 'Upgrade from ISO'
- @screen.wait_and_click('USBUpgradeFromISO.png', 10)
- else
- raise "Unsupported mode '#{mode}'"
+Then /^Tails Installer detects that a device is too small$/ do
+ try_for(10) do
+ tails_installer_match_status(/^The device .* is too small to install Tails/)
end
end
-Then /^Tails Installer detects that a device is too small$/ do
- @screen.wait('TailsInstallerTooSmallDevice.png', 10)
+When /^I am told that the destination device cannot be upgraded$/ do
+ try_for(10) do
+ tails_installer_match_status(/^It is impossible to upgrade the device/)
+ end
end
-When /^I "Clone & Install" Tails to USB drive "([^"]+)"$/ do |name|
- step 'I start Tails Installer in "Clone & Install" mode'
- usb_install_helper(name)
+When /^I am suggested to do a "Install by cloning"$/ do
+ try_for(10) do
+ tails_installer_match_status(
+ /You should instead use "Install by cloning" to upgrade Tails/
+ )
+ end
end
-When /^I "Clone & Upgrade" Tails to USB drive "([^"]+)"$/ do |name|
- step 'I start Tails Installer in "Clone & Upgrade" mode'
- usb_install_helper(name)
+Then /^a suitable USB device is (?:still )?not found$/ do
+ @installer.child(
+ 'No device suitable to install Tails could be found', roleName: 'label'
+ )
end
-When /^I try a "Clone & Upgrade" Tails to USB drive "([^"]+)"$/ do |name|
- begin
- step "I \"Clone & Upgrade\" Tails to USB drive \"#{name}\""
- rescue UpgradeNotSupported
- # this is what we expect
- else
- raise "The USB installer should not succeed"
+Then /^(no|the "([^"]+)") USB drive is selected$/ do |mode, name|
+ try_for(30) do
+ if mode == 'no'
+ tails_installer_selected_device == ''
+ else
+ tails_installer_is_device_selected?(name)
+ end
end
end
-When /^I try to "Upgrade from ISO" USB drive "([^"]+)"$/ do |name|
+When /^I "([^"]*)" Tails to USB drive "([^"]+)"$/ do |mode, name|
+ step "I start Tails Installer in \"#{mode}\" mode"
+ usb_install_helper(name)
+end
+
+When /^I fail to "([^"]*)" Tails to USB drive "([^"]+)"$/ do |mode, name|
begin
- step "I do a \"Upgrade from ISO\" on USB drive \"#{name}\""
+ step "I \"#{mode}\" Tails to USB drive \"#{name}\""
rescue UpgradeNotSupported
# this is what we expect
else
@@ -132,35 +179,20 @@ When /^I try to "Upgrade from ISO" USB drive "([^"]+)"$/ do |name|
end
end
-When /^I am suggested to do a "Clone & Install"$/ do
- @screen.find("USBCannotUpgrade.png")
-end
-
-When /^I am told that the destination device cannot be upgraded$/ do
- @screen.find("USBCannotUpgrade.png")
-end
-
-Given /^I setup a filesystem share containing the Tails ISO$/ do
- shared_iso_dir_on_host = "#{$config["TMPDIR"]}/shared_iso_dir"
- @shared_iso_dir_on_guest = "/tmp/shared_iso_dir"
- FileUtils.mkdir_p(shared_iso_dir_on_host)
- FileUtils.cp(TAILS_ISO, shared_iso_dir_on_host)
- add_after_scenario_hook { FileUtils.rm_r(shared_iso_dir_on_host) }
- $vm.add_share(shared_iso_dir_on_host, @shared_iso_dir_on_guest)
+Given /^I plug and mount a USB drive containing the Tails ISO$/ do
+ iso_dir = share_host_files(TAILS_ISO)
+ @iso_path = "#{iso_dir}/#{File.basename(TAILS_ISO)}"
end
When /^I do a "Upgrade from ISO" on USB drive "([^"]+)"$/ do |name|
step 'I start Tails Installer in "Upgrade from ISO" mode'
- @screen.wait('USBUseLiveSystemISO.png', 10)
- match = @screen.find('USBUseLiveSystemISO.png')
- @screen.click(match.getCenter.offset(0, match.h*2))
- @screen.wait('USBSelectISO.png', 10)
- @screen.wait_and_click('GnomeFileDiagHome.png', 10)
+ @installer.child('Use existing Live system ISO:', roleName: 'label')
+ .parent.button('(None)').click
+ file_chooser = @installer.child('Select a File', roleName: 'file chooser')
@screen.type("l", Sikuli::KeyModifier.CTRL)
- @screen.wait('GnomeFileDiagTypeFilename.png', 10)
- iso = "#{@shared_iso_dir_on_guest}/#{File.basename(TAILS_ISO)}"
- @screen.type(iso)
- @screen.wait_and_click('GnomeFileDiagOpenButton.png', 10)
+ # The only visible text element will be the path entry
+ file_chooser.child(roleName: 'text').typeText(@iso_path + '\n')
+ file_chooser.button('Open').click
usb_install_helper(name)
end
@@ -174,13 +206,22 @@ Given /^I enable all persistence presets$/ do
@screen.type(Sikuli::Key.TAB + Sikuli::Key.SPACE)
end
@screen.wait_and_click('PersistenceWizardSave.png', 10)
+ @screen.wait('PersistenceWizardDone.png', 60)
+ @screen.type(Sikuli::Key.F4, Sikuli::KeyModifier.ALT)
+end
+
+When /^I disable the first persistence preset$/ do
+ step 'I start "Configure persistent volume" via GNOME Activities Overview'
+ @screen.wait('PersistenceWizardPresets.png', 300)
+ @screen.type(Sikuli::Key.SPACE)
+ @screen.wait_and_click('PersistenceWizardSave.png', 10)
@screen.wait('PersistenceWizardDone.png', 30)
@screen.type(Sikuli::Key.F4, Sikuli::KeyModifier.ALT)
end
Given /^I create a persistent partition$/ do
- step 'I start "ConfigurePersistentVolume" via the GNOME "Tails" applications menu'
- @screen.wait('PersistenceWizardStart.png', 20)
+ step 'I start "Configure persistent volume" via GNOME Activities Overview'
+ @screen.wait('PersistenceWizardStart.png', 60)
@screen.type(@persistence_password + "\t" + @persistence_password + Sikuli::Key.ENTER)
@screen.wait('PersistenceWizardPresets.png', 300)
step "I enable all persistence presets"
@@ -254,10 +295,9 @@ Then /^the running Tails is installed on USB drive "([^"]+)"$/ do |target_name|
end
Then /^the ISO's Tails is installed on USB drive "([^"]+)"$/ do |target_name|
- iso = "#{@shared_iso_dir_on_guest}/#{File.basename(TAILS_ISO)}"
iso_root = "/mnt/iso"
$vm.execute("mkdir -p #{iso_root}")
- $vm.execute("mount -o loop #{iso} #{iso_root}")
+ $vm.execute("mount -o loop #{@iso_path} #{iso_root}")
tails_is_installed_helper(target_name, iso_root, "isolinux")
$vm.execute("umount #{iso_root}")
end
@@ -274,10 +314,10 @@ Then /^a Tails persistence partition exists on USB drive "([^"]+)"$/ do |name|
# The LUKS container may already be opened, e.g. by udisks after
# we've run tails-persistence-setup.
- c = $vm.execute("ls -1 /dev/mapper/")
+ c = $vm.execute("ls -1 --hide 'control' /dev/mapper/")
if c.success?
for candidate in c.stdout.split("\n")
- luks_info = $vm.execute("cryptsetup status #{candidate}")
+ luks_info = $vm.execute("cryptsetup status '#{candidate}'")
if luks_info.success? and luks_info.stdout.match("^\s+device:\s+#{dev}$")
luks_dev = "/dev/mapper/#{candidate}"
break
@@ -300,7 +340,7 @@ Then /^a Tails persistence partition exists on USB drive "([^"]+)"$/ do |name|
mount_dir = "/mnt/#{name}"
$vm.execute("mkdir -p #{mount_dir}")
- c = $vm.execute("mount #{luks_dev} #{mount_dir}")
+ c = $vm.execute("mount '#{luks_dev}' #{mount_dir}")
assert(c.success?,
"Couldn't mount opened LUKS device '#{dev}' on drive '#{name}'")
@@ -310,12 +350,9 @@ Then /^a Tails persistence partition exists on USB drive "([^"]+)"$/ do |name|
end
Given /^I enable persistence$/ do
- @screen.wait('TailsGreeterPersistence.png', 10)
- @screen.type(Sikuli::Key.SPACE)
- @screen.wait('TailsGreeterPersistencePassphrase.png', 10)
- match = @screen.find('TailsGreeterPersistencePassphrase.png')
- @screen.click(match.getCenter.offset(match.w*2, match.h/2))
- @screen.type(@persistence_password)
+ @screen.wait_and_click('TailsGreeterPersistencePassphrase.png', 10)
+ @screen.type(@persistence_password + Sikuli::Key.ENTER)
+ @screen.wait('TailsGreeterPersistenceUnlocked.png', 30)
end
def tails_persistence_enabled?
@@ -325,13 +362,21 @@ def tails_persistence_enabled?
'test "$TAILS_PERSISTENCE_ENABLED" = true').success?
end
-Given /^all persistence presets(| from the old Tails version) are enabled$/ do |old_tails|
+Given /^all persistence presets(| from the old Tails version)(| but the first one) are enabled$/ do |old_tails, except_first|
+ assert(old_tails.empty? || except_first.empty?, "Unsupported case.")
try_for(120, :msg => "Persistence is disabled") do
tails_persistence_enabled?
end
+ unexpected_mounts = Array.new
# Check that all persistent directories are mounted
if old_tails.empty?
expected_mounts = persistent_mounts
+ if ! except_first.empty?
+ first_expected_mount_source = expected_mounts.keys[0]
+ first_expected_mount_destination = expected_mounts[first_expected_mount_source]
+ expected_mounts.delete(first_expected_mount_source)
+ unexpected_mounts = [first_expected_mount_destination]
+ end
else
assert_not_nil($remembered_persistence_mounts)
expected_mounts = $remembered_persistence_mounts
@@ -341,17 +386,16 @@ Given /^all persistence presets(| from the old Tails version) are enabled$/ do |
assert(mount.include?("on #{dir} "),
"Persistent directory '#{dir}' is not mounted")
end
+ for dir in unexpected_mounts do
+ assert(! mount.include?("on #{dir} "),
+ "Persistent directory '#{dir}' is mounted")
+ end
end
Given /^persistence is disabled$/ do
assert(!tails_persistence_enabled?, "Persistence is enabled")
end
-Given /^I enable read-only persistence$/ do
- step "I enable persistence"
- @screen.wait_and_click('TailsGreeterPersistenceReadOnly.png', 10)
-end
-
def boot_device
# Approach borrowed from
# config/chroot_local_includes/lib/live/config/998-permissions
@@ -374,23 +418,21 @@ end
Then /^Tails is running from (.*) drive "([^"]+)"$/ do |bus, name|
bus = bus.downcase
case bus
- when "ide"
+ when "sata"
expected_bus = "ata"
else
expected_bus = bus
end
assert_equal(expected_bus, boot_device_type)
actual_dev = boot_device
- # The boot partition differs between a "normal" install using the
- # USB installer and isohybrid installations
- expected_dev_normal = $vm.disk_dev(name) + "1"
- expected_dev_isohybrid = $vm.disk_dev(name) + "4"
- assert(actual_dev == expected_dev_normal ||
- actual_dev == expected_dev_isohybrid,
+ # The boot partition differs between an using Tails installer and
+ # isohybrids. There's also a strange case isohybrids are thought to
+ # be booting from the "raw" device, and not a partition of it
+ # (#10504).
+ expected_devs = ['', '1', '4'].map { |e| $vm.disk_dev(name) + e }
+ assert(expected_devs.include?(actual_dev),
"We are running from device #{actual_dev}, but for #{bus} drive " +
- "'#{name}' we expected to run from either device " +
- "#{expected_dev_normal} (when installed via the USB installer) " +
- "or #{expected_dev_isohybrid} (when installed from an isohybrid)")
+ "'#{name}' we expected to run from one of #{expected_devs}")
end
Then /^the boot device has safe access rights$/ do
@@ -493,6 +535,12 @@ When /^I write some files expected to persist$/ do
end
end
+When /^I write some dotfile expected to persist$/ do
+ assert($vm.execute("touch /live/persistence/TailsData_unlocked/dotfiles/.XXX_persist",
+ :user => LIVE_USER).success?,
+ "Could not create a file in the dotfiles persistence.")
+end
+
When /^I remove some files expected to persist$/ do
persistent_mounts.each do |_, dir|
owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
@@ -529,6 +577,14 @@ Then /^the expected persistent files(| created with the old Tails version) are p
end
end
+Then /^the expected persistent dotfile is present in the filesystem$/ do
+ expected_dirs = persistent_dirs
+ assert($vm.execute("test -L #{expected_dirs['dotfiles']}/.XXX_persist").success?,
+ "Could not find expected persistent dotfile link.")
+ assert($vm.execute("test -e $(readlink -f #{expected_dirs['dotfiles']}/.XXX_persist)").success?,
+ "Could not find expected persistent dotfile link target.")
+end
+
Then /^only the expected files are present on the persistence partition on USB drive "([^"]+)"$/ do |name|
assert(!$vm.is_running?)
disk = {
@@ -568,8 +624,8 @@ Then /^only the expected files are present on the persistence partition on USB d
end
When /^I delete the persistent partition$/ do
- step 'I start "DeletePersistentVolume" via the GNOME "Tails" applications menu'
- @screen.wait("PersistenceWizardDeletionStart.png", 20)
+ step 'I start "Delete persistent volume" via GNOME Activities Overview'
+ @screen.wait("PersistenceWizardDeletionStart.png", 120)
@screen.type(" ")
@screen.wait("PersistenceWizardDone.png", 120)
end
@@ -583,14 +639,109 @@ Given /^I create a ([[:alpha:]]+) label on disk "([^"]+)"$/ do |type, name|
$vm.storage.disk_mklabel(name, type)
end
-Then /^a suitable USB device is (?:still )?not found$/ do
- @screen.wait("TailsInstallerNoQEMUHardDisk.png", 30)
+Given /^the file system changes introduced in version (.+) are (not )?present(?: in the (\S+) Browser's chroot)?$/ do |version, not_present, chroot_browser|
+ assert_equal('1.1~test', version)
+ upgrade_applied = not_present.nil?
+ chroot_browser = "#{chroot_browser.downcase}-browser" if chroot_browser
+ changes = [
+ {
+ filesystem: :rootfs,
+ path: 'some_new_file',
+ status: :added,
+ new_content: <<-EOF
+Some content
+ EOF
+ },
+ {
+ filesystem: :rootfs,
+ path: 'etc/amnesia/version',
+ status: :modified,
+ new_content: <<-EOF
+#{version} - 20380119
+ffffffffffffffffffffffffffffffffffffffff
+live-build: 3.0.5+really+is+2.0.12-0.tails2
+live-boot: 4.0.2-1
+live-config: 4.0.4-1
+ EOF
+ },
+ {
+ filesystem: :rootfs,
+ path: 'etc/os-release',
+ status: :modified,
+ new_content: <<-EOF
+TAILS_PRODUCT_NAME="Tails"
+TAILS_VERSION_ID="#{version}"
+ EOF
+ },
+ {
+ filesystem: :rootfs,
+ path: 'usr/share/common-licenses/BSD',
+ status: :removed
+ },
+ {
+ filesystem: :medium,
+ path: 'utils/linux/syslinux',
+ status: :removed
+ },
+ ]
+ changes.each do |change|
+ case change[:filesystem]
+ when :rootfs
+ path = '/'
+ path += "var/lib/#{chroot_browser}/chroot/" if chroot_browser
+ path += change[:path]
+ when :medium
+ path = '/lib/live/mount/medium/' + change[:path]
+ else
+ raise "Unknown filesysten '#{change[:filesystem]}'"
+ end
+ case change[:status]
+ when :removed
+ assert_equal(!upgrade_applied, $vm.file_exist?(path))
+ when :added
+ assert_equal(upgrade_applied, $vm.file_exist?(path))
+ if upgrade_applied && change[:new_content]
+ assert_equal(change[:new_content], $vm.file_content(path))
+ end
+ when :modified
+ assert($vm.file_exist?(path))
+ if upgrade_applied
+ assert_not_nil(change[:new_content])
+ assert_equal(change[:new_content], $vm.file_content(path))
+ end
+ else
+ raise "Unknown status '#{change[:status]}'"
+ end
+ end
+end
+
+Then /^I am proposed to install an incremental upgrade to version (.+)$/ do |version|
+ recovery_proc = Proc.new do
+ recover_from_upgrader_failure
+ end
+ failure_pic = 'TailsUpgraderFailure.png'
+ success_pic = "TailsUpgraderUpgradeTo#{version}.png"
+ retry_tor(recovery_proc) do
+ match, _ = @screen.waitAny([success_pic, failure_pic], 2*60)
+ assert_equal(success_pic, match)
+ end
end
-Then /^the "(?:[^"]+)" USB drive is selected$/ do
- @screen.wait("TailsInstallerQEMUHardDisk.png", 30)
+When /^I agree to install the incremental upgrade$/ do
+ @screen.click('TailsUpgraderUpgradeNowButton.png')
end
-Then /^no USB drive is selected$/ do
- @screen.wait("TailsInstallerNoQEMUHardDisk.png", 30)
+Then /^I can successfully install the incremental upgrade to version (.+)$/ do |version|
+ step 'I agree to install the incremental upgrade'
+ recovery_proc = Proc.new do
+ recover_from_upgrader_failure
+ step "I am proposed to install an incremental upgrade to version #{version}"
+ step 'I agree to install the incremental upgrade'
+ end
+ failure_pic = 'TailsUpgraderFailure.png'
+ success_pic = "TailsUpgraderDone.png"
+ retry_tor(recovery_proc) do
+ match, _ = @screen.waitAny([success_pic, failure_pic], 2*60)
+ assert_equal(success_pic, match)
+ end
end
diff --git a/cucumber/features/support/config.rb b/cucumber/features/support/config.rb
index 13578d55..54a0f1cd 100644
--- a/cucumber/features/support/config.rb
+++ b/cucumber/features/support/config.rb
@@ -74,25 +74,11 @@ LIBVIRT_REMOTE_SHELL_PORT = 13370 + Integer($executor_number)
MISC_FILES_DIR = "/srv/jenkins/cucumber/features/misc_files"
SERVICES_EXPECTED_ON_ALL_IFACES =
[
- ["cupsd", "0.0.0.0", "631"],
- ["dhclient", "0.0.0.0", "*"]
+ ["cupsd", "*", "631"],
+ ["dhclient", "*", "*"]
]
# OpenDNS
SOME_DNS_SERVER = "208.67.222.222"
-TOR_AUTHORITIES =
- # List grabbed from Tor's sources, src/or/config.c:~750.
- [
- "86.59.21.38",
- "128.31.0.39",
- "194.109.206.212",
- "82.94.251.203",
- "199.254.238.52",
- "131.188.40.189",
- "193.23.244.244",
- "208.83.223.34",
- "171.25.193.9",
- "154.35.175.225",
- ]
VM_XML_PATH = "/srv/jenkins/cucumber/features/domains"
#TAILS_SIGNING_KEY = cmd_helper(". #{Dir.pwd}/config/amnesia; echo ${AMNESIA_DEV_KEYID}").tr(' ', '').chomp
diff --git a/cucumber/features/support/env.rb b/cucumber/features/support/env.rb
index 53f502e1..c52affff 100644
--- a/cucumber/features/support/env.rb
+++ b/cucumber/features/support/env.rb
@@ -23,6 +23,10 @@ def create_git
Dir.mkdir 'config'
FileUtils.touch('config/base_branch')
Dir.mkdir('config/APT_overlays.d')
+ Dir.mkdir('config/APT_snapshots.d')
+ ['debian', 'debian-security', 'torproject'].map do |origin|
+ Dir.mkdir("config/APT_snapshots.d/#{origin}")
+ end
Dir.mkdir 'debian'
File.open('debian/changelog', 'w') do |changelog|
changelog.write(<<END_OF_CHANGELOG)
@@ -88,3 +92,35 @@ RSpec::Matchers.define :have_suite do |suite|
"expected an output with #{suite}"
end
end
+
+RSpec::Matchers.define :have_tagged_snapshot do |tag|
+ match do |string|
+ # e.g.: `http://tagged.snapshots.deb.tails.boum.org/0.10`
+ %r{^http://tagged\.snapshots\.deb\.tails\.boum\.org/#{Regexp.escape(tag)}/[a-z-]+$}.match(string)
+ end
+ failure_message_for_should do |string|
+ "expected the mirror to be #{tag}\nCurrent mirror: #{string}"
+ end
+ failure_message_for_should_not do |string|
+ "expected the mirror not to be #{tag}\nCurrent mirror: #{string}"
+ end
+ description do
+ "expected an output with #{tag}"
+ end
+end
+
+RSpec::Matchers.define :have_time_based_snapshot do |tag|
+ match do |string|
+ # e.g.: `http://time-based.snapshots.deb.tails.boum.org/debian/2016060602`
+ %r{^http://time\-based\.snapshots\.deb\.tails\.boum\.org/[^/]+/\d+}.match(string)
+ end
+ failure_message_for_should do |string|
+ "expected the mirror to be a time-based snapshot\nCurrent mirror: #{string}"
+ end
+ failure_message_for_should_not do |string|
+ "expected the mirror not to be a time-based snapshot\nCurrent mirror: #{string}"
+ end
+ description do
+ "expected a time-based snapshot"
+ end
+end
diff --git a/cucumber/features/support/extra_hooks.rb b/cucumber/features/support/extra_hooks.rb
index 16196a55..c2c57494 100644
--- a/cucumber/features/support/extra_hooks.rb
+++ b/cucumber/features/support/extra_hooks.rb
@@ -1,18 +1,21 @@
# Make the code below work with cucumber >= 2.0. Once we stop
# supporting <2.0 we should probably do this differently, but this way
# we can easily support both at the same time.
+
begin
if not(Cucumber::Core::Ast::Feature.instance_methods.include?(:accept_hook?))
- require 'gherkin/tag_expression'
+ if Gem::Version.new(Cucumber::VERSION) >= Gem::Version.new('2.4.0')
+ require 'cucumber/core/gherkin/tag_expression'
+ else
+ require 'gherkin/tag_expression'
+ Cucumber::Core::Gherkin = Gherkin
+ end
class Cucumber::Core::Ast::Feature
# Code inspired by Cucumber::Core::Test::Case.match_tags?() in
# cucumber-ruby-core 1.1.3, lib/cucumber/core/test/case.rb:~59.
def accept_hook?(hook)
- tag_expr = Gherkin::TagExpression.new(hook.tag_expressions.flatten)
- tags = @tags.map do |t|
- Gherkin::Formatter::Model::Tag.new(t.name, t.line)
- end
- tag_expr.evaluate(tags)
+ tag_expr = Cucumber::Core::Gherkin::TagExpression.new(hook.tag_expressions.flatten)
+ tag_expr.evaluate(@tags)
end
end
end
@@ -53,10 +56,10 @@ if not($at_exit_print_artifacts_dir_patching_done)
alias old_print_stats print_stats
end
def print_stats(*args)
- if Dir.exists?(ARTIFACTS_DIR) and Dir.entries(ARTIFACTS_DIR).size > 2
- @io.puts "Artifacts directory: #{ARTIFACTS_DIR}"
- @io.puts
- end
+ @io.puts "Artifacts directory: #{ARTIFACTS_DIR}"
+ @io.puts
+ @io.puts "Debug log: #{ARTIFACTS_DIR}/debug.log"
+ @io.puts
if self.class.method_defined?(:old_print_stats)
old_print_stats(*args)
end
@@ -74,7 +77,16 @@ def info_log(message = "", options = {})
end
def debug_log(message, options = {})
- $debug_log_fns.each { |fn| fn.call(message, options) } if $debug_log_fns
+ options[:timestamp] = true unless options.has_key?(:timestamp)
+ if $debug_log_fns
+ if options[:timestamp]
+ # Force UTC so the local timezone difference vs UTC won't be
+ # added to the result.
+ elapsed = (Time.now - TIME_AT_START.to_f).utc.strftime("%H:%M:%S.%9N")
+ message = "#{elapsed}: #{message}"
+ end
+ $debug_log_fns.each { |fn| fn.call(message, options) }
+ end
end
require 'cucumber/formatter/pretty'
@@ -104,8 +116,11 @@ module ExtraFormatters
# anything. We only use it do hook into the correct events so we can
# add our extra hooks.
class ExtraHooks
- def initialize(*args)
+ def initialize(runtime, io, options)
# We do not care about any of the arguments.
+ # XXX: We should be able to just have `*args` for the arguments
+ # in the prototype, but since moving to cucumber 2.4 that breaks
+ # this formatter for some unknown reason.
end
def before_feature(feature)
@@ -127,8 +142,8 @@ module ExtraFormatters
# The pretty formatter with debug logging mixed into its output.
class PrettyDebug < Cucumber::Formatter::Pretty
- def initialize(*args)
- super(*args)
+ def initialize(runtime, io, options)
+ super(runtime, io, options)
$debug_log_fns ||= []
$debug_log_fns << self.method(:debug_log)
end
@@ -160,6 +175,13 @@ AfterConfiguration do |config|
# AfterConfiguration hook multiple times. We only want our
# ExtraHooks formatter to be loaded once, otherwise the hooks would
# be run miltiple times.
- extra_hooks = ['ExtraFormatters::ExtraHooks', '/dev/null']
- config.formats << extra_hooks if not(config.formats.include?(extra_hooks))
+ extra_hooks = [
+ ['ExtraFormatters::ExtraHooks', '/dev/null'],
+ ['Cucumber::Formatter::Pretty', "#{ARTIFACTS_DIR}/pretty.log"],
+ ['Cucumber::Formatter::Json', "#{ARTIFACTS_DIR}/cucumber.json"],
+ ['ExtraFormatters::PrettyDebug', "#{ARTIFACTS_DIR}/debug.log"],
+ ]
+ extra_hooks.each do |hook|
+ config.formats << hook if not(config.formats.include?(hook))
+ end
end
diff --git a/cucumber/features/support/helpers/dogtail.rb b/cucumber/features/support/helpers/dogtail.rb
new file mode 100644
index 00000000..2a92649b
--- /dev/null
+++ b/cucumber/features/support/helpers/dogtail.rb
@@ -0,0 +1,233 @@
+module Dogtail
+ module Mouse
+ LEFT_CLICK = 1
+ MIDDLE_CLICK = 2
+ RIGHT_CLICK = 3
+ end
+
+ TREE_API_NODE_SEARCHES = [
+ :button,
+ :child,
+ :childLabelled,
+ :childNamed,
+ :dialog,
+ :menu,
+ :menuItem,
+ :tab,
+ :textentry,
+ ]
+
+ TREE_API_NODE_SEARCH_FIELDS = [
+ :parent,
+ ]
+
+ TREE_API_NODE_ACTIONS = [
+ :click,
+ :doubleClick,
+ :grabFocus,
+ :keyCombo,
+ :point,
+ :typeText,
+ ]
+
+ TREE_API_APP_SEARCHES = TREE_API_NODE_SEARCHES + [
+ :dialog,
+ :window,
+ ]
+
+ # We want to keep this class immutable so that handles always are
+ # left intact when doing new (proxied) method calls. This way we
+ # can support stuff like:
+ #
+ # app = Dogtail::Application.new('gedit')
+ # menu = app.menu('Menu')
+ # menu.click()
+ # menu.something_else()
+ # menu.click()
+ #
+ # i.e. the object referenced by `menu` is never modified by method
+ # calls and can be used as expected.
+
+ class Application
+ @@node_counter ||= 0
+
+ def initialize(app_name, opts = {})
+ @var = "node#{@@node_counter += 1}"
+ @app_name = app_name
+ @opts = opts
+ @opts[:user] ||= LIVE_USER
+ @find_code = "dogtail.tree.root.application('#{@app_name}')"
+ script_lines = [
+ "import dogtail.config",
+ "import dogtail.tree",
+ "import dogtail.predicate",
+ "dogtail.config.logDebugToFile = False",
+ "dogtail.config.logDebugToStdOut = False",
+ "dogtail.config.blinkOnActions = True",
+ "dogtail.config.searchShowingOnly = True",
+ "#{@var} = #{@find_code}",
+ ]
+ run(script_lines)
+ end
+
+ def to_s
+ @var
+ end
+
+ def run(code)
+ code = code.join("\n") if code.class == Array
+ c = RemoteShell::PythonCommand.new($vm, code, user: @opts[:user])
+ if c.failure?
+ raise RuntimeError.new("The Dogtail script raised: #{c.exception}")
+ end
+ return c
+ end
+
+ def child?(*args)
+ !!child(*args)
+ rescue
+ false
+ end
+
+ def exist?
+ run("dogtail.config.searchCutoffCount = 0")
+ run(@find_code)
+ return true
+ rescue
+ return false
+ ensure
+ run("dogtail.config.searchCutoffCount = 20")
+ end
+
+ def self.value_to_s(v)
+ if v == true
+ 'True'
+ elsif v == false
+ 'False'
+ elsif v.class == String
+ "'#{v}'"
+ elsif [Fixnum, Float].include?(v.class)
+ v.to_s
+ else
+ raise "#{self.class.name} does not know how to handle argument type '#{v.class}'"
+ end
+ end
+
+ # Generates a Python-style parameter list from `args`. If the last
+ # element of `args` is a Hash, it's used as Python's kwargs dict.
+ # In the end, the resulting string should be possible to copy-paste
+ # into the parentheses of a Python function call.
+ # Example: [42, {:foo => 'bar'}] => "42, foo = 'bar'"
+ def self.args_to_s(args)
+ return "" if args.size == 0
+ args_list = args
+ args_hash = nil
+ if args_list.class == Array && args_list.last.class == Hash
+ *args_list, args_hash = args_list
+ end
+ (
+ (args_list.nil? ? [] : args_list.map { |e| self.value_to_s(e) }) +
+ (args_hash.nil? ? [] : args_hash.map { |k, v| "#{k}=#{self.value_to_s(v)}" })
+ ).join(', ')
+ end
+
+ # Equivalent to the Tree API's Node.findChildren(), with the
+ # arguments constructing a GenericPredicate to use as parameter.
+ def children(*args)
+ non_predicates = [:recursive, :showingOnly]
+ findChildren_opts = []
+ findChildren_opts_hash = Hash.new
+ if args.last.class == Hash
+ args_hash = args.last
+ non_predicates.each do |opt|
+ if args_hash.has_key?(opt)
+ findChildren_opts_hash[opt] = args_hash[opt]
+ args_hash.delete(opt)
+ end
+ end
+ end
+ findChildren_opts = ""
+ if findChildren_opts_hash.size > 0
+ findChildren_opts = ", " + self.class.args_to_s([findChildren_opts_hash])
+ end
+ predicate_opts = self.class.args_to_s(args)
+ nodes_var = "nodes#{@@node_counter += 1}"
+ find_script_lines = [
+ "#{nodes_var} = #{@var}.findChildren(dogtail.predicate.GenericPredicate(#{predicate_opts})#{findChildren_opts})",
+ "print(len(#{nodes_var}))",
+ ]
+ size = run(find_script_lines).stdout.chomp.to_i
+ return size.times.map do |i|
+ Node.new("#{nodes_var}[#{i}]", @opts)
+ end
+ end
+
+ def get_field(key)
+ run("print(#{@var}.#{key})").stdout.chomp
+ end
+
+ def set_field(key, value)
+ run("#{@var}.#{key} = #{self.class.value_to_s(value)}")
+ end
+
+ def text
+ get_field('text')
+ end
+
+ def text=(value)
+ set_field('text', value)
+ end
+
+ def name
+ get_field('name')
+ end
+
+ def roleName
+ get_field('roleName')
+ end
+
+ TREE_API_APP_SEARCHES.each do |method|
+ define_method(method) do |*args|
+ args_str = self.class.args_to_s(args)
+ method_call = "#{method.to_s}(#{args_str})"
+ Node.new("#{@var}.#{method_call}", @opts)
+ end
+ end
+
+ TREE_API_NODE_SEARCH_FIELDS.each do |field|
+ define_method(field) do
+ Node.new("#{@var}.#{field}", @opts)
+ end
+ end
+
+ end
+
+ class Node < Application
+
+ def initialize(expr, opts = {})
+ @expr = expr
+ @opts = opts
+ @opts[:user] ||= LIVE_USER
+ @find_code = expr
+ @var = "node#{@@node_counter += 1}"
+ run("#{@var} = #{@find_code}")
+ end
+
+ TREE_API_NODE_SEARCHES.each do |method|
+ define_method(method) do |*args|
+ args_str = self.class.args_to_s(args)
+ method_call = "#{method.to_s}(#{args_str})"
+ Node.new("#{@var}.#{method_call}", @opts)
+ end
+ end
+
+ TREE_API_NODE_ACTIONS.each do |method|
+ define_method(method) do |*args|
+ args_str = self.class.args_to_s(args)
+ method_call = "#{method.to_s}(#{args_str})"
+ run("#{@var}.#{method_call}")
+ end
+ end
+
+ end
+end
diff --git a/cucumber/features/support/helpers/exec_helper.rb b/cucumber/features/support/helpers/exec_helper.rb
deleted file mode 100644
index 70d22d37..00000000
--- a/cucumber/features/support/helpers/exec_helper.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-require 'json'
-require 'socket'
-require 'io/wait'
-
-class VMCommand
-
- attr_reader :cmd, :returncode, :stdout, :stderr
-
- def initialize(vm, cmd, options = {})
- @cmd = cmd
- @returncode, @stdout, @stderr = VMCommand.execute(vm, cmd, options)
- end
-
- def VMCommand.wait_until_remote_shell_is_up(vm, timeout = 180)
- try_for(timeout, :msg => "Remote shell seems to be down") do
- Timeout::timeout(20) do
- VMCommand.execute(vm, "echo 'hello?'")
- end
- end
- end
-
- # The parameter `cmd` cannot contain newlines. Separate multiple
- # commands using ";" instead.
- # If `:spawn` is false the server will block until it has finished
- # executing `cmd`. If it's true the server won't block, and the
- # response will always be [0, "", ""] (only used as an
- # ACK). execute() will always block until a response is received,
- # though. Spawning is useful when starting processes in the
- # background (or running scripts that does the same) like our
- # onioncircuits wrapper, or any application we want to interact with.
- def VMCommand.execute(vm, cmd, options = {})
- options[:user] ||= "root"
- options[:spawn] ||= false
- type = options[:spawn] ? "spawn" : "call"
- socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
- debug_log("#{type}ing as #{options[:user]}: #{cmd}")
- begin
- sleep 0.5
- while socket.ready?
- s = socket.recv(1024)
- debug_log("#{type} pre-exit-debris: #{s}") if not(options[:spawn])
- end
- socket.puts( "\nexit\n")
- sleep 1
- s = socket.readline(sep = "\007")
- debug_log("#{type} post-exit-read: #{s}") if not(options[:spawn])
- while socket.ready?
- s = socket.recv(1024)
- debug_log("#{type} post-exit-debris: #{s}") if not(options[:spawn])
- end
- socket.puts( cmd + "\n")
- s = socket.readline(sep = "\000")
- debug_log("#{type} post-cmd-read: #{s}") if not(options[:spawn])
- s.chomp!("\000")
- ensure
- debug_log("closing the remote-command socket") if not(options[:spawn])
- socket.close
- end
- (s, s_err, x) = s.split("\037")
- s_err = "" if s_err.nil?
- (s, s_retcode, y) = s.split("\003")
- (s, s_out, z) = s.split("\002")
- s_out = "" if s_out.nil?
-
- if (s_retcode.to_i.to_s == s_retcode.to_s && x.nil? && y.nil? && z.nil?) then
- debug_log("returning [returncode=`#{s_retcode.to_i}`,\n\toutput=`#{s_out}`,\n\tstderr=`#{s_err}`]\nwhile discarding `#{s}`.") if not(options[:spawn])
- return [s_retcode.to_i, s_out, s_err]
- else
- debug_log("failed to parse results, retrying\n")
- return VMCommand.execute(vm, cmd, options)
- end
- end
-
- def success?
- return @returncode == 0
- end
-
- def failure?
- return not(success?)
- end
-
- def to_s
- "Return status: #{@returncode}\n" +
- "STDOUT:\n" +
- @stdout +
- "STDERR:\n" +
- @stderr
- end
-
-end
diff --git a/cucumber/features/support/helpers/firewall_helper.rb b/cucumber/features/support/helpers/firewall_helper.rb
index fce363c5..f88091de 100644
--- a/cucumber/features/support/helpers/firewall_helper.rb
+++ b/cucumber/features/support/helpers/firewall_helper.rb
@@ -1,121 +1,94 @@
require 'packetfu'
-require 'ipaddr'
-# Extent IPAddr with a private/public address space checks
-class IPAddr
- PrivateIPv4Ranges = [
- IPAddr.new("10.0.0.0/8"),
- IPAddr.new("172.16.0.0/12"),
- IPAddr.new("192.168.0.0/16"),
- IPAddr.new("255.255.255.255/32")
- ]
-
- PrivateIPv6Ranges = [
- IPAddr.new("fc00::/7")
- ]
-
- def private?
- private_ranges = self.ipv4? ? PrivateIPv4Ranges : PrivateIPv6Ranges
- private_ranges.any? { |range| range.include?(self) }
- end
-
- def public?
- !private?
- end
+def looks_like_dhcp_packet?(eth_packet, protocol, sport, dport, ip_packet)
+ protocol == "udp" && sport == 68 && dport == 67 &&
+ eth_packet.eth_daddr == "ff:ff:ff:ff:ff:ff" &&
+ ip_packet && ip_packet.ip_daddr == "255.255.255.255"
end
-class FirewallLeakCheck
- attr_reader :ipv4_tcp_leaks, :ipv4_nontcp_leaks, :ipv6_leaks, :nonip_leaks, :mac_leaks
-
- def initialize(pcap_file, options = {})
- options[:accepted_hosts] ||= []
- options[:ignore_lan] ||= true
- @pcap_file = pcap_file
- packets = PacketFu::PcapFile.new.file_to_array(:filename => @pcap_file)
- mac_leaks = Set.new
- ipv4_tcp_packets = []
- ipv4_nontcp_packets = []
- ipv6_packets = []
- nonip_packets = []
- packets.each do |p|
- if PacketFu::EthPacket.can_parse?(p)
- packet = PacketFu::EthPacket.parse(p)
- mac_leaks << packet.eth_saddr
- mac_leaks << packet.eth_daddr
- end
-
- if PacketFu::TCPPacket.can_parse?(p)
- ipv4_tcp_packets << PacketFu::TCPPacket.parse(p)
- elsif PacketFu::IPPacket.can_parse?(p)
- ipv4_nontcp_packets << PacketFu::IPPacket.parse(p)
- elsif PacketFu::IPv6Packet.can_parse?(p)
- ipv6_packets << PacketFu::IPv6Packet.parse(p)
- elsif PacketFu::Packet.can_parse?(p)
- nonip_packets << PacketFu::Packet.parse(p)
- else
- save_pcap_file
- raise "Found something in the pcap file that cannot be parsed"
- end
+# Returns the unique edges (based on protocol, source/destination
+# address/port) in the graph of all network flows.
+def pcap_connections_helper(pcap_file, opts = {})
+ opts[:ignore_dhcp] = true unless opts.has_key?(:ignore_dhcp)
+ connections = Array.new
+ packets = PacketFu::PcapFile.new.file_to_array(:filename => pcap_file)
+ packets.each do |p|
+ if PacketFu::EthPacket.can_parse?(p)
+ eth_packet = PacketFu::EthPacket.parse(p)
+ else
+ raise 'Found something that is not an ethernet packet'
+ end
+ sport = nil
+ dport = nil
+ if PacketFu::IPv6Packet.can_parse?(p)
+ ip_packet = PacketFu::IPv6Packet.parse(p)
+ protocol = 'ipv6'
+ elsif PacketFu::TCPPacket.can_parse?(p)
+ ip_packet = PacketFu::TCPPacket.parse(p)
+ protocol = 'tcp'
+ sport = ip_packet.tcp_sport
+ dport = ip_packet.tcp_dport
+ elsif PacketFu::UDPPacket.can_parse?(p)
+ ip_packet = PacketFu::UDPPacket.parse(p)
+ protocol = 'udp'
+ sport = ip_packet.udp_sport
+ dport = ip_packet.udp_dport
+ elsif PacketFu::ICMPPacket.can_parse?(p)
+ ip_packet = PacketFu::ICMPPacket.parse(p)
+ protocol = 'icmp'
+ elsif PacketFu::IPPacket.can_parse?(p)
+ ip_packet = PacketFu::IPPacket.parse(p)
+ protocol = 'ip'
+ else
+ raise "Found something that cannot be parsed"
end
- ipv4_tcp_hosts = filter_hosts_from_ippackets(ipv4_tcp_packets,
- options[:ignore_lan])
- accepted = Set.new(options[:accepted_hosts])
- @mac_leaks = mac_leaks
- @ipv4_tcp_leaks = ipv4_tcp_hosts.select { |host| !accepted.member?(host) }
- @ipv4_nontcp_leaks = filter_hosts_from_ippackets(ipv4_nontcp_packets,
- options[:ignore_lan])
- @ipv6_leaks = filter_hosts_from_ippackets(ipv6_packets,
- options[:ignore_lan])
- @nonip_leaks = nonip_packets
- end
- def save_pcap_file
- save_failure_artifact("Network capture", @pcap_file)
- end
+ next if opts[:ignore_dhcp] &&
+ looks_like_dhcp_packet?(eth_packet, protocol,
+ sport, dport, ip_packet)
- # Returns a list of all unique destination IP addresses found in
- # `packets`. Exclude LAN hosts if ignore_lan is set.
- def filter_hosts_from_ippackets(packets, ignore_lan)
- hosts = []
- packets.each do |p|
- candidate = nil
- if p.kind_of?(PacketFu::IPPacket)
- candidate = p.ip_daddr
- elsif p.kind_of?(PacketFu::IPv6Packet)
- candidate = p.ipv6_header.ipv6_daddr
- else
- save_pcap_file
- raise "Expected an IP{v4,v6} packet, but got something else:\n" +
- p.peek_format
- end
- if candidate != nil and (not(ignore_lan) or IPAddr.new(candidate).public?)
- hosts << candidate
+ packet_info = {
+ mac_saddr: eth_packet.eth_saddr,
+ mac_daddr: eth_packet.eth_daddr,
+ protocol: protocol,
+ sport: sport,
+ dport: dport,
+ }
+
+ begin
+ packet_info[:saddr] = ip_packet.ip_saddr
+ packet_info[:daddr] = ip_packet.ip_daddr
+ rescue NoMethodError, NameError
+ begin
+ packet_info[:saddr] = ip_packet.ipv6_saddr
+ packet_info[:daddr] = ip_packet.ipv6_daddr
+ rescue NoMethodError, NameError
+ puts "We were hit by #11508. PacketFu bug? Packet info: #{ip_packet}"
+ packet_info[:saddr] = nil
+ packet_info[:daddr] = nil
end
end
- hosts.uniq
+ connections << packet_info
end
+ connections.uniq.map { |p| OpenStruct.new(p) }
+end
- def assert_no_leaks
- err = ""
- if !@ipv4_tcp_leaks.empty?
- err += "The following IPv4 TCP non-Tor Internet hosts were " +
- "contacted:\n" + ipv4_tcp_leaks.join("\n")
- end
- if !@ipv4_nontcp_leaks.empty?
- err += "The following IPv4 non-TCP Internet hosts were contacted:\n" +
- ipv4_nontcp_leaks.join("\n")
- end
- if !@ipv6_leaks.empty?
- err += "The following IPv6 Internet hosts were contacted:\n" +
- ipv6_leaks.join("\n")
- end
- if !@nonip_leaks.empty?
- err += "Some non-IP packets were sent\n"
- end
- if !err.empty?
- save_pcap_file
- raise err
- end
+class FirewallAssertionFailedError < Test::Unit::AssertionFailedError
+end
+
+# These assertions are made from the perspective of the system under
+# testing when it comes to the concepts of "source" and "destination".
+def assert_all_connections(pcap_file, opts = {}, &block)
+ all = pcap_connections_helper(pcap_file, opts)
+ good = all.find_all(&block)
+ bad = all - good
+ unless bad.empty?
+ raise FirewallAssertionFailedError.new(
+ "Unexpected connections were made:\n" +
+ bad.map { |e| " #{e}" } .join("\n"))
end
+end
+def assert_no_connections(pcap_file, opts = {}, &block)
+ assert_all_connections(pcap_file, opts) { |*args| not(block.call(*args)) }
end
diff --git a/cucumber/features/support/helpers/misc_helpers.rb b/cucumber/features/support/helpers/misc_helpers.rb
index 7e09411f..865d2978 100644
--- a/cucumber/features/support/helpers/misc_helpers.rb
+++ b/cucumber/features/support/helpers/misc_helpers.rb
@@ -1,4 +1,6 @@
require 'date'
+require 'io/console'
+require 'pry'
require 'timeout'
require 'test/unit'
@@ -28,8 +30,12 @@ end
# Call block (ignoring any exceptions it may throw) repeatedly with
# one second breaks until it returns true, or until `timeout` seconds have
-# passed when we throw a Timeout::Error exception.
+# passed when we throw a Timeout::Error exception. If `timeout` is `nil`,
+# then we just run the code block with no timeout.
def try_for(timeout, options = {})
+ if block_given? && timeout.nil?
+ return yield
+ end
options[:delay] ||= 1
last_exception = nil
# Create a unique exception used only for this particular try_for
@@ -76,11 +82,12 @@ def try_for(timeout, options = {})
# ends up there immediately.
rescue unique_timeout_exception => e
msg = options[:msg] || 'try_for() timeout expired'
+ exc_class = options[:exception] || Timeout::Error
if last_exception
msg += "\nLast ignored exception was: " +
"#{last_exception.class}: #{last_exception}"
end
- raise Timeout::Error.new(msg)
+ raise exc_class.new(msg)
end
class TorFailure < StandardError
@@ -89,6 +96,19 @@ end
class MaxRetriesFailure < StandardError
end
+def force_new_tor_circuit()
+ debug_log("Forcing new Tor circuit...")
+ # Tor rate limits NEWNYM to at most one per 10 second period.
+ interval = 10
+ if $__last_newnym
+ elapsed = Time.now - $__last_newnym
+ # We sleep an extra second to avoid tight timings.
+ sleep interval - elapsed + 1 if 0 < elapsed && elapsed < interval
+ end
+ $vm.execute_successfully('tor_control_send "signal NEWNYM"', :libs => 'tor')
+ $__last_newnym = Time.now
+end
+
# This will retry the block up to MAX_NEW_TOR_CIRCUIT_RETRIES
# times. The block must raise an exception for a run to be considered
# as a failure. After a failure recovery_proc will be called (if
@@ -105,11 +125,6 @@ def retry_tor(recovery_proc = nil, &block)
:operation_name => 'Tor operation', &block)
end
-def retry_i2p(recovery_proc = nil, &block)
- retry_action(15, :recovery_proc => recovery_proc,
- :operation_name => 'I2P operation', &block)
-end
-
def retry_action(max_retries, options = {}, &block)
assert(max_retries.is_a?(Integer), "max_retries must be an integer")
options[:recovery_proc] ||= nil
@@ -120,6 +135,10 @@ def retry_action(max_retries, options = {}, &block)
begin
block.call
return
+ rescue NameError => e
+ # NameError most likely means typos, and hiding that is rarely
+ # (never?) a good idea, so we rethrow them.
+ raise e
rescue Exception => e
if retries <= max_retries
debug_log("#{options[:operation_name]} failed (Try #{retries} of " +
@@ -136,16 +155,15 @@ def retry_action(max_retries, options = {}, &block)
end
end
+alias :retry_times :retry_action
+
+class TorBootstrapFailure < StandardError
+end
+
def wait_until_tor_is_working
try_for(270) { $vm.execute('/usr/local/sbin/tor-has-bootstrapped').success? }
-rescue Timeout::Error => e
- c = $vm.execute("journalctl SYSLOG_IDENTIFIER=restart-tor")
- if c.success?
- debug_log("From the journal:\n" + c.stdout.sub(/^/, " "))
- else
- debug_log("Nothing was in the journal about 'restart-tor'")
- end
- raise e
+rescue Timeout::Error
+ raise TorBootstrapFailure.new('Tor failed to bootstrap')
end
def convert_bytes_mod(unit)
@@ -177,13 +195,14 @@ def convert_from_bytes(size, unit)
return size.to_f/convert_bytes_mod(unit).to_f
end
-def cmd_helper(cmd)
+def cmd_helper(cmd, env = {})
if cmd.instance_of?(Array)
cmd << {:err => [:child, :out]}
elsif cmd.instance_of?(String)
cmd += " 2>&1"
end
- IO.popen(cmd) do |p|
+ env = ENV.to_h.merge(env)
+ IO.popen(env, cmd) do |p|
out = p.readlines.join("\n")
p.close
ret = $?
@@ -192,11 +211,23 @@ def cmd_helper(cmd)
end
end
-# This command will grab all router IP addresses from the Tor
-# consensus in the VM + the hardcoded TOR_AUTHORITIES.
-def get_all_tor_nodes
- cmd = 'awk "/^r/ { print \$6 }" /var/lib/tor/cached-microdesc-consensus'
- $vm.execute(cmd).stdout.chomp.split("\n") + TOR_AUTHORITIES
+def all_tor_hosts
+ nodes = Array.new
+ chutney_torrcs = Dir.glob(
+ "#{$config['TMPDIR']}/chutney-data/nodes/*/torrc"
+ )
+ chutney_torrcs.each do |torrc|
+ open(torrc) do |f|
+ nodes += f.grep(/^(Or|Dir)Port\b/).map do |line|
+ { address: $vmnet.bridge_ip_addr, port: line.split.last.to_i }
+ end
+ end
+ end
+ return nodes
+end
+
+def allowed_hosts_under_tor_enforcement
+ all_tor_hosts + @lan_hosts
end
def get_free_space(machine, path)
@@ -246,8 +277,68 @@ def info_log_artifact_location(type, path)
info_log("#{type.capitalize}: #{path}")
end
+def notify_user(message)
+ alarm_script = $config['NOTIFY_USER_COMMAND']
+ return if alarm_script.nil? || alarm_script.empty?
+ cmd_helper(alarm_script.gsub('%m', message))
+end
+
def pause(message = "Paused")
+ notify_user(message)
+ STDERR.puts
+ STDERR.puts message
+ # Ring the ASCII bell for a helpful notification in most terminal
+ # emulators.
+ STDOUT.write "\a"
STDERR.puts
- STDERR.puts "#{message} (Press ENTER to continue!)"
- STDIN.gets
+ loop do
+ STDERR.puts "Return: Continue; d: Debugging REPL"
+ c = STDIN.getch
+ case c
+ when "\r"
+ return
+ when "d"
+ binding.pry(quiet: true)
+ end
+ end
+end
+
+def dbus_send(service, object_path, method, *args, **opts)
+ opts ||= {}
+ ruby_type_to_dbus_type = {
+ String => 'string',
+ Fixnum => 'int32',
+ }
+ typed_args = args.map do |arg|
+ type = ruby_type_to_dbus_type[arg.class]
+ assert_not_nil(type, "No DBus type conversion for Ruby type '#{arg.class}'")
+ "#{type}:#{arg}"
+ end
+ ret = $vm.execute_successfully(
+ "dbus-send --print-reply --dest=#{service} #{object_path} " +
+ " #{method} #{typed_args.join(' ')}",
+ **opts
+ ).stdout.lines
+ # The first line written is about timings and other stuff we don't
+ # care about; we only care about the return values.
+ ret.shift
+ ret.map! do |s|
+ type, val = /^\s*(\S+)\s+(\S+)$/.match(s)[1,2]
+ case type
+ when 'string'
+ # Unquote
+ val[1, val.length - 2]
+ when 'int32'
+ val.to_i
+ else
+ raise "No Ruby type conversion for DBus type '#{type}'"
+ end
+ end
+ if ret.size == 0
+ return nil
+ elsif ret.size == 1
+ return ret.first
+ else
+ return ret
+ end
end
diff --git a/cucumber/features/support/helpers/remote_shell.rb b/cucumber/features/support/helpers/remote_shell.rb
new file mode 100644
index 00000000..b890578b
--- /dev/null
+++ b/cucumber/features/support/helpers/remote_shell.rb
@@ -0,0 +1,171 @@
+require 'base64'
+require 'json'
+require 'socket'
+require 'timeout'
+
+module RemoteShell
+ class ServerFailure < StandardError
+ end
+
+ # Used to differentiate vs Timeout::Error, which is thrown by
+ # try_for() (by default) and often wraps around remote shell usage
+ # -- in that case we don't want to catch that "outer" exception in
+ # our handling of remote shell timeouts below.
+ class Timeout < ServerFailure
+ end
+
+ DEFAULT_TIMEOUT = 20*60
+
+ # Counter providing unique id:s for each communicate() call.
+ @@request_id ||= 0
+
+ def communicate(vm, *args, **opts)
+ opts[:timeout] ||= DEFAULT_TIMEOUT
+ socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
+ id = (@@request_id += 1)
+ # Since we already have defined our own Timeout in the current
+ # scope, we have to be more careful when referring to the Timeout
+ # class from the 'timeout' module. However, note that we want it
+ # to throw our own Timeout exception.
+ Object::Timeout.timeout(opts[:timeout], Timeout) do
+ socket.puts(JSON.dump([id] + args))
+ socket.flush
+ loop do
+ line = socket.readline("\n").chomp("\n")
+ response_id, status, *rest = JSON.load(line)
+ if response_id == id
+ if status != "success"
+ if status == "error" and rest.class == Array and rest.size == 1
+ msg = rest.first
+ raise ServerFailure.new("#{msg}")
+ else
+ raise ServerFailure.new("Uncaught exception: #{status}: #{rest}")
+ end
+ end
+ return rest
+ else
+ debug_log("Dropped out-of-order remote shell response: " +
+ "got id #{response_id} but expected id #{id}")
+ end
+ end
+ end
+ ensure
+ socket.close if defined?(socket) && socket
+ end
+
+ module_function :communicate
+ private :communicate
+
+ class ShellCommand
+ # If `:spawn` is false the server will block until it has finished
+ # executing `cmd`. If it's true the server won't block, and the
+ # response will always be [0, "", ""] (only used as an
+ # ACK). execute() will always block until a response is received,
+ # though. Spawning is useful when starting processes in the
+ # background (or running scripts that does the same) or any
+ # application we want to interact with.
+ def self.execute(vm, cmd, **opts)
+ opts[:user] ||= "root"
+ opts[:spawn] = false unless opts.has_key?(:spawn)
+ type = opts[:spawn] ? "spawn" : "call"
+ debug_log("#{type}ing as #{opts[:user]}: #{cmd}")
+ ret = RemoteShell.communicate(vm, 'sh_' + type, opts[:user], cmd, **opts)
+ debug_log("#{type} returned: #{ret}") if not(opts[:spawn])
+ return ret
+ end
+
+ attr_reader :cmd, :returncode, :stdout, :stderr
+
+ def initialize(vm, cmd, **opts)
+ @cmd = cmd
+ @returncode, @stdout, @stderr = self.class.execute(vm, cmd, **opts)
+ end
+
+ def success?
+ return @returncode == 0
+ end
+
+ def failure?
+ return not(success?)
+ end
+
+ def to_s
+ "Return status: #{@returncode}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+ end
+
+ class PythonCommand
+ def self.execute(vm, code, **opts)
+ opts[:user] ||= "root"
+ show_code = code.chomp
+ if show_code["\n"]
+ show_code = "\n" + show_code.lines.map { |l| " "*4 + l.chomp } .join("\n")
+ end
+ debug_log("executing Python as #{opts[:user]}: #{show_code}")
+ ret = RemoteShell.communicate(
+ vm, 'python_execute', opts[:user], code, **opts
+ )
+ debug_log("execution complete")
+ return ret
+ end
+
+ attr_reader :code, :exception, :stdout, :stderr
+
+ def initialize(vm, code, **opts)
+ @code = code
+ @exception, @stdout, @stderr = self.class.execute(vm, code, **opts)
+ end
+
+ def success?
+ return @exception == nil
+ end
+
+ def failure?
+ return not(success?)
+ end
+
+ def to_s
+ "Exception: #{@exception}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+ end
+
+ # An IO-like object that is more or less equivalent to a File object
+ # opened in rw mode.
+ class File
+ def self.open(vm, mode, path, *args, **opts)
+ debug_log("opening file #{path} in '#{mode}' mode")
+ ret = RemoteShell.communicate(vm, 'file_' + mode, path, *args, **opts)
+ if ret.size != 1
+ raise ServerFailure.new("expected 1 value but got #{ret.size}")
+ end
+ debug_log("#{mode} complete")
+ return ret.first
+ end
+
+ attr_reader :vm, :path
+
+ def initialize(vm, path)
+ @vm, @path = vm, path
+ end
+
+ def read()
+ Base64.decode64(self.class.open(@vm, 'read', @path))
+ end
+
+ def write(data)
+ self.class.open(@vm, 'write', @path, Base64.encode64(data))
+ end
+
+ def append(data)
+ self.class.open(@vm, 'append', @path, Base64.encode64(data))
+ end
+ end
+end
diff --git a/cucumber/features/support/helpers/sikuli_helper.rb b/cucumber/features/support/helpers/sikuli_helper.rb
index 553abd97..167eded3 100644
--- a/cucumber/features/support/helpers/sikuli_helper.rb
+++ b/cucumber/features/support/helpers/sikuli_helper.rb
@@ -1,9 +1,19 @@
require 'rjb'
require 'rjbextension'
$LOAD_PATH << ENV['SIKULI_HOME']
-require 'sikuli-script.jar'
+begin
+ require 'sikulixapi.jar'
+ USING_SIKULIX = true
+rescue LoadError
+ require 'sikuli-script.jar'
+ USING_SIKULIX = false
+end
Rjb::load
+def using_sikulix?
+ USING_SIKULIX
+end
+
package_members = [
"java.io.FileOutputStream",
"java.io.PrintStream",
@@ -16,11 +26,18 @@ package_members = [
"org.sikuli.script.Pattern",
"org.sikuli.script.Region",
"org.sikuli.script.Screen",
- "org.sikuli.script.Settings",
]
+if using_sikulix?
+ package_members << "org.sikuli.basics.Settings"
+ package_members << "org.sikuli.script.ImagePath"
+else
+ package_members << "org.sikuli.script.Settings"
+end
+
translations = Hash[
"org.sikuli.script", "Sikuli",
+ "org.sikuli.basics", "Sikuli",
"java.lang", "Java::Lang",
"java.io", "Java::Io",
]
@@ -186,13 +203,20 @@ def sikuli_script_proxy.new(*args)
end
def s.hide_cursor
- self.hover_point(self.w, self.h/2)
+ self.hover_point(self.w - 1, self.h/2)
end
s
end
# Configure sikuli
+if using_sikulix?
+ Sikuli::ImagePath.add("#{Dir.pwd}/features/images/")
+else
+ java.lang.System.setProperty("SIKULI_IMAGE_PATH",
+ "#{Dir.pwd}/features/images/")
+ ENV["SIKULI_IMAGE_PATH"] = "#{Dir.pwd}/features/images/"
+end
# ruby and rjb doesn't play well together when it comes to static
# fields (and possibly methods) so we instantiate and access the field
@@ -210,5 +234,5 @@ sikuli_settings.MinSimilarity = 0.9
sikuli_settings.ActionLogs = true
sikuli_settings.DebugLogs = false
sikuli_settings.InfoLogs = true
-sikuli_settings.ProfileLogs = true
+sikuli_settings.ProfileLogs = false
sikuli_settings.WaitScanRate = 0.25
diff --git a/cucumber/features/support/helpers/sniffing_helper.rb b/cucumber/features/support/helpers/sniffing_helper.rb
index 213411eb..38b13820 100644
--- a/cucumber/features/support/helpers/sniffing_helper.rb
+++ b/cucumber/features/support/helpers/sniffing_helper.rb
@@ -22,8 +22,18 @@ class Sniffer
end
def capture(filter="not ether src host #{@vmnet.bridge_mac} and not ether proto \\arp and not ether proto \\rarp")
- job = IO.popen(["/usr/sbin/tcpdump", "-n", "-i", @vmnet.bridge_name, "-w",
- @pcap_file, "-U", filter, :err => ["/dev/null", "w"]])
+ job = IO.popen(
+ [
+ "/usr/sbin/tcpdump",
+ "-n",
+ "-U",
+ "--immediate-mode",
+ "-i", @vmnet.bridge_name,
+ "-w", @pcap_file,
+ filter,
+ :err => ["/dev/null", "w"]
+ ]
+ )
@pid = job.pid
end
diff --git a/cucumber/features/support/helpers/storage_helper.rb b/cucumber/features/support/helpers/storage_helper.rb
index de782eed..3bbdb69c 100644
--- a/cucumber/features/support/helpers/storage_helper.rb
+++ b/cucumber/features/support/helpers/storage_helper.rb
@@ -25,7 +25,8 @@ class VMStorage
rescue Libvirt::RetrieveError
@pool = nil
end
- if @pool and not(KEEP_SNAPSHOTS)
+ if @pool and (not(KEEP_SNAPSHOTS) or
+ (KEEP_SNAPSHOTS and not(Dir.exists?(@pool_path))))
VMStorage.clear_storage_pool(@pool)
@pool = nil
end
@@ -79,6 +80,10 @@ class VMStorage
VMStorage.clear_storage_pool_volumes(@pool)
end
+ def list_volumes
+ @pool.list_volumes
+ end
+
def delete_volume(name)
@pool.lookup_volume_by_name(name).delete
end
@@ -144,13 +149,7 @@ class VMStorage
end
def disk_mklabel(name, parttype)
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_init(disk_handle, parttype)
end
end
@@ -158,13 +157,7 @@ class VMStorage
def disk_mkpartfs(name, parttype, fstype, opts = {})
opts[:label] ||= nil
opts[:luks_password] ||= nil
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_disk(disk_handle, parttype)
g.part_set_name(disk_handle, 1, opts[:label]) if opts[:label]
primary_partition = g.list_partitions()[0]
@@ -182,13 +175,7 @@ class VMStorage
end
def disk_mkswap(name, parttype)
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_disk(disk_handle, parttype)
primary_partition = g.list_partitions()[0]
g.mkswap(primary_partition)
@@ -206,7 +193,13 @@ class VMStorage
Guestfs::EVENT_TRACE)
g.set_autosync(1)
disks.each do |disk|
- g.add_drive_opts(disk[:path], disk[:opts])
+ if disk.class == String
+ g.add_drive_opts(disk_path(disk), format: disk_format(disk))
+ elsif disk.class == Hash
+ g.add_drive_opts(disk[:path], disk[:opts])
+ else
+ raise "cannot handle type '#{disk.class}'"
+ end
end
g.launch()
yield(g, *g.list_devices())
diff --git a/cucumber/features/support/helpers/vm_helper.rb b/cucumber/features/support/helpers/vm_helper.rb
index 5d02c115..be3ae5ff 100644
--- a/cucumber/features/support/helpers/vm_helper.rb
+++ b/cucumber/features/support/helpers/vm_helper.rb
@@ -1,3 +1,4 @@
+require 'ipaddr'
require 'libvirt'
require 'rexml/document'
@@ -55,11 +56,6 @@ class VMNet
IPAddr.new(net_xml.elements['network/ip'].attributes['address']).to_s
end
- def guest_real_mac
- net_xml = REXML::Document.new(@net.xml_desc)
- net_xml.elements['network/ip/dhcp/host/'].attributes['mac']
- end
-
def bridge_mac
File.open("/sys/class/net/#{bridge_name}/address", "rb").read.chomp
end
@@ -68,7 +64,7 @@ end
class VM
- attr_reader :domain, :display, :vmnet, :storage
+ attr_reader :domain, :domain_name, :display, :vmnet, :storage
def initialize(virt, xml_path, vmnet, storage, x_display)
@virt = virt
@@ -114,8 +110,20 @@ class VM
end
end
- def real_mac
- @vmnet.guest_real_mac
+ def real_mac(alias_name)
+ REXML::Document.new(@domain.xml_desc)
+ .elements["domain/devices/interface[@type='network']/" +
+ "alias[@name='#{alias_name}']"]
+ .parent.elements['mac'].attributes['address'].to_s
+ end
+
+ def all_real_macs
+ macs = []
+ REXML::Document.new(@domain.xml_desc)
+ .elements.each("domain/devices/interface[@type='network']") do |nic|
+ macs << nic.elements['mac'].attributes['address'].to_s
+ end
+ macs
end
def set_hardware_clock(time)
@@ -131,6 +139,11 @@ class VM
update(domain_rexml.to_s)
end
+ def network_link_state
+ REXML::Document.new(@domain.xml_desc)
+ .elements['domain/devices/interface/link'].attributes['state']
+ end
+
def set_network_link_state(state)
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/devices/interface/link'].attributes['state'] = state
@@ -158,30 +171,44 @@ class VM
update(domain_xml.to_s)
end
- def set_cdrom_image(image)
- image = nil if image == ''
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements.each('domain/devices/disk') do |e|
- if e.attribute('device').to_s == "cdrom"
- if image.nil?
- e.elements.delete('source')
- else
- if ! e.elements['source']
- e.add_element('source')
- end
- e.elements['source'].attributes['file'] = image
- end
- if is_running?
- @domain.update_device(e.to_s)
- else
- update(domain_xml.to_s)
- end
- end
+ def add_cdrom_device
+ if is_running?
+ raise "Can't attach a CDROM device to a running domain"
+ end
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ if domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ raise "A CDROM device already exists"
end
+ cdrom_rexml = REXML::Document.new(File.read("#{@xml_path}/cdrom.xml")).root
+ domain_rexml.elements['domain/devices'].add_element(cdrom_rexml)
+ update(domain_rexml.to_s)
end
- def remove_cdrom
- set_cdrom_image(nil)
+ def remove_cdrom_device
+ if is_running?
+ raise "Can't detach a CDROM device to a running domain"
+ end
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ cdrom_el = domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ if cdrom_el.nil?
+ raise "No CDROM device is present"
+ end
+ domain_rexml.elements["domain/devices"].delete_element(cdrom_el)
+ update(domain_rexml.to_s)
+ end
+
+ def eject_cdrom
+ execute_successfully('/usr/bin/eject -m')
+ end
+
+ def remove_cdrom_image
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ cdrom_el = domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ if cdrom_el.nil?
+ raise "No CDROM device is present"
+ end
+ cdrom_el.delete_element('source')
+ update(domain_rexml.to_s)
rescue Libvirt::Error => e
# While the CD-ROM is removed successfully we still get this
# error, so let's ignore it.
@@ -192,12 +219,27 @@ class VM
raise e if not(Regexp.new(acceptable_error).match(e.to_s))
end
+ def set_cdrom_image(image)
+ if image.nil? or image == ''
+ raise "Can't set cdrom image to an empty string"
+ end
+ remove_cdrom_image
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ cdrom_el = domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ cdrom_el.add_element('source', { 'file' => image })
+ update(domain_rexml.to_s)
+ end
+
def set_cdrom_boot(image)
if is_running?
raise "boot settings can only be set for inactive vms"
end
- set_boot_device('cdrom')
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ if not domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ add_cdrom_device
+ end
set_cdrom_image(image)
+ set_boot_device('cdrom')
end
def list_disk_devs
@@ -209,6 +251,16 @@ class VM
return ret
end
+ def plug_device(xml)
+ if is_running?
+ @domain.attach_device(xml.to_s)
+ else
+ domain_xml = REXML::Document.new(@domain.xml_desc)
+ domain_xml.elements['domain/devices'].add_element(xml)
+ update(domain_xml.to_s)
+ end
+ end
+
def plug_drive(name, type)
if disk_plugged?(name)
raise "disk '#{name}' already plugged"
@@ -238,13 +290,7 @@ class VM
xml.elements['disk/target'].attributes['bus'] = type
xml.elements['disk/target'].attributes['removable'] = removable_usb if removable_usb
- if is_running?
- @domain.attach_device(xml.to_s)
- else
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain/devices'].add_element(xml)
- update(domain_xml.to_s)
- end
+ plug_device(xml)
end
def disk_xml_desc(name)
@@ -320,9 +366,16 @@ class VM
end
plug_drive(name, type) if not(disk_plugged?(name))
set_boot_device('hd')
- # For some reason setting the boot device doesn't prevent cdrom
- # boot unless it's empty
- remove_cdrom
+ # XXX:Stretch: since our isotesters upgraded QEMU from
+ # 2.5+dfsg-4~bpo8+1 to 2.6+dfsg-3.1~bpo8+1 it seems we must remove
+ # the CDROM device to allow disk boot. This is not the case with the same
+ # version on Debian Sid. Let's hope we can remove this ugly
+ # workaround when we only support running the automated test suite
+ # on Stretch.
+ domain_rexml = REXML::Document.new(@domain.xml_desc)
+ if domain_rexml.elements["domain/devices/disk[@device='cdrom']"]
+ remove_cdrom_device
+ end
end
# XXX-9p: Shares don't work together with snapshot save+restore. See
@@ -353,59 +406,6 @@ class VM
return list
end
- def set_ram_size(size, unit = "KiB")
- raise "System memory can only be added to inactive vms" if is_running?
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain/memory'].text = size
- domain_xml.elements['domain/memory'].attributes['unit'] = unit
- domain_xml.elements['domain/currentMemory'].text = size
- domain_xml.elements['domain/currentMemory'].attributes['unit'] = unit
- update(domain_xml.to_s)
- end
-
- def get_ram_size_in_bytes
- domain_xml = REXML::Document.new(@domain.xml_desc)
- unit = domain_xml.elements['domain/memory'].attribute('unit').to_s
- size = domain_xml.elements['domain/memory'].text.to_i
- return convert_to_bytes(size, unit)
- end
-
- def set_arch(arch)
- raise "System architecture can only be set to inactive vms" if is_running?
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain/os/type'].attributes['arch'] = arch
- update(domain_xml.to_s)
- end
-
- def add_hypervisor_feature(feature)
- raise "Hypervisor features can only be added to inactive vms" if is_running?
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain/features'].add_element(feature)
- update(domain_xml.to_s)
- end
-
- def drop_hypervisor_feature(feature)
- raise "Hypervisor features can only be fropped from inactive vms" if is_running?
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain/features'].delete_element(feature)
- update(domain_xml.to_s)
- end
-
- def disable_pae_workaround
- # add_hypervisor_feature("nonpae") results in a libvirt error, and
- # drop_hypervisor_feature("pae") alone won't disable pae. Hence we
- # use this workaround.
- xml = <<EOF
- <qemu:commandline xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
- <qemu:arg value='-cpu'/>
- <qemu:arg value='qemu32,-pae'/>
- </qemu:commandline>
-EOF
- domain_xml = REXML::Document.new(@domain.xml_desc)
- domain_xml.elements['domain'].add_element(REXML::Document.new(xml))
- update(domain_xml.to_s)
- end
-
def set_os_loader(type)
if is_running?
raise "boot settings can only be set for inactive vms"
@@ -431,7 +431,7 @@ EOF
def execute(cmd, options = {})
options[:user] ||= "root"
- options[:spawn] ||= false
+ options[:spawn] = false unless options.has_key?(:spawn)
if options[:libs]
libs = options[:libs]
options.delete(:libs)
@@ -442,7 +442,7 @@ EOF
cmds << cmd
cmd = cmds.join(" && ")
end
- return VMCommand.new(self, cmd, options)
+ return RemoteShell::ShellCommand.new(self, cmd, options)
end
def execute_successfully(*args)
@@ -470,7 +470,9 @@ EOF
end
def has_network?
- return execute("/sbin/ifconfig eth0 | grep -q 'inet addr'").success?
+ nmcli_info = execute('nmcli device show eth0').stdout
+ has_ipv4_addr = /^IP4.ADDRESS(\[\d+\])?:\s*([0-9.\/]+)$/.match(nmcli_info)
+ network_link_state == 'up' && has_ipv4_addr
end
def has_process?(process)
@@ -483,7 +485,7 @@ EOF
def select_virtual_desktop(desktop_number, user = LIVE_USER)
assert(desktop_number >= 0 && desktop_number <=3,
- "Only values between 0 and 3 are valid virtual desktop numbers")
+ "Only values between 0 and 1 are valid virtual desktop numbers")
execute_successfully(
"xdotool set_desktop '#{desktop_number}'",
:user => user
@@ -504,11 +506,17 @@ EOF
# Often when xdotool fails to focus a window it'll work when retried
# after redrawing the screen. Switching to a new virtual desktop then
# back seems to be a reliable way to handle this.
- select_virtual_desktop(3)
+ # Sadly we have to rely on a lot of sleep() here since there's
+ # little on the screen etc that we truly can rely on.
+ sleep 5
+ select_virtual_desktop(1)
+ sleep 5
select_virtual_desktop(0)
- sleep 5 # there aren't any visual indicators which can be used here
+ sleep 5
do_focus(window_title, user)
end
+ rescue
+ # noop
end
def file_exist?(file)
diff --git a/cucumber/features/support/hooks.rb b/cucumber/features/support/hooks.rb
index 1bb6cfd5..a55d361a 100644
--- a/cucumber/features/support/hooks.rb
+++ b/cucumber/features/support/hooks.rb
@@ -14,18 +14,23 @@ AfterConfiguration do |config|
prioritized_features = [
# Features not using snapshots but using large amounts of scratch
# space for other reasons:
- 'features/erase_memory.feature',
'features/untrusted_partitions.feature',
# Features using temporary snapshots:
'features/apt.feature',
- 'features/i2p.feature',
'features/root_access_control.feature',
'features/time_syncing.feature',
'features/tor_bridges.feature',
+ # Features using large amounts of scratch space for other reasons:
+ 'features/erase_memory.feature',
# This feature needs the almost biggest snapshot (USB install,
# excluding persistence) and will create yet another disk and
# install Tails on it. This should be the peak of disk usage.
'features/usb_install.feature',
+ # This feature needs a copy of the ISO and creates a new disk.
+ 'features/usb_upgrade.feature',
+ # This feature needs a very big snapshot (USB install with persistence)
+ # and another, network-enabled snapshot.
+ 'features/emergency_shutdown.feature',
]
feature_files = config.feature_files
# The &-intersection is specified to keep the element ordering of
@@ -127,6 +132,21 @@ def save_failure_artifact(type, path)
$failure_artifacts << [type, path]
end
+# Due to Tails' Tor enforcement, we only allow contacting hosts that
+# are Tor nodes or located on the LAN. However, when we try
+# to verify that only such hosts are contacted we have a problem --
+# we run all Tor nodes (via Chutney) *and* LAN hosts (used on some
+# tests) on the same host, the one running the test suite. Hence we
+# need to always explicitly track which nodes are LAN or not.
+#
+# Warning: when a host is added via this function, it is only added
+# for the current scenario. As such, if this is done before saving a
+# snapshot, it will not remain after the snapshot is loaded.
+def add_lan_host(ipaddr, port)
+ @lan_hosts ||= []
+ @lan_hosts << { address: ipaddr, port: port }
+end
+
BeforeFeature('@product') do |feature|
if TAILS_ISO.nil?
raise "No ISO image specified, and none could be found in the " +
@@ -159,6 +179,7 @@ BeforeFeature('@product') do |feature|
$vmstorage = VMStorage.new($virt, VM_XML_PATH)
$started_first_product_feature = true
end
+ ensure_chutney_is_running
end
AfterFeature('@product') do
@@ -169,6 +190,10 @@ AfterFeature('@product') do
end
end
end
+ $vmstorage.list_volumes.each do |vol_name|
+ next if vol_name == '__internal'
+ $vmstorage.delete_volume(vol_name)
+ end
end
# Cucumber Before hooks are executed in the order they are listed, and
@@ -198,6 +223,8 @@ Before('@product') do |scenario|
@os_loader = "MBR"
@sudo_password = "asdf"
@persistence_password = "asdf"
+ # See comment for add_lan_host() above.
+ @lan_hosts ||= []
end
# Cucumber After hooks are executed in the *reverse* order they are
@@ -224,6 +251,11 @@ After('@product') do |scenario|
info_log("Scenario failed at time #{elapsed}")
screen_capture = @screen.capture
save_failure_artifact("Screenshot", screen_capture.getFilename)
+ if scenario.exception.kind_of?(FirewallAssertionFailedError)
+ Dir.glob("#{$config["TMPDIR"]}/*.pcap").each do |pcap_file|
+ save_failure_artifact("Network capture", pcap_file)
+ end
+ end
$failure_artifacts.sort!
$failure_artifacts.each do |type, file|
artifact_name = sanitize_filename("#{elapsed}_#{scenario.name}#{File.extname(file)}")
@@ -233,7 +265,12 @@ After('@product') do |scenario|
info_log
info_log_artifact_location(type, artifact_path)
end
- pause("Scenario failed") if $config["PAUSE_ON_FAIL"]
+ if $config["INTERACTIVE_DEBUGGING"]
+ pause(
+ "Scenario failed: #{scenario.name}. " +
+ "The error was: #{scenario.exception.class.name}: #{scenario.exception}"
+ )
+ end
else
if @video_path && File.exist?(@video_path) && not($config['CAPTURE_ALL'])
FileUtils.rm(@video_path)
@@ -252,14 +289,10 @@ end
After('@product', '@check_tor_leaks') do |scenario|
@tor_leaks_sniffer.stop
if scenario.passed?
- if @bridge_hosts.nil?
- expected_tor_nodes = get_all_tor_nodes
- else
- expected_tor_nodes = @bridge_hosts
+ allowed_nodes = @bridge_hosts ? @bridge_hosts : allowed_hosts_under_tor_enforcement
+ assert_all_connections(@tor_leaks_sniffer.pcap_file) do |c|
+ allowed_nodes.include?({ address: c.daddr, port: c.dport })
end
- leaks = FirewallLeakCheck.new(@tor_leaks_sniffer.pcap_file,
- :accepted_hosts => expected_tor_nodes)
- leaks.assert_no_leaks
end
end