common_steps.rb 35.5 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
  if $vm.has_network?
34
35
    if $vm.execute("systemctl --quiet is-active tor@default.service").success?
      $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
40
      wait_until_tor_is_working
    end
41
  else
42
    $vm.host_to_guest_time_sync
43
  end
44
45
end

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

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

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

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

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

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)
78
79
80
  end
end

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

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

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

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

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

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

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

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
anonym's avatar
anonym committed
120
  if network_unplugged
121
    step "the network is unplugged"
anonym's avatar
anonym committed
122
123
  else
    step "the network is plugged"
124
  end
125
126
  step "I start the computer"
  step "the computer boots Tails"
127
128
  if do_login
    step "I log in to a new session"
anonym's avatar
anonym committed
129
    if network_unplugged
130
131
      step "all notifications have disappeared"
    else
anonym's avatar
anonym committed
132
      step "Tor is ready"
133
      step "all notifications have disappeared"
anonym's avatar
anonym committed
134
      step "available upgrades have been checked"
135
    end
136
  end
137
138
end

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

161
When /^I power off the computer$/ do
162
  assert($vm.is_running?,
163
         "Trying to power off an already powered off VM")
164
  $vm.power_off
165
166
167
168
169
170
171
172
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
173
  $vm.destroy_and_undefine
174
175
end

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

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

anonym's avatar
anonym committed
194
Given /^Tails is at the boot menu's cmdline( after rebooting)?$/ do |reboot|
anonym's avatar
anonym committed
195
  boot_timeout = 3*60
196
197
198
199
200
201
202
203
  # 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.
204
  tab_spammer_code = <<-EOF
anonym's avatar
anonym committed
205
206
207
208
209
210
211
212
213
214
215
216
    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
217
  EOF
218
  # Our UEFI firmware (OVMF) has the interesting "feature" that pressing
219
220
221
  # 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.
222
  dealt_with_uefi_setup = false
anonym's avatar
anonym committed
223
224
  # The below code is not completely reliable, so we might have to
  # retry by rebooting.
225
226
  try_for(boot_timeout) do
    begin
anonym's avatar
anonym committed
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
231
232
233
234
        Process.kill("TSTP", tab_spammer.pid)
        @screen.type(Sikuli::Key.ENTER)
        Process.kill("CONT", tab_spammer.pid)
        dealt_with_uefi_setup = true
      end
235
      @screen.wait(boot_menu_cmdline_image, 15)
236
    rescue FindFailed => e
237
238
239
240
      debug_log('We missed the boot menu before we could deal with it, ' +
                'resetting...')
      dealt_with_uefi_setup = false
      $vm.reset
241
      raise e
242
243
244
    ensure
      Process.kill("TERM", tab_spammer.pid)
      tab_spammer.close
245
    end
246
    true
247
  end
248
end
249

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

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

278
279
280
281
282
283
284
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()
285
286
end

287
Given /^I enable the specific Tor configuration option$/ do
288
  open_greeter_additional_settings()
289
290
291
  @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
  @screen.wait_and_click("TailsGreeterSpecificTorConfiguration.png", 10)
  @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
292
293
end

294
Given /^I set an administration password$/ do
295
296
  open_greeter_additional_settings()
  @screen.wait_and_click("TailsGreeterAdminPassword.png", 20)
297
298
299
  @screen.type(@sudo_password)
  @screen.type(Sikuli::Key.TAB)
  @screen.type(@sudo_password)
300
  @screen.type(Sikuli::Key.ENTER)
301
302
end

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

312
Given /^the Tails desktop is ready$/ do
313
  desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
314
  @screen.wait(desktop_started_picture, 180)
315
316
317
318
319
320
321
322
323
  # Workaround #13461 by restarting nautilus-desktop
  # if Desktop icons are not visible
  begin
    @screen.wait("DesktopTailsDocumentation.png", 30)
  rescue FindFailed
    step 'I kill the process "nautilus-desktop"'
    $vm.spawn('nautilus-desktop', user: LIVE_USER)
    @screen.wait("DesktopTailsDocumentation.png", 30)
  end
324
325
326
327
328
329
330
  # 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
  )
331
332
333
334
335
  # We need to enable the accessibility toolkit for dogtail.
  $vm.execute_successfully(
    'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
    :user => LIVE_USER,
  )
336
337
end

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

Given /^Tor is ready$/ do
  step "Tor has built a circuit"
  step "the time has synced"
352
353
354
  begin
    try_for(30) { $vm.execute('systemctl is-system-running').success? }
  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}"
358
  end
359
360
361
362
363
364
end

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

365
366
367
class TimeSyncingError < StandardError
end

368
Given /^the time has synced$/ do
369
370
  begin
    ["/run/tordate/done", "/run/htpdate/success"].each do |file|
anonym's avatar
anonym committed
371
      try_for(300) { $vm.execute("test -e #{file}").success? }
372
    end
anonym's avatar
anonym committed
373
  rescue
374
    File.open("#{$config["TMPDIR"]}/log.htpdate", 'w') do |file|
anonym's avatar
anonym committed
375
      file.write($vm.execute('cat /var/log/htpdate.log').stdout)
376
    end
anonym's avatar
anonym committed
377
378
    raise TimeSyncingError.new("Time syncing failed")
  end
379
380
end

381
382
Given /^available upgrades have been checked$/ do
  try_for(300) {
383
    $vm.execute("test -e '/run/tails-upgrader/checked_upgrades'").success?
384
385
386
  }
end

387
When /^I start the Tor Browser( in offline mode)?$/ do |offline|
anonym's avatar
anonym committed
388
  step 'I start "Tor Browser" via GNOME Activities Overview'
389
390
391
  if offline
    offline_prompt = Dogtail::Application.new('zenity')
                     .dialog('Tor is not ready')
392
393
394
    start_button = offline_prompt.button('Start Tor Browser')
    start_button.grabFocus
    start_button.click
395
  end
396
  step "the Tor Browser has started#{offline}"
397
398
399
  if offline
    step 'the Tor Browser shows the "The proxy server is refusing connections" error'
  end
400
401
end

402
Given /^the Tor Browser (?:has started|starts)( in offline mode)?$/ do |offline|
anonym's avatar
anonym committed
403
  try_for(60) do
404
    @torbrowser = Dogtail::Application.new('Firefox')
405
    @torbrowser.child?(roleName: 'frame', recursive: false)
anonym's avatar
anonym committed
406
  end
407
408
end

intrigeri's avatar
intrigeri committed
409
Given /^the Tor Browser loads the (startup page|Tails homepage|Tails roadmap)$/ do |page|
410
411
  case page
  when "startup page"
412
    title = 'Tails'
intrigeri's avatar
intrigeri committed
413
414
  when "Tails homepage"
    title = 'Tails - Privacy for anyone anywhere'
415
  when "Tails roadmap"
416
    title = 'Roadmap - Tails - Tails Ticket Tracker'
417
418
419
  else
    raise "Unsupported page: #{page}"
  end
anonym's avatar
anonym committed
420
  step "\"#{title}\" has loaded in the Tor Browser"
421
422
end

423
When /^I request a new identity using Torbutton$/ do
424
425
  @torbrowser.child('Tor Browser', roleName: 'push button').click
  @torbrowser.child('New Identity', roleName: 'push button').click
426
427
428
429
430
431
432
end

When /^I acknowledge Torbutton's New Identity confirmation prompt$/ do
  @screen.wait('GnomeQuestionDialogIcon.png', 30)
  step 'I type "y"'
end

433
434
435
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
436
  step 'the Tor Browser shows the "The proxy server is refusing connections" error'
437
438
439
440
441
442
443
444
  @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)
445
446
end

447
Given /^all notifications have disappeared$/ do
448
449
450
  # These magic coordinates always locates GNOME's clock in the top
  # bar, which when clicked opens the calendar.
  x, y = 512, 10
451
  gnome_shell = Dogtail::Application.new('gnome-shell')
452
453
  retry_action(10, recovery_proc: Proc.new { @screen.type(Sikuli::Key.ESC) }) do
    @screen.click_point(x, y)
454
    unless gnome_shell.child?('No Notifications', roleName: 'label')
455
456
      @screen.click('GnomeCloseAllNotificationsButton.png')
    end
457
    gnome_shell.child?('No Notifications', roleName: 'label')
458
  end
459
  @screen.type(Sikuli::Key.ESC)
Tails developers's avatar
Tails developers committed
460
461
end

462
Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, image, time|
463
464
465
  if negation
    @screen.waitVanish(image, time.to_i)
  else
466
467
    @screen.wait(image, time.to_i)
  end
468
469
470
end

Then /^all Internet traffic has only flowed through Tor$/ do
471
  allowed_hosts = allowed_hosts_under_tor_enforcement
anonym's avatar
anonym committed
472
  assert_all_connections(@sniffer.pcap_file) do |c|
473
    allowed_hosts.include?({ address: c.daddr, port: c.dport })
474
  end
475
end
476

477
478
Given /^I enter the sudo password in the pkexec prompt$/ do
  step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
479
480
end

481
482
483
def deal_with_polkit_prompt(password, opts = {})
  opts[:expect_success] ||= true
  image = 'PolicyKitAuthPrompt.png'
484
  @screen.wait(image, 60)
485
  @screen.type(password)
486
  @screen.type(Sikuli::Key.ENTER)
487
488
489
490
491
  if opts[:expect_success]
    @screen.waitVanish(image, 20)
  else
    @screen.wait('PolicyKitAuthFailure.png', 20)
  end
492
493
494
end

Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
495
  deal_with_polkit_prompt(password)
496
497
end

kytv's avatar
kytv committed
498
499
500
501
502
503
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
504
505
end

506
Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time|
Tails developers's avatar
Tails developers committed
507
508
  try_for(time.to_i, :msg => "Process '#{process}' is not running after " +
                             "waiting for #{time} seconds") do
509
    $vm.has_process?(process)
510
511
512
  end
end

513
514
515
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
516
    not $vm.has_process?(process)
517
518
519
  end
end

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)
524
  }
525
526
end

527
Then /^Tails eventually (shuts down|restarts)$/ do |mode|
intrigeri's avatar
intrigeri committed
528
529
530
531
  try_for(3*60) do
    if mode == 'restarts'
      @screen.find('TailsGreeter.png')
      true
532
    else
intrigeri's avatar
intrigeri committed
533
      ! $vm.is_running?
534
    end
535
536
537
538
  end
end

Given /^I shutdown Tails and wait for the computer to power off$/ do
539
  $vm.spawn("poweroff")
540
541
542
543
  step 'Tails eventually shuts down'
end

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

554
When /^I warm reboot the computer$/ do
555
  $vm.spawn("reboot")
556
557
end

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

anonym's avatar
anonym committed
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")
570
end
571

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

599
Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
600
601
602
603
  $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
604
end
605

606
607
608
When /^I start and focus GNOME Terminal$/ do
  step 'I start "GNOME Terminal" via GNOME Activities Overview'
  @screen.wait('GnomeTerminalWindow.png', 40)
609
610
end

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

620
621
622
623
624
625
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"
  ) {
626
    $vm.file_exist?(file)
627
628
629
630
  }
end

When /^the file "([^"]+)" does not exist$/ do |file|
631
  assert(! ($vm.file_exist?(file)))
632
633
634
end

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

When /^the directory "([^"]+)" does not exist$/ do |directory|
639
  assert(! ($vm.directory_exist?(directory)))
640
641
642
end

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}")
645
end
646

647
def is_persistent?(app)
648
  conf = get_persistence_presets_config(true)["#{app}"]
649
650
  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
651
  c.success? and c.stdout.chomp != "aufs"
652
653
654
655
656
657
658
659
660
661
662
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

anonym's avatar
anonym committed
663
Given /^I start "([^"]+)" via GNOME Activities Overview$/ do |app_name|
664
665
666
667
668
669
670
671
672
673
674
  # Search disambiguations: below we assume that there is only one
  # result, since multiple results introduces a race that leads to a
  # non-deterministic choice (at least under load). To make the life
  # easier for users of this step, let's collect workarounds here.
  case app_name
  when 'GNOME Terminal'
    # "GNOME Terminal" and "Terminal" shows both the (non-Root)
    # "Terminal" and "Root Terminal" search results, so let's use a
    # keyword only found in the former's .desktop file.
    app_name = 'commandline'
  end
675
676
677
  @screen.wait('GnomeApplicationsMenu.png', 10)
  $vm.execute_successfully('xdotool key Super', user: LIVE_USER)
  @screen.wait('GnomeActivitiesOverview.png', 10)
678
679
680
681
682
683
684
  # Trigger startup of search providers
  @screen.type(app_name[0])
  # Give search providers some time to start (#13469#note-5) otherwise
  # our search sometimes returns no results at all.
  sleep 1
  # Type the rest of the search query
  @screen.type(app_name[1..-1])
685
  @screen.type(Sikuli::Key.ENTER, Sikuli::KeyModifier.CTRL)
686
end
687
688
689
690
691
692

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

When /^I press the "([^"]+)" key$/ do |key|
693
694
695
  begin
    @screen.type(eval("Sikuli::Key.#{key}"))
  rescue RuntimeError
Tails developers's avatar
Tails developers committed
696
    raise "unsupported key #{key}"
697
698
  end
end
699
700
701
702

Then /^the (amnesiac|persistent) Tor Browser directory (exists|does not exist)$/ do |persistent_or_not, mode|
  case persistent_or_not
  when "amnesiac"
703
    dir = "/home/#{LIVE_USER}/Tor Browser"
704
  when "persistent"
705
    dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
  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

722
Then /^there is no GNOME bookmark for the persistent Tor Browser directory$/ do
723
724
725
726
727
  try_for(65) do
    @screen.wait_and_click('GnomePlaces.png', 10)
    @screen.wait("GnomePlacesWithoutTorBrowserPersistent.png", 10)
    @screen.type(Sikuli::Key.ESC)
  end
728
729
end

730
def pulseaudio_sink_inputs
731
  pa_info = $vm.execute_successfully('pacmd info', :user => LIVE_USER).stdout
732
733
734
735
736
737
738
739
740
741
  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

742
743
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'
744
  info = xul_application_info('Tor Browser')
745
746
  # Sometimes the double-click is lost (#12131).
  retry_action(10) do
747
748
    @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'
749
  end
750
751
end

752
753
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
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
759
760
761
762
763
    @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)
764
  elsif output_dir == "default downloads"
765
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
766
767
  else
    @screen.type(output_dir + '/')
768
769
770
771
772
  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)
773
  if should_work
774
    try_for(20, :msg => "The page was not saved to #{output_dir}/#{output_file}") {
775
      $vm.file_exist?("#{output_dir}/#{output_file}")
776
777
778
779
    }
  else
    @screen.wait("TorBrowserCannotSavePage.png", 10)
  end
780
781
782
783
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"
784
    output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
785
  else
786
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
787
788
  end
  @screen.type("p", Sikuli::KeyModifier.CTRL)
anonym's avatar
anonym committed
789
790
  print_dialog = @torbrowser.child('Print', roleName: 'dialog')
  print_dialog.child('Print to File', 'table cell').click
791
792
793
794
795
796
797
  print_dialog.child('~/Tor Browser/output.pdf', roleName: 'push button').click()
  @screen.wait("Gtk3PrintFileDialog.png", 10)
  # Only the file's basename is selected when the file selector dialog opens,
  # so we type only the desired file's basename to replace it
  $vm.set_clipboard(output_dir + '/' + output_file.sub(/[.]pdf$/, ''))
  @screen.type('v', Sikuli::KeyModifier.CTRL)
  @screen.type(Sikuli::Key.ENTER)
798
  @screen.wait_and_click("Gtk3PrintButton.png", 10)
anonym's avatar
anonym committed
799
  try_for(30, :msg => "The page was not printed to #{output_dir}/#{output_file}") {
800
    $vm.file_exist?("#{output_dir}/#{output_file}")
801
802
  }
end
803

anonym's avatar
anonym committed
804
Given /^a web server is running on the LAN$/ do
805
806
807
  @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
808
809
810
811
812
813
814
815
816
817
818
819
820
821
  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")
822
823
  server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
                                   :Port => #{@web_server_port},
anonym's avatar
anonym committed
824
825
826
827
828
829
                                   :DocumentRoot => "/dev/null")
  server.mount_proc("/") do |req, res|
    res.body = "#{web_server_hello_msg}"
  end
  server.start
EOF
830
  add_lan_host(@web_server_ip_addr, @web_server_port)
831
  proc = IO.popen(['ruby', '-e', code])
anonym's avatar
anonym committed
832
833
834
835
  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
836
  add_after_scenario_hook { Process.kill("TERM", proc.pid) }
anonym's avatar
anonym committed
837
838
839
840
841
842
843
844
845
846

  # 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
847
    msg = $vm.execute_successfully("curl #{@web_server_url}",
anonym's avatar
anonym committed
848
                                   :user => LIVE_USER).stdout.chomp
849
    web_server_hello_msg == msg
anonym's avatar
anonym committed
850
851
  end
end
852

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

857
858
859
860
861
862
863
864
865
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
866

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

879
When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"$/ do |anti_test, profile, file|
anonym's avatar
anonym committed
880
881
882
883
884
  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]
885
  begin
886
887
888
889
890
891
892
893
894
    try_for(10, { :delay => 1 }) {
      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))
      true
    }
895
  rescue Timeout::Error, Test::Unit::AssertionFailedError => e
anonym's avatar
anonym committed
896
    raise e, "AppArmor has #{anti_test ? "" : "not "}denied the operation"
897
898
  end
end
899

900
Then /^I force Tor to use a new circuit$/ do
anonym's avatar
anonym committed
901
  force_new_tor_circuit
902
end
903
904
905
906
907
908

When /^I eject the boot medium$/ do
  dev = boot_device
  dev_type = device_info(dev)['ID_TYPE']
  case dev_type
  when 'cd'
909
    $vm.eject_cdrom
910
911
912
913
914
915
916
  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
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932

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
933
934
935
936

def share_host_files(files)
  files = [files] if files.class == String
  assert_equal(Array, files.class)
937
  disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
intrigeri's avatar
intrigeri committed
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