common_steps.rb 32.1 KB
Newer Older
1
2
require 'fileutils'

3
4
5
6
7
def post_vm_start_hook
  # Sometimes the first click is lost (presumably it's used to give
  # focus to virt-viewer or similar) so we do that now rather than
  # having an important click lost. The point we click should be
  # somewhere where no clickable elements generally reside.
8
  @screen.click_point(@screen.w - 1, @screen.h/2)
9
10
end

kytv's avatar
kytv committed
11
12
def context_menu_helper(top, bottom, menu_item)
  try_for(60) do
13
14
15
16
17
18
19
    t = @screen.wait(top, 10)
    b = @screen.wait(bottom, 10)
    # In Sikuli, lower x == closer to the left, lower y == closer to the top
    assert(t.y < b.y)
    center = Sikuli::Location.new(((t.x + t.w) + b.x)/2,
                                  ((t.y + t.h) + b.y)/2)
    @screen.right_click(center)
kytv's avatar
kytv committed
20
21
22
23
24
25
    @screen.hide_cursor
    @screen.wait_and_click(menu_item, 10)
    return
  end
end

26
def post_snapshot_restore_hook
27
  $vm.wait_until_remote_shell_is_up
28
  post_vm_start_hook
29

30
31
32
  # The guest's Tor's circuits' states are likely to get out of sync
  # with the other relays, so we ensure that we have fresh circuits.
  # Time jumps and incorrect clocks also confuses Tor in many ways.
33
  $vm.host_to_guest_time_sync
34
35
36
37
38
39
40
  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.spawn("restart-tor")
    wait_until_tor_is_working
  end
41
42
end

43
Given /^a computer$/ do
44
45
  $vm.destroy_and_undefine if $vm
  $vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
46
47
end

48
Given /^the computer has (\d+) ([[:alpha:]]+) of RAM$/ do |size, unit|
49
  $vm.set_ram_size(size, unit)
50
51
end

52
Given /^the computer is set to boot from the Tails DVD$/ do
53
  $vm.set_cdrom_boot(TAILS_ISO)
54
55
end

56
Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name|
57
  $vm.set_disk_boot(name, type.downcase)
58
59
end

60
Given /^I (temporarily )?create an? (\d+) ([[:alpha:]]+) disk named "([^"]+)"$/ do |temporary, size, unit, name|
61
  $vm.storage.create_new_disk(name, {:size => size, :unit => unit,
62
                                     :type => "qcow2"})
63
  add_after_scenario_hook { $vm.storage.delete_volume(name) } if temporary
64
65
end

66
Given /^I plug (.+) drive "([^"]+)"$/ do |bus, name|
67
68
  $vm.plug_drive(name, bus.downcase)
  if $vm.is_running?
69
70
71
72
73
    step "drive \"#{name}\" is detected by Tails"
  end
end

Then /^drive "([^"]+)" is detected by Tails$/ do |name|
74
  raise "Tails is not running" unless $vm.is_running?
75
  try_for(10, :msg => "Drive '#{name}' is not detected by Tails") do
76
    $vm.disk_detected?(name)
77
78
79
  end
end

80
Given /^the network is plugged$/ do
81
  $vm.plug_network
82
83
84
end

Given /^the network is unplugged$/ do
85
  $vm.unplug_network
86
87
end

88
89
90
91
92
Given /^the network connection is ready(?: within (\d+) seconds)?$/ do |timeout|
  timeout ||= 30
  try_for(timeout.to_i) { $vm.has_network? }
end

93
Given /^the hardware clock is set to "([^"]*)"$/ do |time|
94
  $vm.set_hardware_clock(DateTime.parse(time).to_time)
95
96
end

97
Given /^I capture all network traffic$/ do
98
  @sniffer = Sniffer.new("sniffer", $vmnet)
99
  @sniffer.capture
100
101
102
103
  add_after_scenario_hook do
    @sniffer.stop
    @sniffer.clear
  end
104
105
106
107
108
109
110
end

Given /^I set Tails to boot with options "([^"]*)"$/ do |options|
  @boot_options = options
end

When /^I start the computer$/ do
111
  assert(!$vm.is_running?,
112
         "Trying to start a VM that is already running")
113
  $vm.start
114
  post_vm_start_hook
115
116
end

117
Given /^I start Tails( from DVD)?( with network unplugged)?( and I login)?$/ do |dvd_boot, network_unplugged, do_login|
118
  step "the computer is set to boot from the Tails DVD" if dvd_boot
anonym's avatar
anonym committed
119
  if network_unplugged
120
    step "the network is unplugged"
anonym's avatar
anonym committed
121
122
  else
    step "the network is plugged"
123
  end
124
125
  step "I start the computer"
  step "the computer boots Tails"
126
127
  if do_login
    step "I log in to a new session"
anonym's avatar
anonym committed
128
    if network_unplugged
129
130
      step "all notifications have disappeared"
    else
anonym's avatar
anonym committed
131
      step "Tor is ready"
132
      step "all notifications have disappeared"
anonym's avatar
anonym committed
133
      step "available upgrades have been checked"
134
    end
135
  end
136
137
end

anonym's avatar
anonym committed
138
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|
139
  step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\""
anonym's avatar
anonym committed
140
  if network_unplugged
141
    step "the network is unplugged"
anonym's avatar
anonym committed
142
143
  else
    step "the network is plugged"
144
  end
145
146
  step "I start the computer"
  step "the computer boots Tails"
147
  if do_login
148
    step "I enable persistence" if persistence_on
149
    step "I log in to a new session"
anonym's avatar
anonym committed
150
    if network_unplugged
151
      step "all notifications have disappeared"
152
    else
anonym's avatar
anonym committed
153
      step "Tor is ready"
154
      step "all notifications have disappeared"
anonym's avatar
anonym committed
155
      step "available upgrades have been checked"
156
157
    end
  end
158
159
end

160
When /^I power off the computer$/ do
161
  assert($vm.is_running?,
162
         "Trying to power off an already powered off VM")
163
  $vm.power_off
164
165
166
167
168
169
170
171
end

When /^I cold reboot the computer$/ do
  step "I power off the computer"
  step "I start the computer"
end

When /^I destroy the computer$/ do
172
  $vm.destroy_and_undefine
173
174
end

175
def boot_menu_cmdline_image
176
177
  case @os_loader
  when "UEFI"
178
    'TailsBootMenuKernelCmdlineUEFI.png'
179
  else
180
    'TailsBootMenuKernelCmdline.png'
181
182
  end
end
183

anonym's avatar
anonym committed
184
def boot_menu_tab_msg_image
185
186
  case @os_loader
  when "UEFI"
187
    'TailsBootSplashTabMsgUEFI.png'
188
  else
189
    'TailsBootSplashTabMsg.png'
190
  end
191
192
end

anonym's avatar
anonym committed
193
194
def memory_wipe_timeout
  nr_gigs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
anonym's avatar
anonym committed
195
  nr_gigs_of_ram*30
anonym's avatar
anonym committed
196
end
197

anonym's avatar
anonym committed
198
Given /^Tails is at the boot menu's cmdline( after rebooting)?$/ do |reboot|
anonym's avatar
anonym committed
199
  boot_timeout = 3*60
200
  # We need some extra time for memory wiping if rebooting
anonym's avatar
anonym committed
201
  boot_timeout += memory_wipe_timeout if reboot
202
203
204
205
206
207
208
209
  # Simply looking for the boot splash image is not robust; sometimes
  # sikuli is not fast enough to see it. Here we hope that spamming
  # TAB, which will halt the boot process by showing the prompt for
  # the kernel cmdline, will make this a bit more robust. We want this
  # spamming to happen in parallel with Sikuli waiting for the image,
  # but multi-threading etc is working extremely poor in our Ruby +
  # jrb environment when Sikuli is involved. Hence we run the spamming
  # from a separate process.
210
  tab_spammer_code = <<-EOF
anonym's avatar
anonym committed
211
212
213
214
215
216
217
218
219
220
221
222
    require 'libvirt'
    tab_key_code = 0xf
    virt = Libvirt::open("qemu:///system")
    begin
      domain = virt.lookup_domain_by_name('#{$vm.domain_name}')
      loop do
        domain.send_key(Libvirt::Domain::KEYCODE_SET_LINUX, 0, [tab_key_code])
        sleep 0.1
      end
    ensure
      virt.close
    end
223
  EOF
224
  # Our UEFI firmware (OVMF) has the interesting "feature" that pressing
225
226
227
  # any button will open its setup menu, so we have to exit the setup,
  # and to not have the TAB spammer potentially interfering we pause
  # it meanwhile.
228
  dealt_with_uefi_setup = false
anonym's avatar
anonym committed
229
230
  # The below code is not completely reliable, so we might have to
  # retry by rebooting.
231
232
  try_for(boot_timeout) do
    begin
anonym's avatar
anonym committed
233
      tab_spammer = IO.popen(['ruby', '-e', tab_spammer_code])
234
      if not(dealt_with_uefi_setup) && @os_loader == 'UEFI'
235
        @screen.wait('UEFIFirmwareSetup.png', 30)
236
237
238
239
240
        Process.kill("TSTP", tab_spammer.pid)
        @screen.type(Sikuli::Key.ENTER)
        Process.kill("CONT", tab_spammer.pid)
        dealt_with_uefi_setup = true
      end
241
      @screen.wait(boot_menu_cmdline_image, 15)
242
    rescue FindFailed => e
243
244
245
246
      debug_log('We missed the boot menu before we could deal with it, ' +
                'resetting...')
      dealt_with_uefi_setup = false
      $vm.reset
247
      retry
248
249
250
    ensure
      Process.kill("TERM", tab_spammer.pid)
      tab_spammer.close
251
    end
252
  end
253
end
254

255
Given /^the computer (re)?boots Tails$/ do |reboot|
anonym's avatar
anonym committed
256
  step "Tails is at the boot menu's cmdline" + (reboot ? ' after rebooting' : '')
anonym's avatar
anonym committed
257
  @screen.type(" autotest_never_use_this_option blacklist=psmouse #{@boot_options}" +
258
               Sikuli::Key.ENTER)
259
  @screen.wait('TailsGreeter.png', 5*60)
260
  $vm.wait_until_remote_shell_is_up
anonym's avatar
anonym committed
261
  step 'I configure Tails to use a simulated Tor network'
262
263
end

264
265
266
267
Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
  case lang
  when 'German'
    @language = "German"
268
    @screen.wait_and_click('TailsGreeterLanguage.png', 10)
269
270
271
272
    @screen.wait('TailsGreeterLanguagePopover.png', 10)
    @screen.type(@language)
    sleep(2) # Gtk needs some time to filter the results
    @screen.type(Sikuli::Key.ENTER)
273
    @screen.wait_and_click("TailsGreeterLoginButton#{@language}.png", 10)
274
275
276
277
278
  when ''
    @screen.wait_and_click('TailsGreeterLoginButton.png', 10)
  else
    raise "Unsupported language: #{lang}"
  end
279
  step 'Tails Greeter has applied all settings'
280
  step 'the Tails desktop is ready'
281
282
end

283
284
285
286
287
288
289
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()
290
291
end

292
Given /^I enable the specific Tor configuration option$/ do
293
  open_greeter_additional_settings()
294
295
296
  @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
  @screen.wait_and_click("TailsGreeterSpecificTorConfiguration.png", 10)
  @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
297
298
end

299
Given /^I set an administration password$/ do
300
301
  open_greeter_additional_settings()
  @screen.wait_and_click("TailsGreeterAdminPassword.png", 20)
302
303
304
  @screen.type(@sudo_password)
  @screen.type(Sikuli::Key.TAB)
  @screen.type(@sudo_password)
305
  @screen.type(Sikuli::Key.ENTER)
306
307
end

308
309
310
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.
intrigeri's avatar
intrigeri committed
311
  try_for(120) {
312
    $vm.execute_successfully("loginctl").stdout
anonym's avatar
anonym committed
313
      .match(/^\s*\S+\s+\d+\s+#{LIVE_USER}\s+seat\d+\s+\S+\s*$/) != nil
314
  }
315
316
end

317
Given /^the Tails desktop is ready$/ do
318
  desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
319
  @screen.wait(desktop_started_picture, 180)
320
321
  # We wait for the Florence icon to be displayed to ensure reliable systray icon clicking.
  @screen.wait("GnomeSystrayFlorence.png", 30)
322
323
324
325
326
327
328
  # 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.
  $vm.execute_successfully(
    'gsettings set org.gnome.desktop.session idle-delay 0',
    :user => LIVE_USER
  )
329
330
331
332
333
  # We need to enable the accessibility toolkit for dogtail.
  $vm.execute_successfully(
    'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
    :user => LIVE_USER,
  )
334
335
end

336
337
338
339
340
341
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
342
343
344
  try_for(timeout) do
    notification_list.child?(title, roleName: 'label', showingOnly: false)
  end
345
end
346
347
348
349

Given /^Tor is ready$/ do
  step "Tor has built a circuit"
  step "the time has synced"
350
351
352
353
  if $vm.execute('systemctl is-system-running').failure?
    units_status = $vm.execute('systemctl').stdout
    raise "At least one system service failed to start:\n#{units_status}"
  end
354
355
356
357
358
359
360
end

Given /^Tor has built a circuit$/ do
  wait_until_tor_is_working
end

Given /^the time has synced$/ do
361
  ["/run/tordate/done", "/run/htpdate/success"].each do |file|
362
    try_for(300) { $vm.execute("test -e #{file}").success? }
363
364
365
  end
end

366
367
Given /^available upgrades have been checked$/ do
  try_for(300) {
368
    $vm.execute("test -e '/run/tails-upgrader/checked_upgrades'").success?
369
370
371
  }
end

372
When /^I start the Tor Browser( in offline mode)?$/ do |offline|
373
  step 'I start "Tor Browser" via the GNOME "Internet" applications menu'
374
375
376
377
378
  if offline
    offline_prompt = Dogtail::Application.new('zenity')
                     .dialog('Tor is not ready')
    offline_prompt.button('Start Tor Browser').click
  end
379
  step "the Tor Browser has started#{offline}"
380
381
382
  if offline
    step 'the Tor Browser shows the "The proxy server is refusing connections" error'
  end
383
384
end

385
Given /^the Tor Browser has started( in offline mode)?$/ do |offline|
anonym's avatar
anonym committed
386
  try_for(60) do
387
    @torbrowser = Dogtail::Application.new('Firefox')
388
    @torbrowser.child?(roleName: 'frame', recursive: false)
anonym's avatar
anonym committed
389
  end
390
391
end

392
Given /^the Tor Browser loads the (startup page|Tails roadmap)$/ do |page|
393
394
  case page
  when "startup page"
395
    title = 'Tails - News'
396
  when "Tails roadmap"
anonym's avatar
anonym committed
397
    title = 'Roadmap - Tails - RiseupLabs Code Repository'
398
399
400
  else
    raise "Unsupported page: #{page}"
  end
anonym's avatar
anonym committed
401
  step "\"#{title}\" has loaded in the Tor Browser"
402
403
end

404
405
406
407
408
409
410
411
412
413
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

414
415
416
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"
anonym's avatar
anonym committed
417
  step 'the Tor Browser shows the "The proxy server is refusing connections" error'
418
419
420
421
422
423
424
425
  @screen.type("d", Sikuli::KeyModifier.CTRL)
  @screen.wait("TorBrowserBookmarkPrompt.png", 10)
  @screen.type(url + Sikuli::Key.ENTER)
end

Given /^the Tor Browser has a bookmark to eff.org$/ do
  @screen.type("b", Sikuli::KeyModifier.ALT)
  @screen.wait("TorBrowserEFFBookmark.png", 10)
426
427
end

428
Given /^all notifications have disappeared$/ do
429
430
431
  # These magic coordinates always locates GNOME's clock in the top
  # bar, which when clicked opens the calendar.
  x, y = 512, 10
432
  gnome_shell = Dogtail::Application.new('gnome-shell')
433
434
  retry_action(10, recovery_proc: Proc.new { @screen.type(Sikuli::Key.ESC) }) do
    @screen.click_point(x, y)
435
    unless gnome_shell.child?('No Notifications', roleName: 'label')
436
437
      @screen.click('GnomeCloseAllNotificationsButton.png')
    end
438
    gnome_shell.child?('No Notifications', roleName: 'label')
439
  end
440
  @screen.type(Sikuli::Key.ESC)
Tails developers's avatar
Tails developers committed
441
442
end

443
444
445
446
447
448
449
Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, image, time|
  begin
    @screen.wait(image, time.to_i)
    raise "found '#{image}' while expecting not to" if negation
  rescue FindFailed => e
    raise e if not(negation)
  end
450
451
452
end

Then /^all Internet traffic has only flowed through Tor$/ do
453
  allowed_hosts = allowed_hosts_under_tor_enforcement
anonym's avatar
anonym committed
454
  assert_all_connections(@sniffer.pcap_file) do |c|
455
    allowed_hosts.include?({ address: c.daddr, port: c.dport })
456
  end
457
end
458

459
460
Given /^I enter the sudo password in the pkexec prompt$/ do
  step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
461
462
end

463
464
465
def deal_with_polkit_prompt(password, opts = {})
  opts[:expect_success] ||= true
  image = 'PolicyKitAuthPrompt.png'
466
  @screen.wait(image, 60)
467
  @screen.type(password)
468
  @screen.type(Sikuli::Key.ENTER)
469
470
471
472
473
  if opts[:expect_success]
    @screen.waitVanish(image, 20)
  else
    @screen.wait('PolicyKitAuthFailure.png', 20)
  end
474
475
476
end

Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
477
  deal_with_polkit_prompt(password)
478
479
end

kytv's avatar
kytv committed
480
481
482
483
484
485
Given /^process "([^"]+)" is (not )?running$/ do |process, not_running|
  if not_running
    assert(!$vm.has_process?(process), "Process '#{process}' is running")
  else
    assert($vm.has_process?(process), "Process '#{process}' is not running")
  end
486
487
end

488
Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time|
Tails developers's avatar
Tails developers committed
489
490
  try_for(time.to_i, :msg => "Process '#{process}' is not running after " +
                             "waiting for #{time} seconds") do
491
    $vm.has_process?(process)
492
493
494
  end
end

495
496
497
Given /^process "([^"]+)" has stopped running after at most (\d+) seconds$/ do |process, time|
  try_for(time.to_i, :msg => "Process '#{process}' is still running after " +
                             "waiting for #{time} seconds") do
498
    not $vm.has_process?(process)
499
500
501
  end
end

502
Given /^I kill the process "([^"]+)"$/ do |process|
503
  $vm.execute("killall #{process}")
504
  try_for(10, :msg => "Process '#{process}' could not be killed") {
505
    !$vm.has_process?(process)
506
  }
507
508
end

509
Then /^Tails eventually (shuts down|restarts)$/ do |mode|
510
511
  nr_gibs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
  timeout = nr_gibs_of_ram*5*60
512
  # Work around Tails bug #11786, where something goes wrong when we
513
514
  # kexec to the new kernel for memory wiping and gets dropped to a
  # BusyBox shell instead.
515
  try_for(timeout) do
516
517
    if @screen.existsAny(['TailsBug11786a.png', 'TailsBug11786b.png'])
      puts "We were hit by bug #11786: memory wiping got stuck"
518
519
520
521
522
523
524
      if mode == 'restarts'
        $vm.reset
      else
        $vm.power_off
      end
    else
      if mode == 'restarts'
anonym's avatar
anonym committed
525
        @screen.find('TailsGreeter.png')
526
527
528
529
530
        true
      else
        ! $vm.is_running?
      end
    end
531
532
533
534
  end
end

Given /^I shutdown Tails and wait for the computer to power off$/ do
535
  $vm.spawn("poweroff")
536
537
538
539
  step 'Tails eventually shuts down'
end

When /^I request a shutdown using the emergency shutdown applet$/ do
540
  @screen.hide_cursor
541
  @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
542
543
544
545
546
  # 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'
547
  @screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10)
548
549
end

550
When /^I warm reboot the computer$/ do
551
  $vm.spawn("reboot")
552
553
end

554
555
556
When /^I request a reboot using the emergency shutdown applet$/ do
  @screen.hide_cursor
  @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
557
558
559
  # See comment on /^I request a shutdown using the emergency shutdown applet$/
  # that explains why we need to wait.
  step 'I wait 5 seconds'
560
  @screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10)
561
562
end

anonym's avatar
anonym committed
563
Given /^the package "([^"]+)" is installed$/ do |package|
564
  assert($vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?,
565
         "Package '#{package}' is not installed")
566
end
567

568
569
570
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
571
572
573
574
575
576
[connection]
id=#{con_name}
uuid=b04afa94-c3a1-41bf-aa12-1a743d964162
interface-name=eth0
type=ethernet
EOF
577
578
579
580
    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}'")
581
582
  elsif version and version == '3.x'
    raise "Unsupported version '#{version}'"
583
584
585
586
587
588
  else
    $vm.execute_successfully(
      "nmcli connection add con-name #{con_name} " + \
      "type ethernet autoconnect yes ifname eth0"
    )
  end
589
590
591
592
593
594
  try_for(10) {
    nm_con_list = $vm.execute("nmcli --terse --fields NAME connection show").stdout
    nm_con_list.split("\n").include? "#{con_name}"
  }
end

595
Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
596
597
598
599
  $vm.execute("nmcli connection up id #{con_name}")
  try_for(60) do
    $vm.execute("nmcli --terse --fields NAME,STATE connection show").stdout.chomp.split("\n").include?("#{con_name}:activated")
  end
600
end
601
602

When /^I start and focus GNOME Terminal$/ do
603
  step 'I start "GNOME Terminal" via the GNOME "Utilities" applications menu'
intrigeri's avatar
intrigeri committed
604
  @screen.wait('GnomeTerminalWindow.png', 40)
605
606
607
end

When /^I run "([^"]+)" in GNOME Terminal$/ do |command|
608
  if !$vm.has_process?("gnome-terminal-server")
609
610
611
612
    step "I start and focus GNOME Terminal"
  else
    @screen.wait_and_click('GnomeTerminalWindow.png', 20)
  end
613
614
  @screen.type(command + Sikuli::Key.ENTER)
end
615

616
617
618
619
620
621
When /^the file "([^"]+)" exists(?:| after at most (\d+) seconds)$/ do |file, timeout|
  timeout = 0 if timeout.nil?
  try_for(
    timeout.to_i,
    :msg => "The file #{file} does not exist after #{timeout} seconds"
  ) {
622
    $vm.file_exist?(file)
623
624
625
626
  }
end

When /^the file "([^"]+)" does not exist$/ do |file|
627
  assert(! ($vm.file_exist?(file)))
628
629
630
end

When /^the directory "([^"]+)" exists$/ do |directory|
631
  assert($vm.directory_exist?(directory))
632
633
634
end

When /^the directory "([^"]+)" does not exist$/ do |directory|
635
  assert(! ($vm.directory_exist?(directory)))
636
637
638
end

When /^I copy "([^"]+)" to "([^"]+)" as user "([^"]+)"$/ do |source, destination, user|
639
  c = $vm.execute("cp \"#{source}\" \"#{destination}\"", :user => LIVE_USER)
640
  assert(c.success?, "Failed to copy file:\n#{c.stdout}\n#{c.stderr}")
641
end
642

643
644
def is_persistent?(app)
  conf = get_persistence_presets(true)["#{app}"]
645
646
  c = $vm.execute("findmnt --noheadings --output SOURCE --target '#{conf}'")
  # This check assumes that we haven't enabled read-only persistence.
anonym's avatar
anonym committed
647
  c.success? and c.stdout.chomp != "aufs"
648
649
650
651
652
653
654
655
656
657
658
end

Then /^persistence for "([^"]+)" is (|not )enabled$/ do |app, enabled|
  case enabled
  when ''
    assert(is_persistent?(app), "Persistence should be enabled.")
  when 'not '
    assert(!is_persistent?(app), "Persistence should not be enabled.")
  end
end

659
Given /^I start "([^"]+)" via the GNOME "([^"]+)" applications menu$/ do |app_name, submenu|
anonym's avatar
anonym committed
660
661
  # XXX: Dogtail is buggy when interacting with the Applications menu
  # (see #11718) so we use the GNOME Applications Overview instead.
662
663
664
665
  @screen.wait('GnomeApplicationsMenu.png', 10)
  $vm.execute_successfully('xdotool key Super', user: LIVE_USER)
  @screen.wait('GnomeActivitiesOverview.png', 10)
  @screen.type(app_name)
666
  @screen.type(Sikuli::Key.ENTER, Sikuli::KeyModifier.CTRL)
667
end
668
669
670
671
672
673

When /^I type "([^"]+)"$/ do |string|
  @screen.type(string)
end

When /^I press the "([^"]+)" key$/ do |key|
674
675
676
  begin
    @screen.type(eval("Sikuli::Key.#{key}"))
  rescue RuntimeError
Tails developers's avatar
Tails developers committed
677
    raise "unsupported key #{key}"
678
679
  end
end
680
681
682
683

Then /^the (amnesiac|persistent) Tor Browser directory (exists|does not exist)$/ do |persistent_or_not, mode|
  case persistent_or_not
  when "amnesiac"
684
    dir = "/home/#{LIVE_USER}/Tor Browser"
685
  when "persistent"
686
    dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
  end
  step "the directory \"#{dir}\" #{mode}"
end

Then /^there is a GNOME bookmark for the (amnesiac|persistent) Tor Browser directory$/ do |persistent_or_not|
  case persistent_or_not
  when "amnesiac"
    bookmark_image = 'TorBrowserAmnesicFilesBookmark.png'
  when "persistent"
    bookmark_image = 'TorBrowserPersistentFilesBookmark.png'
  end
  @screen.wait_and_click('GnomePlaces.png', 10)
  @screen.wait(bookmark_image, 40)
  @screen.type(Sikuli::Key.ESC)
end

703
Then /^there is no GNOME bookmark for the persistent Tor Browser directory$/ do
704
705
706
707
708
  try_for(65) do
    @screen.wait_and_click('GnomePlaces.png', 10)
    @screen.wait("GnomePlacesWithoutTorBrowserPersistent.png", 10)
    @screen.type(Sikuli::Key.ESC)
  end
709
710
end

711
def pulseaudio_sink_inputs
712
  pa_info = $vm.execute_successfully('pacmd info', :user => LIVE_USER).stdout
713
714
715
716
717
718
719
720
721
722
  sink_inputs_line = pa_info.match(/^\d+ sink input\(s\) available\.$/)[0]
  return sink_inputs_line.match(/^\d+/)[0].to_i
end

When /^(no|\d+) application(?:s?) (?:is|are) playing audio(?:| after (\d+) seconds)$/ do |nb, wait_time|
  nb = 0 if nb == "no"
  sleep wait_time.to_i if ! wait_time.nil?
  assert_equal(nb.to_i, pulseaudio_sink_inputs)
end

723
724
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'
anonym's avatar
anonym committed
725
  info = xul_application_info('Tor Browser')
726
727
  # Sometimes the double-click is lost (#12131).
  retry_action(10) do
anonym's avatar
anonym committed
728
    @screen.wait_and_double_click(image, 10) if $vm.execute("pgrep --uid #{info[:user]} --full --exact '#{info[:cmd_regex]}'").failure?
729
730
    step 'the Tor Browser has started'
  end
731
732
733
734
735
736
737
738
739
740
741
742
743
744
end

When /^I click the blocked video icon$/ do
  @screen.wait_and_click("TorBrowserBlockedVideo.png", 30)
end

When /^I accept to temporarily allow playing this video$/ do
  @screen.wait_and_click("TorBrowserOkButton.png", 10)
end

When /^I click the HTML5 play button$/ do
  @screen.wait_and_click("TorBrowserHtml5PlayButton.png", 30)
end

745
746
When /^I (can|cannot) save the current page as "([^"]+[.]html)" to the (.*) directory$/ do |should_work, output_file, output_dir|
  should_work = should_work == 'can' ? true : false
747
  @screen.type("s", Sikuli::KeyModifier.CTRL)
748
  @screen.wait("TorBrowserSaveDialog.png", 10)
749
  if output_dir == "persistent Tor Browser"
750
    output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
751
752
753
754
755
756
    @screen.wait_and_click("GtkTorBrowserPersistentBookmark.png", 10)
    @screen.wait("GtkTorBrowserPersistentBookmarkSelected.png", 10)
    # The output filename (without its extension) is already selected,
    # let's use the keyboard shortcut to focus its field
    @screen.type("n", Sikuli::KeyModifier.ALT)
    @screen.wait("TorBrowserSaveOutputFileSelected.png", 10)
757
  elsif output_dir == "default downloads"
758
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
759
760
  else
    @screen.type(output_dir + '/')
761
762
763
764
765
  end
  # Only the part of the filename before the .html extension can be easily replaced
  # so we have to remove it before typing it into the arget filename entry widget.
  @screen.type(output_file.sub(/[.]html$/, ''))
  @screen.type(Sikuli::Key.ENTER)
766
767
  if should_work
    try_for(10, :msg => "The page was not saved to #{output_dir}/#{output_file}") {
768
      $vm.file_exist?("#{output_dir}/#{output_file}")
769
770
771
772
    }
  else
    @screen.wait("TorBrowserCannotSavePage.png", 10)
  end
773
774
775
776
end

When /^I can print the current page as "([^"]+[.]pdf)" to the (default downloads|persistent Tor Browser) directory$/ do |output_file, output_dir|
  if output_dir == "persistent Tor Browser"
777
    output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
778
  else
779
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
780
781
  end
  @screen.type("p", Sikuli::KeyModifier.CTRL)
782
  @screen.wait("TorBrowserPrintDialog.png", 20)
783
  @screen.wait_and_click("BrowserPrintToFile.png", 10)
784
785
786
787
788
  @screen.wait_and_double_click("TorBrowserPrintOutputFile.png", 10)
  @screen.hide_cursor
  @screen.wait("TorBrowserPrintOutputFileSelected.png", 10)
  # Only the file's basename is selected by double-clicking,
  # so we type only the desired file's basename to replace it
789
  @screen.type(output_dir + '/' + output_file.sub(/[.]pdf$/, '') + Sikuli::Key.ENTER)
anonym's avatar
anonym committed
790
  try_for(30, :msg => "The page was not printed to #{output_dir}/#{output_file}") {
791
    $vm.file_exist?("#{output_dir}/#{output_file}")
792
793
  }
end
794

anonym's avatar
anonym committed
795
Given /^a web server is running on the LAN$/ do
796
797
798
  @web_server_ip_addr = $vmnet.bridge_ip_addr
  @web_server_port = 8000
  @web_server_url = "http://#{@web_server_ip_addr}:#{@web_server_port}"
anonym's avatar
anonym committed
799
800
801
802
803
804
805
806
807
808
809
810
811
812
  web_server_hello_msg = "Welcome to the LAN web server!"

  # I've tested ruby Thread:s, fork(), etc. but nothing works due to
  # various strange limitations in the ruby interpreter. For instance,
  # apparently concurrent IO has serious limits in the thread
  # scheduler (e.g. sikuli's wait() would block WEBrick from reading
  # from its socket), and fork():ing results in a lot of complex
  # cucumber stuff (like our hooks!) ending up in the child process,
  # breaking stuff in the parent process. After asking some supposed
  # ruby pros, I've settled on the following.
  code = <<-EOF
  require "webrick"
  STDOUT.reopen("/dev/null", "w")
  STDERR.reopen("/dev/null", "w")
813
814
  server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
                                   :Port => #{@web_server_port},
anonym's avatar
anonym committed
815
816
817
818
819
820
                                   :DocumentRoot => "/dev/null")
  server.mount_proc("/") do |req, res|
    res.body = "#{web_server_hello_msg}"
  end
  server.start
EOF
821
  add_lan_host(@web_server_ip_addr, @web_server_port)
822
  proc = IO.popen(['ruby', '-e', code])
anonym's avatar
anonym committed
823
824
825
826
  try_for(10, :msg => "It seems the LAN web server failed to start") do
    Process.kill(0, proc.pid) == 1
  end

anonym's avatar
anonym committed
827
  add_after_scenario_hook { Process.kill("TERM", proc.pid) }
anonym's avatar
anonym committed
828
829
830
831
832
833
834
835
836
837

  # It seems necessary to actually check that the LAN server is
  # serving, possibly because it isn't doing so reliably when setting
  # up. If e.g. the Unsafe Browser (which *should* be able to access
  # the web server) tries to access it too early, Firefox seems to
  # take some random amount of time to retry fetching. Curl gives a
  # more consistent result, so let's rely on that instead. Note that
  # this forces us to capture traffic *after* this step in case
  # accessing this server matters, like when testing the Tor Browser..
  try_for(30, :msg => "Something is wrong with the LAN web server") do
838
    msg = $vm.execute_successfully("curl #{@web_server_url}",
anonym's avatar
anonym committed
839
                                   :user => LIVE_USER).stdout.chomp
840
    web_server_hello_msg == msg
anonym's avatar
anonym committed
841
842
  end
end
843

anonym's avatar
anonym committed
844
When /^I open a page on the LAN web server in the (.*)$/ do |browser|
845
846
  step "I open the address \"#{@web_server_url}\" in the #{browser}"
end
847

848
849
850
851
852
853
854
855
856
Given /^I wait (?:between (\d+) and )?(\d+) seconds$/ do |min, max|
  if min
    time = rand(max.to_i - min.to_i + 1) + min.to_i
  else
    time = max.to_i
  end
  puts "Slept for #{time} seconds"
  sleep(time)
end
857

anonym's avatar
anonym committed
858
Given /^I (?:re)?start monitoring the AppArmor log of "([^"]+)"$/ do |profile|
859
860
  # AppArmor log entries may be dropped if printk rate limiting is
  # enabled.
861
  $vm.execute_successfully('sysctl -w kernel.printk_ratelimit=0')
anonym's avatar
anonym committed
862
863
  # We will only care about entries for this profile from this time
  # and on.
864
865
  guest_time = $vm.execute_successfully(
    'date +"%Y-%m-%d %H:%M:%S"').stdout.chomp
anonym's avatar
anonym committed
866
867
  @apparmor_profile_monitoring_start ||= Hash.new
  @apparmor_profile_monitoring_start[profile] = guest_time
868
869
end

870
When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"(?: after at most (\d+) seconds)?$/ do |anti_test, profile, file, time|
anonym's avatar
anonym committed
871
872
873
874
875
  assert(@apparmor_profile_monitoring_start &&
         @apparmor_profile_monitoring_start[profile],
         "It seems the profile '#{profile}' isn't being monitored by the " +
         "'I monitor the AppArmor log of ...' step")
  audit_line_regex = 'apparmor="DENIED" operation="open" profile="%s" name="%s"' % [profile, file]
876
  block = Proc.new do
877
878
879
880
881
882
    audit_log = $vm.execute(
      "journalctl --full --no-pager " +
      "--since='#{@apparmor_profile_monitoring_start[profile]}' " +
      "SYSLOG_IDENTIFIER=kernel | grep -w '#{audit_line_regex}'"
    ).stdout.chomp
    assert(audit_log.empty? == (anti_test ? true : false))
883
884
885
886
887
888
889
890
891
    true
  end
  begin
    if time
      try_for(time.to_i) { block.call }
    else
      block.call
    end
  rescue Timeout::Error, Test::Unit::AssertionFailedError => e
anonym's avatar
anonym committed
892
    raise e, "AppArmor has #{anti_test ? "" : "not "}denied the operation"
893
894
  end
end
895

896
Then /^I force Tor to use a new circuit$/ do
anonym's avatar
anonym committed
897
  force_new_tor_circuit
898
end
899
900
901
902
903
904

When /^I eject the boot medium$/ do
  dev = boot_device
  dev_type = device_info(dev)['ID_TYPE']
  case dev_type
  when 'cd'
905
    $vm.eject_cdrom
906
907
908
909
910
911
912
  when 'disk'
    boot_disk_name = $vm.disk_name(dev)
    $vm.unplug_drive(boot_disk_name)
  else
    raise "Unsupported medium type '#{dev_type}' for boot device '#{dev}'"
  end
end
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928

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
929
930
931
932

def share_host_files(files)
  files = [files] if files.class == String
  assert_equal(Array, files.class)
933
  disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
934
935
  # Let's add some extra space for filesysten overhead etc.
  disk_size += [convert_to_bytes(1, 'MiB'), (disk_size * 0.10).ceil].max
936
  disk = random_alpha_string(10)
937
  step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
  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
953
954
955
956

When /^Tails system time is magically synchronized$/ do
  $vm.host_to_guest_time_sync
end