summaryrefslogtreecommitdiffstats
path: root/features
diff options
context:
space:
mode:
Diffstat (limited to 'features')
-rw-r--r--features/apt.feature8
-rw-r--r--features/domains/fs_share.xml6
-rw-r--r--features/electrum.feature2
-rw-r--r--features/icedove.feature11
-rw-r--r--features/images/ElectrumConnectServer.pngbin3929 -> 0 bytes
-rw-r--r--features/images/ElectrumCreateNewSeed.pngbin0 -> 2496 bytes
-rw-r--r--features/images/ElectrumNoWallet.pngbin3434 -> 3872 bytes
-rw-r--r--features/images/ElectrumStatus.pngbin4304 -> 997 bytes
-rw-r--r--features/images/GpgAppletDecryptVerify.pngbin2349 -> 2537 bytes
-rw-r--r--features/images/GpgAppletEncryptPassphrase.pngbin2613 -> 2773 bytes
-rw-r--r--features/images/GpgAppletKeySelected.pngbin1164 -> 1181 bytes
-rw-r--r--features/images/TailsBug11786a.pngbin0 -> 1971 bytes
-rw-r--r--features/images/TailsBug11786b.pngbin0 -> 1592 bytes
-rw-r--r--features/images/TailsHomepage.pngbin4743 -> 3678 bytes
-rw-r--r--features/images/TailsOfflineDocHomepage.pngbin4978 -> 0 bytes
-rw-r--r--features/images/TorBrowserHtml5PlayButton.pngbin535 -> 208 bytes
-rw-r--r--features/images/TorBrowserSaveOutputFileSelected.pngbin1718 -> 1647 bytes
-rw-r--r--features/mat.feature12
-rw-r--r--features/misc_files/sample.pdfbin22347 -> 0 bytes
-rw-r--r--features/misc_files/sample.pngbin0 -> 150 bytes
-rw-r--r--features/misc_files/sample.tex8
-rwxr-xr-xfeatures/scripts/vm-execute5
-rw-r--r--features/step_definitions/apt.rb24
-rw-r--r--features/step_definitions/checks.rb49
-rw-r--r--features/step_definitions/common_steps.rb97
-rw-r--r--features/step_definitions/electrum.rb11
-rw-r--r--features/step_definitions/encryption.rb8
-rw-r--r--features/step_definitions/erase_memory.rb23
-rw-r--r--features/step_definitions/firewall_leaks.rb2
-rw-r--r--features/step_definitions/icedove.rb30
-rw-r--r--features/step_definitions/pidgin.rb9
-rw-r--r--features/step_definitions/snapshots.rb9
-rw-r--r--features/step_definitions/tor.rb15
-rw-r--r--features/step_definitions/totem.rb19
-rw-r--r--features/step_definitions/usb.rb24
-rw-r--r--features/support/extra_hooks.rb11
-rw-r--r--features/support/helpers/dogtail.rb14
-rw-r--r--features/support/helpers/exec_helper.rb69
-rw-r--r--features/support/helpers/firewall_helper.rb26
-rw-r--r--features/support/helpers/misc_helpers.rb19
-rw-r--r--features/support/helpers/remote_shell.rb132
-rw-r--r--features/support/helpers/storage_helper.rb32
-rw-r--r--features/support/helpers/vm_helper.rb40
-rw-r--r--features/support/hooks.rb12
-rw-r--r--features/torified_browsing.feature2
-rw-r--r--features/totem.feature19
-rw-r--r--features/usb_upgrade.feature17
47 files changed, 454 insertions, 311 deletions
diff --git a/features/apt.feature b/features/apt.feature
index 7a1f0ec..c835d27 100644
--- a/features/apt.feature
+++ b/features/apt.feature
@@ -9,15 +9,17 @@ Feature: Installing packages through APT
Given I have started Tails from DVD and logged in with an administration password and the network is connected
Scenario: APT sources are configured correctly
- Then the only hosts in APT sources are "ftp.us.debian.org,security.debian.org,deb.tails.boum.org,deb.torproject.org"
+ Then the only hosts in APT sources are "vwakviie2ienjx6t.onion,sgvtcaew4bxjd7ln.onion,jenw7xbd6tf7vfhp.onion,sdscoq7snqtznauu.onion"
@check_tor_leaks
Scenario: Install packages using apt
- When I update APT using apt
+ When I configure APT to use non-onion sources
+ And I update APT using apt
Then I should be able to install a package using apt
@check_tor_leaks
Scenario: Install packages using Synaptic
- When I start Synaptic
+ When I configure APT to use non-onion sources
+ And I start Synaptic
And I update APT using Synaptic
Then I should be able to install a package using Synaptic
diff --git a/features/domains/fs_share.xml b/features/domains/fs_share.xml
deleted file mode 100644
index 718755e..0000000
--- a/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/features/electrum.feature b/features/electrum.feature
index 9beb2e8..b611d23 100644
--- a/features/electrum.feature
+++ b/features/electrum.feature
@@ -21,7 +21,7 @@ Feature: Electrum Bitcoin client
Then persistence for "electrum" is enabled
When I start Electrum through the GNOME menu
But a bitcoin wallet is not present
- Then I am prompted to create a new wallet
+ Then I am prompted to configure Electrum
When I create a new bitcoin wallet
Then a bitcoin wallet is present
And I see the main Electrum client window
diff --git a/features/icedove.feature b/features/icedove.feature
index 59dab9c..298be78 100644
--- a/features/icedove.feature
+++ b/features/icedove.feature
@@ -29,11 +29,15 @@ Feature: Icedove email client
Given I cancel setting up an email account
Then I see that Torbirdy is configured to use Tor
+ #11890
+ @fragile
Scenario: Icedove's autoconfiguration wizard defaults to IMAP and secure protocols
When I enter my email credentials into the autoconfiguration wizard
Then the autoconfiguration wizard's choice for the incoming server is secure IMAP
Then the autoconfiguration wizard's choice for the outgoing server is secure SMTP
+ #11890
+ @fragile
Scenario: Icedove can send emails, and receive emails over IMAP
When I enter my email credentials into the autoconfiguration wizard
Then the autoconfiguration wizard's choice for the incoming server is secure IMAP
@@ -42,12 +46,13 @@ Feature: Icedove email client
And I fetch my email
Then I can find the email I sent to myself in my inbox
- Scenario: Icedove can send emails, and receive emails over POP3
+ #11890
+ @fragile
+ Scenario: Icedove can download the inbox with POP3
When I enter my email credentials into the autoconfiguration wizard
Then the autoconfiguration wizard's choice for the incoming server is secure IMAP
When I select the autoconfiguration wizard's POP3 choice
Then the autoconfiguration wizard's choice for the incoming server is secure POP3
When I accept the autoconfiguration wizard's configuration
- And I send an email to myself
And I fetch my email
- Then I can find the email I sent to myself in my inbox
+ Then my Icedove inbox is non-empty
diff --git a/features/images/ElectrumConnectServer.png b/features/images/ElectrumConnectServer.png
deleted file mode 100644
index 9e587ed..0000000
--- a/features/images/ElectrumConnectServer.png
+++ /dev/null
Binary files differ
diff --git a/features/images/ElectrumCreateNewSeed.png b/features/images/ElectrumCreateNewSeed.png
new file mode 100644
index 0000000..9d56724
--- /dev/null
+++ b/features/images/ElectrumCreateNewSeed.png
Binary files differ
diff --git a/features/images/ElectrumNoWallet.png b/features/images/ElectrumNoWallet.png
index a6213e6..784ec0d 100644
--- a/features/images/ElectrumNoWallet.png
+++ b/features/images/ElectrumNoWallet.png
Binary files differ
diff --git a/features/images/ElectrumStatus.png b/features/images/ElectrumStatus.png
index 769a2fe..78a0ce0 100644
--- a/features/images/ElectrumStatus.png
+++ b/features/images/ElectrumStatus.png
Binary files differ
diff --git a/features/images/GpgAppletDecryptVerify.png b/features/images/GpgAppletDecryptVerify.png
index 3879f6b..c584792 100644
--- a/features/images/GpgAppletDecryptVerify.png
+++ b/features/images/GpgAppletDecryptVerify.png
Binary files differ
diff --git a/features/images/GpgAppletEncryptPassphrase.png b/features/images/GpgAppletEncryptPassphrase.png
index b2a9f99..6f07a29 100644
--- a/features/images/GpgAppletEncryptPassphrase.png
+++ b/features/images/GpgAppletEncryptPassphrase.png
Binary files differ
diff --git a/features/images/GpgAppletKeySelected.png b/features/images/GpgAppletKeySelected.png
index b67cdf9..761a832 100644
--- a/features/images/GpgAppletKeySelected.png
+++ b/features/images/GpgAppletKeySelected.png
Binary files differ
diff --git a/features/images/TailsBug11786a.png b/features/images/TailsBug11786a.png
new file mode 100644
index 0000000..2e6428b
--- /dev/null
+++ b/features/images/TailsBug11786a.png
Binary files differ
diff --git a/features/images/TailsBug11786b.png b/features/images/TailsBug11786b.png
new file mode 100644
index 0000000..49ad928
--- /dev/null
+++ b/features/images/TailsBug11786b.png
Binary files differ
diff --git a/features/images/TailsHomepage.png b/features/images/TailsHomepage.png
index e21c0f7..66ded74 100644
--- a/features/images/TailsHomepage.png
+++ b/features/images/TailsHomepage.png
Binary files differ
diff --git a/features/images/TailsOfflineDocHomepage.png b/features/images/TailsOfflineDocHomepage.png
deleted file mode 100644
index 95d5ea6..0000000
--- a/features/images/TailsOfflineDocHomepage.png
+++ /dev/null
Binary files differ
diff --git a/features/images/TorBrowserHtml5PlayButton.png b/features/images/TorBrowserHtml5PlayButton.png
index 11d0eaf..f15e261 100644
--- a/features/images/TorBrowserHtml5PlayButton.png
+++ b/features/images/TorBrowserHtml5PlayButton.png
Binary files differ
diff --git a/features/images/TorBrowserSaveOutputFileSelected.png b/features/images/TorBrowserSaveOutputFileSelected.png
index cda7a09..8de38a9 100644
--- a/features/images/TorBrowserSaveOutputFileSelected.png
+++ b/features/images/TorBrowserSaveOutputFileSelected.png
Binary files differ
diff --git a/features/mat.feature b/features/mat.feature
index 506c44f..1b6345f 100644
--- a/features/mat.feature
+++ b/features/mat.feature
@@ -1,14 +1,10 @@
-#11901: mat does not clean PDF files anymore
-@product @fragile
+@product
Feature: Metadata Anonymization Toolkit
As a Tails user
I want to be able to remove leaky metadata from documents and media files
- # In this feature we cannot restore from snapshots since it's
- # incompatible with filesystem shares.
-
- Scenario: MAT can clean a PDF file
+ Scenario: MAT can clean a PNG file
Given a computer
- And I setup a filesystem share containing a sample PDF
And I start Tails from DVD with network unplugged and I login
- Then MAT can clean some sample PDF file
+ And I plug and mount a USB drive containing a sample PNG
+ Then MAT can clean some sample PNG file
diff --git a/features/misc_files/sample.pdf b/features/misc_files/sample.pdf
deleted file mode 100644
index d0cc950..0000000
--- a/features/misc_files/sample.pdf
+++ /dev/null
Binary files differ
diff --git a/features/misc_files/sample.png b/features/misc_files/sample.png
new file mode 100644
index 0000000..facf4269
--- /dev/null
+++ b/features/misc_files/sample.png
Binary files differ
diff --git a/features/misc_files/sample.tex b/features/misc_files/sample.tex
deleted file mode 100644
index 043faae..0000000
--- a/features/misc_files/sample.tex
+++ /dev/null
@@ -1,8 +0,0 @@
-\documentclass[12pt]{article}
-\title{Sample PDF document}
-\author{Tails developers}
-\date{March 12, 2013}
-\begin{document}
-\maketitle
-Does this PDF still have metadata?
-\end{document}
diff --git a/features/scripts/vm-execute b/features/scripts/vm-execute
index fc1bf45..6486a2e 100755
--- a/features/scripts/vm-execute
+++ b/features/scripts/vm-execute
@@ -2,7 +2,8 @@
require 'optparse'
begin
- require "#{`git rev-parse --show-toplevel`.chomp}/features/support/helpers/exec_helper.rb"
+ require "#{`git rev-parse --show-toplevel`.chomp}/features/support/helpers/remote_shell.rb"
+ require "#{`git rev-parse --show-toplevel`.chomp}/features/support/helpers/misc_helpers.rb"
rescue LoadError => e
raise "This script must be run from within Tails' Git directory."
end
@@ -45,7 +46,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::Command.new(FakeVM.new, cmd, cmd_opts)
puts "Return status: #{c.returncode}"
puts "STDOUT:\n#{c.stdout}"
puts "STDERR:\n#{c.stderr}"
diff --git a/features/step_definitions/apt.rb b/features/step_definitions/apt.rb
index 5163167..87b30c09 100644
--- a/features/step_definitions/apt.rb
+++ b/features/step_definitions/apt.rb
@@ -2,13 +2,33 @@ 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
+
+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
diff --git a/features/step_definitions/checks.rb b/features/step_definitions/checks.rb
index 507a61b..d61c89f 100644
--- a/features/step_definitions/checks.rb
+++ b/features/step_definitions/checks.rb
@@ -128,17 +128,6 @@ Then /^the VirtualBox guest modules are available$/ do
"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
if @language == 'German'
expected_title = 'Tails - Hilfe & Support'
@@ -156,24 +145,36 @@ Then /^the support documentation page opens in Tor Browser$/ do
)
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}'",
+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
diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb
index 722710d..a5af7b1 100644
--- a/features/step_definitions/common_steps.rb
+++ b/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,17 +23,6 @@ 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)
@@ -92,9 +63,6 @@ def post_snapshot_restore_hook
$vm.wait_until_remote_shell_is_up
post_vm_start_hook
- # XXX-9p: See XXX-9p above
- #activate_filesystem_shares
-
# 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.
# Time jumps and incorrect clocks also confuses Tor in many ways.
@@ -136,7 +104,7 @@ Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name|
$vm.set_disk_boot(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
@@ -343,7 +311,6 @@ Given /^the computer (re)?boots Tails$/ do |reboot|
Sikuli::Key.ENTER)
@screen.wait('TailsGreeter.png', 5*60)
$vm.wait_until_remote_shell_is_up
- activate_filesystem_shares
step 'I configure Tails to use a simulated Tor network'
end
@@ -462,7 +429,7 @@ end
Given /^the Tor Browser (?:has started and )?load(?:ed|s) the (startup page|Tails roadmap)$/ do |page|
case page
when "startup page"
- title = 'Tails - Dear Tails user'
+ title = 'Tails - News'
when "Tails roadmap"
title = 'Roadmap - Tails - RiseupLabs Code Repository'
else
@@ -575,16 +542,31 @@ Given /^I kill the process "([^"]+)"$/ do |process|
}
end
-Then /^Tails eventually shuts down$/ do
- try_for(memory_wipe_timeout, :msg => "VM is still running") do
- ! $vm.is_running?
+Then /^Tails eventually (shuts down|restarts)$/ do |mode|
+ nr_gibs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
+ timeout = nr_gibs_of_ram*5*60
+ # Work around Tails bug #11786, where something goes wrong when we
+ # kexec to the new kernel for memory wiping and gets dropped to a
+ # BusyBox shell instead.
+ try_for(timeout) do
+ if @screen.existsAny(['TailsBug11786a.png', 'TailsBug11786b.png'])
+ puts "We were hit by bug #11786: memory wiping got stuck"
+ if mode == 'restarts'
+ $vm.reset
+ else
+ $vm.power_off
+ end
+ else
+ if mode == 'restarts'
+ @screen.find('TailsGreeter.png')
+ true
+ else
+ ! $vm.is_running?
+ end
+ end
end
end
-Then /^Tails eventually restarts$/ do
- step 'the computer reboots Tails'
-end
-
Given /^I shutdown Tails and wait for the computer to power off$/ do
$vm.spawn("poweroff")
step 'Tails eventually shuts down'
@@ -656,11 +638,10 @@ method=auto
[ipv4]
method=auto
EOF
- con_content.split("\n").each do |line|
- $vm.execute("echo '#{line}' >> /tmp/NM.#{con_name}")
- end
+ tmp_path = "/tmp/NM.#{con_name}"
+ $vm.file_overwrite(tmp_path, con_content)
con_file = "/etc/NetworkManager/system-connections/#{con_name}"
- $vm.execute("install -m 0600 '/tmp/NM.#{con_name}' '#{con_file}'")
+ $vm.execute("install -m 0600 '#{tmp_path}' '#{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
@@ -993,3 +974,27 @@ Then /^Tails is running version (.+)$/ do |version|
.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
diff --git a/features/step_definitions/electrum.rb b/features/step_definitions/electrum.rb
index 4c02d82..712b3f0 100644
--- a/features/step_definitions/electrum.rb
+++ b/features/step_definitions/electrum.rb
@@ -17,6 +17,8 @@ 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
@@ -27,11 +29,10 @@ When /^I create a new bitcoin wallet$/ do
@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
@@ -40,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/features/step_definitions/encryption.rb b/features/step_definitions/encryption.rb
index 68f620e..4e5ca02 100644
--- a/features/step_definitions/encryption.rb
+++ b/features/step_definitions/encryption.rb
@@ -23,10 +23,10 @@ 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
diff --git a/features/step_definitions/erase_memory.rb b/features/step_definitions/erase_memory.rb
index 21ea7bd..3625360 100644
--- a/features/step_definitions/erase_memory.rb
+++ b/features/step_definitions/erase_memory.rb
@@ -205,10 +205,23 @@ end
When /^I shutdown and wait for Tails to finish wiping the memory$/ do
$vm.spawn("halt")
- try_for(memory_wipe_timeout, { :msg => "memory wipe didn't finish, probably the VM crashed" }) do
- # We spam keypresses to prevent console blanking from hiding the
- # image we're waiting for
- @screen.type(" ")
- @screen.find('MemoryWipeCompleted.png')
+ match = nil
+ begin
+ try_for(memory_wipe_timeout, msg: "memory wipe didn't finish, probably the VM crashed") do
+ # We spam keypresses to prevent console blanking from hiding the
+ # image we're waiting for
+ @screen.type(" ")
+ match, _ = @screen.findAny(
+ ['MemoryWipeCompleted.png', 'TailsBug11786a.png', 'TailsBug11786b.png']
+ )
+ match != nil
+ end
+ # Just throw the same exception as a if the try_for would fail
+ raise Timeout::Error if match != 'MemoryWipeCompleted.png'
+ rescue Timeout::Error
+ puts "Cannot tell if memory wipe completed. " +
+ "One possible reason for this is #11786, " +
+ "so let's go on and rely on the next steps to check " +
+ "how well memory was wiped."
end
end
diff --git a/features/step_definitions/firewall_leaks.rb b/features/step_definitions/firewall_leaks.rb
index ca814a6..0cd94cc 100644
--- a/features/step_definitions/firewall_leaks.rb
+++ b/features/step_definitions/firewall_leaks.rb
@@ -1,5 +1,5 @@
Then(/^the firewall leak detector has detected leaks$/) do
- assert_raise(Test::Unit::AssertionFailedError) do
+ assert_raise(FirewallAssertionFailedError) do
step 'all Internet traffic has only flowed through Tor'
end
end
diff --git a/features/step_definitions/icedove.rb b/features/step_definitions/icedove.rb
index 7e94716..07625c0 100644
--- a/features/step_definitions/icedove.rb
+++ b/features/step_definitions/icedove.rb
@@ -13,6 +13,14 @@ def icedove_wizard
icedove_app.child('Mail Account Setup', roleName: 'frame')
end
+def icedove_inbox
+ folder_view = icedove_main.child($config['Icedove']['address'],
+ roleName: 'table row').parent
+ folder_view.children(roleName: 'table row', recursive: false).find do |e|
+ e.name.match(/^Inbox( .*)?$/)
+ end
+end
+
When /^I start Icedove$/ do
workaround_pref_lines = [
# When we generate a random subject line it may contain one of the
@@ -208,12 +216,7 @@ end
Then /^I can find the email I sent to myself in my inbox$/ do
recovery_proc = Proc.new { step 'I fetch my email' }
retry_tor(recovery_proc) do
- folder_view = icedove_main.child($config['Icedove']['address'],
- roleName: 'table row').parent
- inbox = folder_view.children(roleName: 'table row', recursive: false).find do |e|
- e.name.match(/^Inbox( .*)?$/)
- end
- inbox.click
+ icedove_inbox.click
filter = icedove_main.child('Filter these messages <Ctrl+Shift+K>',
roleName: 'entry')
filter.typeText(@subject)
@@ -232,3 +235,18 @@ Then /^I can find the email I sent to myself in my inbox$/ do
inbox_view.button('Delete').click
end
end
+
+Then /^my Icedove inbox is non-empty$/ do
+ icedove_inbox.click
+ # The button is located on the first row in the message list, the
+ # one that shows the column labels (Subject, From, ...).
+ message_list = icedove_main.child('Select columns to display',
+ roleName: 'push button')
+ .parent.parent
+ visible_messages = message_list.children(recursive: false,
+ roleName: 'table row')
+ # The first element is the column label row, which is not a message,
+ # so let's remove it.
+ visible_messages.shift
+ assert(visible_messages.size > 0)
+end
diff --git a/features/step_definitions/pidgin.rb b/features/step_definitions/pidgin.rb
index 74fe151..b28632c 100644
--- a/features/step_definitions/pidgin.rb
+++ b/features/step_definitions/pidgin.rb
@@ -35,7 +35,7 @@ def focus_pidgin_irc_conversation_window(account)
# for a message from InfoServ first then default to looking for '#i2p'
try_for(20) do
begin
- $vm.focus_window('InfoServ')
+ $vm.focus_window('irc.echelon.i2p')
rescue ExecutionFailedInVM
$vm.focus_window('#i2p')
end
@@ -204,8 +204,9 @@ 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("@")
@@ -264,7 +265,7 @@ def default_chan (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
diff --git a/features/step_definitions/snapshots.rb b/features/step_definitions/snapshots.rb
index 8675915..7199464 100644
--- a/features/step_definitions/snapshots.rb
+++ b/features/step_definitions/snapshots.rb
@@ -169,12 +169,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|
@@ -183,10 +183,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/features/step_definitions/tor.rb b/features/step_definitions/tor.rb
index ff78d81..73b3abb 100644
--- a/features/step_definitions/tor.rb
+++ b/features/step_definitions/tor.rb
@@ -256,7 +256,8 @@ def stream_isolation_info(application)
when "Tor Browser"
{
:grep_monitor_expr => '/firefox\>',
- :socksport => 9150
+ :socksport => 9150,
+ :controller => true,
}
when "Gobby"
{
@@ -288,17 +289,19 @@ When /^I monitor the network connections of (.*)$/ do |application|
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+/)[4]
+ 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
diff --git a/features/step_definitions/totem.rb b/features/step_definitions/totem.rb
index 520e7d6..a5b88d1 100644
--- a/features/step_definitions/totem.rb
+++ b/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/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
diff --git a/features/step_definitions/usb.rb b/features/step_definitions/usb.rb
index ce46916..55e2df8 100644
--- a/features/step_definitions/usb.rb
+++ b/features/step_definitions/usb.rb
@@ -78,7 +78,10 @@ end
def usb_install_helper(name)
@screen.wait('USBTailsLogo.png', 10)
- if @screen.exists("USBCannotUpgrade.png")
+ text = Dogtail::Application.new('tails-installer')
+ .child('', roleName: 'text').text
+ dev = $vm.disk_dev(name)
+ if text.match(/It is impossible to upgrade the device .+ #{dev}\d* /)
raise UpgradeNotSupported
end
begin
@@ -87,7 +90,8 @@ def usb_install_helper(name)
@screen.wait_and_click('USBCreateLiveUSBConfirmYes.png', 10)
@screen.wait('USBInstallationComplete.png', 30*60)
rescue FindFailed => e
- debug_log("Tails Installer debug log:\n" + $vm.file_content('/tmp/tails-installer-*'))
+ 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
@@ -153,13 +157,9 @@ 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|
@@ -171,8 +171,7 @@ When /^I do a "Upgrade from ISO" on USB drive "([^"]+)"$/ do |name|
@screen.wait_and_click('GnomeFileDiagHome.png', 10)
@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.type(@iso_path)
@screen.wait_and_click('GnomeFileDiagOpenButton.png', 10)
usb_install_helper(name)
end
@@ -276,10 +275,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
diff --git a/features/support/extra_hooks.rb b/features/support/extra_hooks.rb
index b990b9b..c2c5749 100644
--- a/features/support/extra_hooks.rb
+++ b/features/support/extra_hooks.rb
@@ -77,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'
diff --git a/features/support/helpers/dogtail.rb b/features/support/helpers/dogtail.rb
index 59fac3c..2d81205 100644
--- a/features/support/helpers/dogtail.rb
+++ b/features/support/helpers/dogtail.rb
@@ -85,15 +85,19 @@ module Dogtail
lines = [lines] if lines.class != Array
script = build_script(lines)
script_path = $vm.execute_successfully('mktemp', @opts).stdout.chomp
- $vm.file_overwrite(script_path, script, @opts[:user])
+ $vm.file_overwrite(script_path, script)
args = ["/usr/bin/python '#{script_path}'", @opts]
if @opts[:allow_failure]
- ret = $vm.execute(*args)
+ return $vm.execute(*args)
else
- ret = $vm.execute_successfully(*args)
+ begin
+ return $vm.execute_successfully(*args)
+ rescue Exception => e
+ debug_log("Failing Dogtail script (#{script_path}):")
+ script.split("\n").each { |line| debug_log(" "*4 + line) }
+ raise e
+ end
end
- $vm.execute("rm -f '#{script_path}'")
- ret
end
def self.value_to_s(v)
diff --git a/features/support/helpers/exec_helper.rb b/features/support/helpers/exec_helper.rb
deleted file mode 100644
index 6f221de..0000000
--- a/features/support/helpers/exec_helper.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'json'
-require 'socket'
-
-class VMCommand
-
- @@request_id ||= 0
-
- 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 = 90)
- try_for(timeout, :msg => "Remote shell seems to be down") do
- Timeout::timeout(3) do
- VMCommand.execute(vm, "echo 'hello?'")
- end
- end
- end
-
- # 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"
- id = (@@request_id += 1)
- socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
- debug_log("#{type}ing as #{options[:user]}: #{cmd}")
- socket.puts(JSON.dump([id, type, options[:user], cmd]))
- loop do
- s = socket.readline(sep = "\0").chomp("\0")
- response_id, *rest = JSON.load(s)
- if response_id == id
- debug_log("#{type} returned: #{s}") if not(options[:spawn])
- return rest
- else
- debug_log("Dropped out-of-order remote shell response: " +
- "got id #{response_id} but expected id #{id}")
- end
- end
- ensure
- socket.close if defined?(socket) && socket
- 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/features/support/helpers/firewall_helper.rb b/features/support/helpers/firewall_helper.rb
index ed2a09b..49e9853 100644
--- a/features/support/helpers/firewall_helper.rb
+++ b/features/support/helpers/firewall_helper.rb
@@ -3,7 +3,7 @@ require 'packetfu'
# 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
+ 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|
@@ -45,28 +45,40 @@ def pcap_connections_helper(pcap_file, opts = {})
next if opts[:ignore_dhcp]
end
- connections << {
+ packet_info = {
mac_saddr: eth_packet.eth_saddr,
mac_daddr: eth_packet.eth_daddr,
protocol: protocol,
- saddr: ip_packet.ip_saddr,
- daddr: ip_packet.ip_daddr,
sport: sport,
dport: dport,
}
+ # It seems *Packet.parse can return nil despite *Packet.can_parse?
+ # returning true.
+ if ip_packet
+ packet_info[:saddr] = ip_packet.ip_saddr
+ packet_info[:daddr] = ip_packet.ip_daddr
+ else
+ puts "We were hit by #11508. PacketFu bug? Packet info: #{packet_info}"
+ end
+ connections << packet_info
end
connections.uniq.map { |p| OpenStruct.new(p) }
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
- save_failure_artifact("Network capture", pcap_file) unless bad.empty?
- assert(bad.empty?, "Unexpected connections were made:\n" +
- bad.map { |e| " #{e}" } .join("\n"))
+ 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)
diff --git a/features/support/helpers/misc_helpers.rb b/features/support/helpers/misc_helpers.rb
index db907e5..ed9c54e 100644
--- a/features/support/helpers/misc_helpers.rb
+++ b/features/support/helpers/misc_helpers.rb
@@ -30,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
@@ -78,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
@@ -266,9 +271,19 @@ 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
loop do
STDERR.puts "Return: Continue; d: Debugging REPL"
diff --git a/features/support/helpers/remote_shell.rb b/features/support/helpers/remote_shell.rb
new file mode 100644
index 0000000..b2465a3
--- /dev/null
+++ b/features/support/helpers/remote_shell.rb
@@ -0,0 +1,132 @@
+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 Command
+ # 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, 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
+
+ # 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, 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/features/support/helpers/storage_helper.rb b/features/support/helpers/storage_helper.rb
index 0e452ed..9cf0db2 100644
--- a/features/support/helpers/storage_helper.rb
+++ b/features/support/helpers/storage_helper.rb
@@ -144,13 +144,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 +152,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 +170,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 +188,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/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb
index 04cf7bc..3029acf 100644
--- a/features/support/helpers/vm_helper.rb
+++ b/features/support/helpers/vm_helper.rb
@@ -452,7 +452,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)
@@ -463,7 +463,7 @@ EOF
cmds << cmd
cmd = cmds.join(" && ")
end
- return VMCommand.new(self, cmd, options)
+ return RemoteShell::Command.new(self, cmd, options)
end
def execute_successfully(*args)
@@ -482,7 +482,12 @@ EOF
end
def wait_until_remote_shell_is_up(timeout = 90)
- VMCommand.wait_until_remote_shell_is_up(self, timeout)
+ msg = 'hello?'
+ try_for(timeout, :msg => "Remote shell seems to be down") do
+ Timeout::timeout(3) do
+ execute_successfully("echo '#{msg}'").stdout.chomp == msg
+ end
+ end
end
def host_to_guest_time_sync
@@ -540,27 +545,24 @@ EOF
execute("test -d '#{directory}'").success?
end
- def file_content(file, user = 'root')
- # We don't quote #{file} on purpose: we sometimes pass environment variables
- # or globs that we want to be interpreted by the shell.
- cmd = execute("cat #{file}", :user => user)
- assert(cmd.success?,
- "Could not cat '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}")
- return cmd.stdout
+ def file_open(path)
+ f = RemoteShell::File.new(self, path)
+ yield f if block_given?
+ return f
end
- def file_append(file, lines, user = 'root')
+ def file_content(path)
+ file_open(path) { |f| return f.read() }
+ end
+
+ def file_overwrite(path, lines)
lines = lines.join("\n") if lines.class == Array
- # Use some tricky quoting to allow any character to be appended
- lines.gsub!("'", "'\"'\"'")
- cmd = execute("echo '#{lines}' >> '#{file}'", :user => user)
- assert(cmd.success?,
- "Could not append to '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}")
+ file_open(path) { |f| return f.write(lines) }
end
- def file_overwrite(*args)
- execute_successfully("rm -f '#{args.first}'")
- file_append(*args)
+ def file_append(path, lines)
+ lines = lines.join("\n") if lines.class == Array
+ file_open(path) { |f| return f.append(lines) }
end
def set_clipboard(text)
diff --git a/features/support/hooks.rb b/features/support/hooks.rb
index 6693120..35e9f2d 100644
--- a/features/support/hooks.rb
+++ b/features/support/hooks.rb
@@ -244,6 +244,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)}")
@@ -253,7 +258,12 @@ After('@product') do |scenario|
info_log
info_log_artifact_location(type, artifact_path)
end
- pause("Scenario failed") if $config["INTERACTIVE_DEBUGGING"]
+ 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)
diff --git a/features/torified_browsing.feature b/features/torified_browsing.feature
index 39e1dbf..878340f 100644
--- a/features/torified_browsing.feature
+++ b/features/torified_browsing.feature
@@ -106,7 +106,7 @@ Feature: Browsing the web using the Tor Browser
Given I have started Tails from DVD and logged in and the network is connected
When I double-click on the "Tails documentation" link on the Desktop
Then the Tor Browser has started
- And I see "TailsOfflineDocHomepage.png" after at most 10 seconds
+ And "Tails - Getting started..." has loaded in the Tor Browser
Scenario: The Tor Browser uses TBB's shared libraries
Given I have started Tails from DVD and logged in and the network is connected
diff --git a/features/totem.feature b/features/totem.feature
index d8d9054..2944739 100644
--- a/features/totem.feature
+++ b/features/totem.feature
@@ -9,9 +9,8 @@ Feature: Using Totem
Given I create sample videos
Scenario: Watching a MP4 video stored on the non-persistent filesystem
- Given a computer
- And I setup a filesystem share containing sample videos
- And I start Tails from DVD with network unplugged and I login
+ Given I have started Tails from DVD without network and logged in
+ And I plug and mount a USB drive containing sample videos
And I copy the sample videos to "/home/amnesia" as user "amnesia"
And the file "/home/amnesia/video.mp4" exists
Given I start monitoring the AppArmor log of "/usr/bin/totem"
@@ -52,22 +51,14 @@ Feature: Using Totem
Then I can watch a WebM video over HTTPs
Scenario: Watching MP4 videos stored on the persistent volume should work as expected given our AppArmor confinement
- Given I have started Tails without network from a USB drive with a persistent partition and stopped at Tails Greeter's login screen
- # Due to bug #5571 we have to reboot to be able to use
- # filesystem shares.
- And I shutdown Tails and wait for the computer to power off
- And I setup a filesystem share containing sample videos
- And I start Tails from USB drive "__internal" with network unplugged and I login with persistence enabled
+ Given I have started Tails without network from a USB drive with a persistent partition enabled and logged in
+ And I plug and mount a USB drive containing sample videos
And I copy the sample videos to "/home/amnesia/Persistent" as user "amnesia"
- And I copy the sample videos to "/home/amnesia/.gnupg" as user "amnesia"
- And I shutdown Tails and wait for the computer to power off
- And I start Tails from USB drive "__internal" with network unplugged and I login with persistence enabled
- And the file "/home/amnesia/Persistent/video.mp4" exists
When I open "/home/amnesia/Persistent/video.mp4" with Totem
Then I see "SampleLocalMp4VideoFrame.png" after at most 40 seconds
Given I close Totem
- And the file "/home/amnesia/.gnupg/video.mp4" exists
And I start monitoring the AppArmor log of "/usr/bin/totem"
+ And I copy the sample videos to "/home/amnesia/.gnupg" as user "amnesia"
When I try to open "/home/amnesia/.gnupg/video.mp4" with Totem
Then I see "TotemUnableToOpen.png" after at most 10 seconds
And AppArmor has denied "/usr/bin/totem" from opening "/home/amnesia/.gnupg/video.mp4"
diff --git a/features/usb_upgrade.feature b/features/usb_upgrade.feature
index 0aa9a95..33cc0da 100644
--- a/features/usb_upgrade.feature
+++ b/features/usb_upgrade.feature
@@ -11,9 +11,8 @@ Feature: Upgrading an old Tails USB installation
# dependencies (which are documented below).
Scenario: Try to "Upgrade from ISO" Tails to a pristine USB drive
- Given a computer
- And I setup a filesystem share containing the Tails ISO
- And I start Tails from DVD with network unplugged and I login
+ Given I have started Tails from DVD without network and logged in
+ And I plug and mount a USB drive containing the Tails ISO
And I temporarily create a 4 GiB disk named "pristine"
And I plug USB drive "pristine"
And I start Tails Installer in "Upgrade from ISO" mode
@@ -29,9 +28,8 @@ Feature: Upgrading an old Tails USB installation
And I am told that the destination device cannot be upgraded
Scenario: Try to "Upgrade from ISO" Tails to a USB drive with GPT and a FAT partition
- Given a computer
- And I setup a filesystem share containing the Tails ISO
- And I start Tails from DVD with network unplugged and I login
+ Given I have started Tails from DVD without network and logged in
+ And I plug and mount a USB drive containing the Tails ISO
And I temporarily create a 4 GiB disk named "gptfat"
And I create a gpt partition with a vfat filesystem on disk "gptfat"
And I plug USB drive "gptfat"
@@ -132,8 +130,8 @@ Feature: Upgrading an old Tails USB installation
Scenario: Upgrading an old Tails USB installation from an ISO image, running on the old version
Given a computer
And I clone USB drive "old" to a new USB drive "to_upgrade"
- And I setup a filesystem share containing the Tails ISO
When I start Tails from USB drive "old" with network unplugged and I login
+ And I plug and mount a USB drive containing the Tails ISO
And I plug USB drive "to_upgrade"
And I do a "Upgrade from ISO" on USB drive "to_upgrade"
Then the ISO's Tails is installed on USB drive "to_upgrade"
@@ -141,10 +139,9 @@ Feature: Upgrading an old Tails USB installation
# Depends on scenario: Writing files to a read/write-enabled persistent partition with the old Tails USB installation
Scenario: Upgrading an old Tails USB installation from an ISO image, running on the new version
- Given a computer
+ Given I have started Tails from DVD without network and logged in
+ And I plug and mount a USB drive containing the Tails ISO
And I clone USB drive "old" to a new USB drive "to_upgrade"
- And I setup a filesystem share containing the Tails ISO
- And I start Tails from DVD with network unplugged and I login
And I plug USB drive "to_upgrade"
And I do a "Upgrade from ISO" on USB drive "to_upgrade"
Then the ISO's Tails is installed on USB drive "to_upgrade"