4 # Sometimes the first click is lost (presumably it's used to give
5 # focus to virt-viewer or similar) so we do that now rather than
6 # having an important click lost. The point we click should be
7 # somewhere where no clickable elements generally reside.
8 @screen.click_point(@screen.w - 1, @screen.h/2)
11 def context_menu_helper(top, bottom, menu_item)
13 t = @screen.wait(top, 10)
14 b = @screen.wait(bottom, 10)
15 # In Sikuli, lower x == closer to the left, lower y == closer to the top
17 center = Sikuli::Location.new(((t.x + t.w) + b.x)/2,
18 ((t.y + t.h) + b.y)/2)
19 @screen.right_click(center)
21 @screen.wait_and_click(menu_item, 10)
26 def post_snapshot_restore_hook
27 $vm.wait_until_remote_shell_is_up
30 # The guest's Tor's circuits' states are likely to get out of sync
31 # with the other relays, so we ensure that we have fresh circuits.
32 # Time jumps and incorrect clocks also confuses Tor in many ways.
34 if $vm.execute("systemctl --quiet is-active tor@default.service").success?
35 $vm.execute("systemctl stop tor@default.service")
36 $vm.execute("systemctl --no-block restart tails-tor-has-bootstrapped.target")
37 $vm.host_to_guest_time_sync
38 $vm.execute("systemctl start tor@default.service")
39 wait_until_tor_is_working
42 $vm.host_to_guest_time_sync
46 Given /^a computer$/ do
47 $vm.destroy_and_undefine if $vm
48 $vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
51 Given /^the computer is set to boot from the Tails DVD$/ do
52 $vm.set_cdrom_boot(TAILS_ISO)
55 Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name|
56 $vm.set_disk_boot(name, type.downcase)
59 Given /^I (temporarily )?create an? (\d+) ([[:alpha:]]+) (?:([[:alpha:]]+) )?disk named "([^"]+)"$/ do |temporary, size, unit, type, name|
61 $vm.storage.create_new_disk(name, {:size => size, :unit => unit,
63 add_after_scenario_hook { $vm.storage.delete_volume(name) } if temporary
66 Given /^I plug (.+) drive "([^"]+)"$/ do |bus, name|
67 $vm.plug_drive(name, bus.downcase)
70 step "drive \"#{name}\" is detected by Tails"
74 Then /^drive "([^"]+)" is detected by Tails$/ do |name|
75 raise "Tails is not running" unless $vm.is_running?
76 try_for(20, :msg => "Drive '#{name}' is not detected by Tails") do
77 $vm.disk_detected?(name)
81 Given /^the network is plugged$/ do
85 Given /^the network is unplugged$/ do
89 Given /^the network connection is ready(?: within (\d+) seconds)?$/ do |timeout|
91 try_for(timeout.to_i) { $vm.has_network? }
94 Given /^the hardware clock is set to "([^"]*)"$/ do |time|
95 $vm.set_hardware_clock(DateTime.parse(time).to_time)
98 Given /^I capture all network traffic$/ do
99 @sniffer = Sniffer.new("sniffer", $vmnet)
101 add_after_scenario_hook do
107 Given /^I set Tails to boot with options "([^"]*)"$/ do |options|
108 @boot_options = options
111 When /^I start the computer$/ do
112 assert(!$vm.is_running?,
113 "Trying to start a VM that is already running")
118 Given /^I start Tails( from DVD)?( with network unplugged)?( and I login)?$/ do |dvd_boot, network_unplugged, do_login|
119 step "the computer is set to boot from the Tails DVD" if dvd_boot
121 step "the network is unplugged"
123 step "the network is plugged"
125 step "I start the computer"
126 step "the computer boots Tails"
128 step "I log in to a new session"
130 step "all notifications have disappeared"
133 step "all notifications have disappeared"
134 step "available upgrades have been checked"
139 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|
140 step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\""
142 step "the network is unplugged"
144 step "the network is plugged"
146 step "I start the computer"
147 step "the computer boots Tails"
149 step "I enable persistence" if persistence_on
150 step "I log in to a new session"
152 step "all notifications have disappeared"
155 step "all notifications have disappeared"
156 step "available upgrades have been checked"
161 When /^I power off the computer$/ do
162 assert($vm.is_running?,
163 "Trying to power off an already powered off VM")
167 When /^I cold reboot the computer$/ do
168 step "I power off the computer"
169 step "I start the computer"
172 When /^I destroy the computer$/ do
173 $vm.destroy_and_undefine
176 def boot_menu_cmdline_image
179 'TailsBootMenuKernelCmdlineUEFI.png'
181 'TailsBootMenuKernelCmdline.png'
185 def boot_menu_tab_msg_image
188 'TailsBootSplashTabMsgUEFI.png'
190 'TailsBootSplashTabMsg.png'
194 Given /^Tails is at the boot menu's cmdline( after rebooting)?$/ do |reboot|
196 # Simply looking for the boot splash image is not robust; sometimes
197 # sikuli is not fast enough to see it. Here we hope that spamming
198 # TAB, which will halt the boot process by showing the prompt for
199 # the kernel cmdline, will make this a bit more robust. We want this
200 # spamming to happen in parallel with Sikuli waiting for the image,
201 # but multi-threading etc is working extremely poor in our Ruby +
202 # jrb environment when Sikuli is involved. Hence we run the spamming
203 # from a separate process.
204 tab_spammer_code = <<-EOF
207 virt = Libvirt::open("qemu:///system")
209 domain = virt.lookup_domain_by_name('#{$vm.domain_name}')
211 domain.send_key(Libvirt::Domain::KEYCODE_SET_LINUX, 0, [tab_key_code])
218 # Our UEFI firmware (OVMF) has the interesting "feature" that pressing
219 # any button will open its setup menu, so we have to exit the setup,
220 # and to not have the TAB spammer potentially interfering we pause
222 dealt_with_uefi_setup = false
223 # The below code is not completely reliable, so we might have to
224 # retry by rebooting.
225 try_for(boot_timeout) do
227 tab_spammer = IO.popen(['ruby', '-e', tab_spammer_code])
228 if not(dealt_with_uefi_setup) && @os_loader == 'UEFI'
229 @screen.wait('UEFIFirmwareSetup.png', 30)
230 Process.kill("TSTP", tab_spammer.pid)
231 @screen.type(Sikuli::Key.ENTER)
232 Process.kill("CONT", tab_spammer.pid)
233 dealt_with_uefi_setup = true
235 @screen.wait(boot_menu_cmdline_image, 15)
236 rescue FindFailed => e
237 debug_log('We missed the boot menu before we could deal with it, ' +
239 dealt_with_uefi_setup = false
243 Process.kill("TERM", tab_spammer.pid)
250 Given /^the computer (re)?boots Tails$/ do |reboot|
251 step "Tails is at the boot menu's cmdline" + (reboot ? ' after rebooting' : '')
252 @screen.type(" autotest_never_use_this_option blacklist=psmouse #{@boot_options}" +
254 @screen.wait('TailsGreeter.png', 5*60)
255 $vm.wait_until_remote_shell_is_up
256 step 'I configure Tails to use a simulated Tor network'
259 Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
263 @screen.wait_and_click('TailsGreeterLanguage.png', 10)
264 @screen.wait('TailsGreeterLanguagePopover.png', 10)
265 @screen.type(@language)
266 sleep(2) # Gtk needs some time to filter the results
267 @screen.type(Sikuli::Key.ENTER)
268 @screen.wait_and_click("TailsGreeterLoginButton#{@language}.png", 10)
270 @screen.wait_and_click('TailsGreeterLoginButton.png', 10)
272 raise "Unsupported language: #{lang}"
274 step 'Tails Greeter has applied all settings'
275 step 'the Tails desktop is ready'
278 def open_greeter_additional_settings
279 @screen.click('TailsGreeterAddMoreOptions.png')
280 @screen.wait('TailsGreeterAdditionalSettingsDialog.png', 10)
283 Given /^I open Tails Greeter additional settings dialog$/ do
284 open_greeter_additional_settings()
287 Given /^I enable the specific Tor configuration option$/ do
288 open_greeter_additional_settings()
289 @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
290 @screen.wait_and_click("TailsGreeterSpecificTorConfiguration.png", 10)
291 @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
294 Given /^I set an administration password$/ do
295 open_greeter_additional_settings()
296 @screen.wait_and_click("TailsGreeterAdminPassword.png", 20)
297 @screen.type(@sudo_password)
298 @screen.type(Sikuli::Key.TAB)
299 @screen.type(@sudo_password)
300 @screen.type(Sikuli::Key.ENTER)
303 Given /^Tails Greeter has applied all settings$/ do
304 # I.e. it is done with PostLogin, which is ensured to happen before
305 # a logind session is opened for LIVE_USER.
307 $vm.execute_successfully("loginctl").stdout
308 .match(/^\s*\S+\s+\d+\s+#{LIVE_USER}\s+seat\d+\s+\S+\s*$/) != nil
312 Given /^the Tails desktop is ready$/ do
313 desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
314 @screen.wait(desktop_started_picture, 180)
315 # Workaround #13461 by restarting nautilus-desktop
316 # if Desktop icons are not visible
318 @screen.wait("DesktopTailsDocumentation.png", 30)
320 step 'I kill the process "nautilus-desktop"'
321 $vm.spawn('nautilus-desktop', user: LIVE_USER)
322 @screen.wait("DesktopTailsDocumentation.png", 30)
324 # Disable screen blanking since we sometimes need to wait long
325 # enough for it to activate, which can mess with Sikuli wait():ing
327 $vm.execute_successfully(
328 'gsettings set org.gnome.desktop.session idle-delay 0',
331 # We need to enable the accessibility toolkit for dogtail.
332 $vm.execute_successfully(
333 'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
338 When /^I see the "(.+)" notification(?: after at most (\d+) seconds)?$/ do |title, timeout|
339 timeout = timeout ? timeout.to_i : nil
340 gnome_shell = Dogtail::Application.new('gnome-shell')
341 notification_list = gnome_shell.child(
342 'No Notifications', roleName: 'label', showingOnly: false
345 notification_list.child?(title, roleName: 'label', showingOnly: false)
349 Given /^Tor is ready$/ do
350 step "Tor has built a circuit"
351 step "the time has synced"
353 try_for(30) { $vm.execute('systemctl is-system-running').success? }
354 rescue Timeout::Error
355 jobs = $vm.execute('systemctl list-jobs').stdout
356 units_status = $vm.execute('systemctl').stdout
357 raise "The system is not fully running yet:\n#{jobs}\n#{units_status}"
361 Given /^Tor has built a circuit$/ do
362 wait_until_tor_is_working
365 class TimeSyncingError < StandardError
368 Given /^the time has synced$/ do
370 ["/run/tordate/done", "/run/htpdate/success"].each do |file|
371 try_for(300) { $vm.execute("test -e #{file}").success? }
374 File.open("#{$config["TMPDIR"]}/log.htpdate", 'w') do |file|
375 file.write($vm.execute('cat /var/log/htpdate.log').stdout)
377 raise TimeSyncingError.new("Time syncing failed")
381 Given /^available upgrades have been checked$/ do
383 $vm.execute("test -e '/run/tails-upgrader/checked_upgrades'").success?
387 When /^I start the Tor Browser( in offline mode)?$/ do |offline|
388 step 'I start "Tor Browser" via GNOME Activities Overview'
390 offline_prompt = Dogtail::Application.new('zenity')
391 .dialog('Tor is not ready')
392 start_button = offline_prompt.button('Start Tor Browser')
393 start_button.grabFocus
396 step "the Tor Browser has started#{offline}"
398 step 'the Tor Browser shows the "The proxy server is refusing connections" error'
402 Given /^the Tor Browser (?:has started|starts)( in offline mode)?$/ do |offline|
404 @torbrowser = Dogtail::Application.new('Firefox')
405 @torbrowser.child?(roleName: 'frame', recursive: false)
409 Given /^the Tor Browser loads the (startup page|Tails homepage|Tails roadmap)$/ do |page|
413 when "Tails homepage"
414 title = 'Tails - Privacy for anyone anywhere'
416 title = 'Roadmap - Tails - Tails Ticket Tracker'
418 raise "Unsupported page: #{page}"
420 step "\"#{title}\" has loaded in the Tor Browser"
423 When /^I request a new identity using Torbutton$/ do
424 @torbrowser.child('Tor Browser', roleName: 'push button').click
425 @torbrowser.child('New Identity', roleName: 'push button').click
428 When /^I acknowledge Torbutton's New Identity confirmation prompt$/ do
429 @screen.wait('GnomeQuestionDialogIcon.png', 30)
433 Given /^I add a bookmark to eff.org in the Tor Browser$/ do
434 url = "https://www.eff.org"
435 step "I open the address \"#{url}\" in the Tor Browser"
436 step 'the Tor Browser shows the "The proxy server is refusing connections" error'
437 @screen.type("d", Sikuli::KeyModifier.CTRL)
438 @screen.wait("TorBrowserBookmarkPrompt.png", 10)
439 @screen.type(url + Sikuli::Key.ENTER)
442 Given /^the Tor Browser has a bookmark to eff.org$/ do
443 @screen.type("b", Sikuli::KeyModifier.ALT)
444 @screen.wait("TorBrowserEFFBookmark.png", 10)
447 Given /^all notifications have disappeared$/ do
448 # These magic coordinates always locates GNOME's clock in the top
449 # bar, which when clicked opens the calendar.
451 gnome_shell = Dogtail::Application.new('gnome-shell')
452 retry_action(10, recovery_proc: Proc.new { @screen.type(Sikuli::Key.ESC) }) do
453 @screen.click_point(x, y)
454 unless gnome_shell.child?('No Notifications', roleName: 'label')
455 @screen.click('GnomeCloseAllNotificationsButton.png')
457 gnome_shell.child?('No Notifications', roleName: 'label')
459 @screen.type(Sikuli::Key.ESC)
462 Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, image, time|
464 @screen.waitVanish(image, time.to_i)
466 @screen.wait(image, time.to_i)
470 Then /^all Internet traffic has only flowed through Tor$/ do
471 allowed_hosts = allowed_hosts_under_tor_enforcement
472 assert_all_connections(@sniffer.pcap_file) do |c|
473 allowed_hosts.include?({ address: c.daddr, port: c.dport })
477 Given /^I enter the sudo password in the pkexec prompt$/ do
478 step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
481 def deal_with_polkit_prompt(password, opts = {})
482 opts[:expect_success] ||= true
483 image = 'PolicyKitAuthPrompt.png'
484 @screen.wait(image, 60)
485 @screen.type(password)
486 @screen.type(Sikuli::Key.ENTER)
487 if opts[:expect_success]
488 @screen.waitVanish(image, 20)
490 @screen.wait('PolicyKitAuthFailure.png', 20)
494 Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
495 deal_with_polkit_prompt(password)
498 Given /^process "([^"]+)" is (not )?running$/ do |process, not_running|
500 assert(!$vm.has_process?(process), "Process '#{process}' is running")
502 assert($vm.has_process?(process), "Process '#{process}' is not running")
506 Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time|
507 try_for(time.to_i, :msg => "Process '#{process}' is not running after " +
508 "waiting for #{time} seconds") do
509 $vm.has_process?(process)
513 Given /^process "([^"]+)" has stopped running after at most (\d+) seconds$/ do |process, time|
514 try_for(time.to_i, :msg => "Process '#{process}' is still running after " +
515 "waiting for #{time} seconds") do
516 not $vm.has_process?(process)
520 Given /^I kill the process "([^"]+)"$/ do |process|
521 $vm.execute("killall #{process}")
522 try_for(10, :msg => "Process '#{process}' could not be killed") {
523 !$vm.has_process?(process)
527 Then /^Tails eventually (shuts down|restarts)$/ do |mode|
529 if mode == 'restarts'
530 @screen.find('TailsGreeter.png')
538 Given /^I shutdown Tails and wait for the computer to power off$/ do
539 $vm.spawn("poweroff")
540 step 'Tails eventually shuts down'
543 When /^I request a shutdown using the emergency shutdown applet$/ do
545 @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
546 # Sometimes the next button too fast, before the menu has settled
547 # down to its final size and the icon we want to click is in its
548 # final position. dogtail might allow us to fix that, but given how
549 # rare this problem is, it's not worth the effort.
550 step 'I wait 5 seconds'
551 @screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10)
554 When /^I warm reboot the computer$/ do
558 When /^I request a reboot using the emergency shutdown applet$/ do
560 @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
561 # See comment on /^I request a shutdown using the emergency shutdown applet$/
562 # that explains why we need to wait.
563 step 'I wait 5 seconds'
564 @screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10)
567 Given /^the package "([^"]+)" is installed$/ do |package|
568 assert($vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?,
569 "Package '#{package}' is not installed")
572 Given /^I add a ([a-z0-9.]+ |)wired DHCP NetworkManager connection called "([^"]+)"$/ do |version, con_name|
573 if version and version == '2.x'
577 uuid=b04afa94-c3a1-41bf-aa12-1a743d964162
581 con_file = "/etc/NetworkManager/system-connections/#{con_name}"
582 $vm.file_overwrite(con_file, con_content)
583 $vm.execute_successfully("chmod 600 '#{con_file}'")
584 $vm.execute_successfully("nmcli connection load '#{con_file}'")
585 elsif version and version == '3.x'
586 raise "Unsupported version '#{version}'"
588 $vm.execute_successfully(
589 "nmcli connection add con-name #{con_name} " + \
590 "type ethernet autoconnect yes ifname eth0"
594 nm_con_list = $vm.execute("nmcli --terse --fields NAME connection show").stdout
595 nm_con_list.split("\n").include? "#{con_name}"
599 Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
600 $vm.execute("nmcli connection up id #{con_name}")
602 $vm.execute("nmcli --terse --fields NAME,STATE connection show").stdout.chomp.split("\n").include?("#{con_name}:activated")
606 When /^I start and focus GNOME Terminal$/ do
607 step 'I start "GNOME Terminal" via GNOME Activities Overview'
608 @screen.wait('GnomeTerminalWindow.png', 40)
611 When /^I run "([^"]+)" in GNOME Terminal$/ do |command|
612 if !$vm.has_process?("gnome-terminal-server")
613 step "I start and focus GNOME Terminal"
615 @screen.wait_and_click('GnomeTerminalWindow.png', 20)
617 @screen.type(command + Sikuli::Key.ENTER)
620 When /^the file "([^"]+)" exists(?:| after at most (\d+) seconds)$/ do |file, timeout|
621 timeout = 0 if timeout.nil?
624 :msg => "The file #{file} does not exist after #{timeout} seconds"
626 $vm.file_exist?(file)
630 When /^the file "([^"]+)" does not exist$/ do |file|
631 assert(! ($vm.file_exist?(file)))
634 When /^the directory "([^"]+)" exists$/ do |directory|
635 assert($vm.directory_exist?(directory))
638 When /^the directory "([^"]+)" does not exist$/ do |directory|
639 assert(! ($vm.directory_exist?(directory)))
642 When /^I copy "([^"]+)" to "([^"]+)" as user "([^"]+)"$/ do |source, destination, user|
643 c = $vm.execute("cp \"#{source}\" \"#{destination}\"", :user => LIVE_USER)
644 assert(c.success?, "Failed to copy file:\n#{c.stdout}\n#{c.stderr}")
647 def is_persistent?(app)
648 conf = get_persistence_presets_config(true)["#{app}"]
649 c = $vm.execute("findmnt --noheadings --output SOURCE --target '#{conf}'")
650 # This check assumes that we haven't enabled read-only persistence.
651 c.success? and c.stdout.chomp != "aufs"
654 Then /^persistence for "([^"]+)" is (|not )enabled$/ do |app, enabled|
657 assert(is_persistent?(app), "Persistence should be enabled.")
659 assert(!is_persistent?(app), "Persistence should not be enabled.")
663 Given /^I start "([^"]+)" via GNOME Activities Overview$/ do |app_name|
664 # Search disambiguations: below we assume that there is only one
665 # result, since multiple results introduces a race that leads to a
666 # non-deterministic choice (at least under load). To make the life
667 # easier for users of this step, let's collect workarounds here.
669 when 'GNOME Terminal'
670 # "GNOME Terminal" and "Terminal" shows both the (non-Root)
671 # "Terminal" and "Root Terminal" search results, so let's use a
672 # keyword only found in the former's .desktop file.
673 app_name = 'commandline'
675 @screen.wait('GnomeApplicationsMenu.png', 10)
676 $vm.execute_successfully('xdotool key Super', user: LIVE_USER)
677 @screen.wait('GnomeActivitiesOverview.png', 10)
678 # Trigger startup of search providers
679 @screen.type(app_name[0])
680 # Give search providers some time to start (#13469#note-5) otherwise
681 # our search sometimes returns no results at all.
683 # Type the rest of the search query
684 @screen.type(app_name[1..-1])
685 @screen.type(Sikuli::Key.ENTER, Sikuli::KeyModifier.CTRL)
688 When /^I type "([^"]+)"$/ do |string|
692 When /^I press the "([^"]+)" key$/ do |key|
694 @screen.type(eval("Sikuli::Key.#{key}"))
696 raise "unsupported key #{key}"
700 Then /^the (amnesiac|persistent) Tor Browser directory (exists|does not exist)$/ do |persistent_or_not, mode|
701 case persistent_or_not
703 dir = "/home/#{LIVE_USER}/Tor Browser"
705 dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
707 step "the directory \"#{dir}\" #{mode}"
710 Then /^there is a GNOME bookmark for the (amnesiac|persistent) Tor Browser directory$/ do |persistent_or_not|
711 case persistent_or_not
713 bookmark_image = 'TorBrowserAmnesicFilesBookmark.png'
715 bookmark_image = 'TorBrowserPersistentFilesBookmark.png'
717 @screen.wait_and_click('GnomePlaces.png', 10)
718 @screen.wait(bookmark_image, 40)
719 @screen.type(Sikuli::Key.ESC)
722 Then /^there is no GNOME bookmark for the persistent Tor Browser directory$/ do
724 @screen.wait_and_click('GnomePlaces.png', 10)
725 @screen.wait("GnomePlacesWithoutTorBrowserPersistent.png", 10)
726 @screen.type(Sikuli::Key.ESC)
730 def pulseaudio_sink_inputs
731 pa_info = $vm.execute_successfully('pacmd info', :user => LIVE_USER).stdout
732 sink_inputs_line = pa_info.match(/^\d+ sink input\(s\) available\.$/)[0]
733 return sink_inputs_line.match(/^\d+/)[0].to_i
736 When /^(no|\d+) application(?:s?) (?:is|are) playing audio(?:| after (\d+) seconds)$/ do |nb, wait_time|
738 sleep wait_time.to_i if ! wait_time.nil?
739 assert_equal(nb.to_i, pulseaudio_sink_inputs)
742 When /^I double-click on the (Tails documentation|Report an Error) launcher on the desktop$/ do |launcher|
743 image = 'Desktop' + launcher.split.map { |s| s.capitalize } .join + '.png'
744 info = xul_application_info('Tor Browser')
745 # Sometimes the double-click is lost (#12131).
747 @screen.wait_and_double_click(image, 10) if $vm.execute("pgrep --uid #{info[:user]} --full --exact '#{info[:cmd_regex]}'").failure?
748 step 'the Tor Browser has started'
752 When /^I (can|cannot) save the current page as "([^"]+[.]html)" to the (.*) directory$/ do |should_work, output_file, output_dir|
753 should_work = should_work == 'can' ? true : false
754 @screen.type("s", Sikuli::KeyModifier.CTRL)
755 @screen.wait("Gtk3SaveFileDialog.png", 10)
756 if output_dir == "persistent Tor Browser"
757 output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
758 @screen.wait_and_click("GtkTorBrowserPersistentBookmark.png", 10)
759 @screen.wait("GtkTorBrowserPersistentBookmarkSelected.png", 10)
760 # The output filename (without its extension) is already selected,
761 # let's use the keyboard shortcut to focus its field
762 @screen.type("n", Sikuli::KeyModifier.ALT)
763 @screen.wait("TorBrowserSaveOutputFileSelected.png", 10)
764 elsif output_dir == "default downloads"
765 output_dir = "/home/#{LIVE_USER}/Tor Browser"
767 @screen.type(output_dir + '/')
769 # Only the part of the filename before the .html extension can be easily replaced
770 # so we have to remove it before typing it into the arget filename entry widget.
771 @screen.type(output_file.sub(/[.]html$/, ''))
772 @screen.type(Sikuli::Key.ENTER)
774 try_for(20, :msg => "The page was not saved to #{output_dir}/#{output_file}") {
775 $vm.file_exist?("#{output_dir}/#{output_file}")
778 @screen.wait("TorBrowserCannotSavePage.png", 10)
782 When /^I can print the current page as "([^"]+[.]pdf)" to the (default downloads|persistent Tor Browser) directory$/ do |output_file, output_dir|
783 if output_dir == "persistent Tor Browser"
784 output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
786 output_dir = "/home/#{LIVE_USER}/Tor Browser"
788 @screen.type("p", Sikuli::KeyModifier.CTRL)
789 print_dialog = @torbrowser.child('Print', roleName: 'dialog')
790 print_dialog.child('Print to File', 'table cell').click
791 print_dialog.child('~/Tor Browser/output.pdf', roleName: 'push button').click()
792 @screen.wait("Gtk3PrintFileDialog.png", 10)
793 # Only the file's basename is selected when the file selector dialog opens,
794 # so we type only the desired file's basename to replace it
795 $vm.set_clipboard(output_dir + '/' + output_file.sub(/[.]pdf$/, ''))
796 @screen.type('v', Sikuli::KeyModifier.CTRL)
797 @screen.type(Sikuli::Key.ENTER)
798 @screen.wait_and_click("Gtk3PrintButton.png", 10)
799 try_for(30, :msg => "The page was not printed to #{output_dir}/#{output_file}") {
800 $vm.file_exist?("#{output_dir}/#{output_file}")
804 Given /^a web server is running on the LAN$/ do
805 @web_server_ip_addr = $vmnet.bridge_ip_addr
806 @web_server_port = 8000
807 @web_server_url = "http://#{@web_server_ip_addr}:#{@web_server_port}"
808 web_server_hello_msg = "Welcome to the LAN web server!"
810 # I've tested ruby Thread:s, fork(), etc. but nothing works due to
811 # various strange limitations in the ruby interpreter. For instance,
812 # apparently concurrent IO has serious limits in the thread
813 # scheduler (e.g. sikuli's wait() would block WEBrick from reading
814 # from its socket), and fork():ing results in a lot of complex
815 # cucumber stuff (like our hooks!) ending up in the child process,
816 # breaking stuff in the parent process. After asking some supposed
817 # ruby pros, I've settled on the following.
820 STDOUT.reopen("/dev/null", "w")
821 STDERR.reopen("/dev/null", "w")
822 server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
823 :Port => #{@web_server_port},
824 :DocumentRoot => "/dev/null")
825 server.mount_proc("/") do |req, res|
826 res.body = "#{web_server_hello_msg}"
830 add_lan_host(@web_server_ip_addr, @web_server_port)
831 proc = IO.popen(['ruby', '-e', code])
832 try_for(10, :msg => "It seems the LAN web server failed to start") do
833 Process.kill(0, proc.pid) == 1
836 add_after_scenario_hook { Process.kill("TERM", proc.pid) }
838 # It seems necessary to actually check that the LAN server is
839 # serving, possibly because it isn't doing so reliably when setting
840 # up. If e.g. the Unsafe Browser (which *should* be able to access
841 # the web server) tries to access it too early, Firefox seems to
842 # take some random amount of time to retry fetching. Curl gives a
843 # more consistent result, so let's rely on that instead. Note that
844 # this forces us to capture traffic *after* this step in case
845 # accessing this server matters, like when testing the Tor Browser..
846 try_for(30, :msg => "Something is wrong with the LAN web server") do
847 msg = $vm.execute_successfully("curl #{@web_server_url}",
848 :user => LIVE_USER).stdout.chomp
849 web_server_hello_msg == msg
853 When /^I open a page on the LAN web server in the (.*)$/ do |browser|
854 step "I open the address \"#{@web_server_url}\" in the #{browser}"
857 Given /^I wait (?:between (\d+) and )?(\d+) seconds$/ do |min, max|
859 time = rand(max.to_i - min.to_i + 1) + min.to_i
863 puts "Slept for #{time} seconds"
867 Given /^I (?:re)?start monitoring the AppArmor log of "([^"]+)"$/ do |profile|
868 # AppArmor log entries may be dropped if printk rate limiting is
870 $vm.execute_successfully('sysctl -w kernel.printk_ratelimit=0')
871 # We will only care about entries for this profile from this time
873 guest_time = $vm.execute_successfully(
874 'date +"%Y-%m-%d %H:%M:%S"').stdout.chomp
875 @apparmor_profile_monitoring_start ||= Hash.new
876 @apparmor_profile_monitoring_start[profile] = guest_time
879 When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"$/ do |anti_test, profile, file|
880 assert(@apparmor_profile_monitoring_start &&
881 @apparmor_profile_monitoring_start[profile],
882 "It seems the profile '#{profile}' isn't being monitored by the " +
883 "'I monitor the AppArmor log of ...' step")
884 audit_line_regex = 'apparmor="DENIED" operation="open" profile="%s" name="%s"' % [profile, file]
886 try_for(10, { :delay => 1 }) {
887 audit_log = $vm.execute(
888 "journalctl --full --no-pager " +
889 "--since='#{@apparmor_profile_monitoring_start[profile]}' " +
890 "SYSLOG_IDENTIFIER=kernel | grep -w '#{audit_line_regex}'"
892 assert(audit_log.empty? == (anti_test ? true : false))
895 rescue Timeout::Error, Test::Unit::AssertionFailedError => e
896 raise e, "AppArmor has #{anti_test ? "" : "not "}denied the operation"
900 Then /^I force Tor to use a new circuit$/ do
901 force_new_tor_circuit
904 When /^I eject the boot medium$/ do
906 dev_type = device_info(dev)['ID_TYPE']
911 boot_disk_name = $vm.disk_name(dev)
912 $vm.unplug_drive(boot_disk_name)
914 raise "Unsupported medium type '#{dev_type}' for boot device '#{dev}'"
918 Given /^Tails is fooled to think it is running version (.+)$/ do |version|
919 $vm.execute_successfully(
921 "'s/^TAILS_VERSION_ID=.*$/TAILS_VERSION_ID=\"#{version}\"/' " +
926 Then /^Tails is running version (.+)$/ do |version|
927 v1 = $vm.execute_successfully('tails-version').stdout.split.first
928 assert_equal(version, v1, "The version doesn't match tails-version's output")
929 v2 = $vm.file_content('/etc/os-release')
930 .scan(/TAILS_VERSION_ID="(#{version})"/).flatten.first
931 assert_equal(version, v2, "The version doesn't match /etc/os-release")
934 def share_host_files(files)
935 files = [files] if files.class == String
936 assert_equal(Array, files.class)
937 disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
938 # Let's add some extra space for filesystem overhead etc.
939 disk_size += [convert_to_bytes(1, 'MiB'), (disk_size * 0.15).ceil].max
940 disk = random_alpha_string(10)
941 step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
942 step "I create a gpt partition labeled \"#{disk}\" with an ext4 " +
943 "filesystem on disk \"#{disk}\""
944 $vm.storage.guestfs_disk_helper(disk) do |g, _|
945 partition = g.list_partitions().first
946 g.mount(partition, "/")
947 files.each { |f| g.upload(f, "/" + File.basename(f)) }
949 step "I plug USB drive \"#{disk}\""
950 mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
951 dev = $vm.disk_dev(disk)
952 partition = dev + '1'
953 $vm.execute_successfully("mount #{partition} #{mount_dir}")
954 $vm.execute_successfully("chmod -R a+rX '#{mount_dir}'")
958 def mount_USB_drive(disk, fs_options = {})
959 fs_options[:encrypted] ||= false
960 @tmp_usb_drive_mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
961 dev = $vm.disk_dev(disk)
962 partition = dev + '1'
963 if fs_options[:encrypted]
964 password = fs_options[:password]
965 assert_not_nil(password)
966 luks_mapping = "#{disk}_unlocked"
967 $vm.execute_successfully(
968 "echo #{password} | " +
969 "cryptsetup luksOpen #{partition} #{luks_mapping}"
971 $vm.execute_successfully(
972 "mount /dev/mapper/#{luks_mapping} #{@tmp_usb_drive_mount_dir}"
975 $vm.execute_successfully("mount #{partition} #{@tmp_usb_drive_mount_dir}")
977 @tmp_filesystem_disk = disk
978 @tmp_filesystem_options = fs_options
979 @tmp_filesystem_partition = partition
980 return @tmp_usb_drive_mount_dir
983 When(/^I plug and mount a (\d+) MiB USB drive with an? (.*)$/) do |size_MiB, fs|
984 disk_size = convert_to_bytes(size_MiB.to_i, 'MiB')
985 disk = random_alpha_string(10)
986 step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
987 step "I create a gpt partition labeled \"#{disk}\" with " +
988 "an #{fs} on disk \"#{disk}\""
989 step "I plug USB drive \"#{disk}\""
991 fs_options[:filesystem] = /(.*) filesystem/.match(fs)[1]
992 if /\bencrypted with password\b/.match(fs)
993 fs_options[:encrypted] = true
994 fs_options[:password] = /encrypted with password "([^"]+)"/.match(fs)[1]
996 mount_dir = mount_USB_drive(disk, fs_options)
997 @tmp_filesystem_size_b = convert_to_bytes(
998 avail_space_in_mountpoint_kB(mount_dir),
1003 When(/^I mount the USB drive again$/) do
1004 mount_USB_drive(@tmp_filesystem_disk, @tmp_filesystem_options)
1007 When(/^I umount the USB drive$/) do
1008 $vm.execute_successfully("umount #{@tmp_usb_drive_mount_dir}")
1009 if @tmp_filesystem_options[:encrypted]
1010 $vm.execute_successfully("cryptsetup luksClose #{@tmp_filesystem_disk}_unlocked")
1014 When /^Tails system time is magically synchronized$/ do
1015 $vm.host_to_guest_time_sync
1018 # Useful for debugging scenarios: e.g. inject this step in a scenario
1019 # at some point when you want to investigate the state.
1024 # Useful for debugging Tails features: let's say you want to fix a bug
1025 # exposed by $SCENARIO, and is working on a fix in $FILE locally. To
1026 # immediately test your fix, simply inject this step into $SCENARIO,
1027 # so that $FILE is put in place (obviously this depends on that no
1028 # extra steps are needed to make $FILE's changes go "live").
1029 When /^I upload "([^"]*)" to "([^"]*)"$/ do |source, destination|
1030 [source, destination].each { |s| s.sub!(/\/*$/, '') }
1031 Dir.glob(source).each do |path|
1032 if File.directory?(path)
1033 new_destination = "#{destination}/#{File.basename(path)}"
1034 $vm.execute_successfully("mkdir -p '#{new_destination}'")
1035 Dir.new(path).each do |child|
1036 next if child == '.' or child == '..'
1037 step "I upload \"#{path}/#{child}\" to \"#{new_destination}\""
1040 File.open(path) do |f|
1041 final_destination = destination
1042 if $vm.directory_exist?(final_destination)
1043 final_destination += "/#{File.basename(path)}"
1045 $vm.file_overwrite(final_destination, f.read)