diff options
Diffstat (limited to 'cucumber/features/step_definitions')
25 files changed, 1261 insertions, 924 deletions
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 |