common_steps.rb 33 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
27
28
# This helper requires that the notification image is the one shown in
# the notification applet's list, not the notification pop-up.
def robust_notification_wait(notification_image, time_to_wait)
29
30
  error_msg = "Didn't not manage to open the notification applet"
  wait_start = Time.now
31
32
  try_for(time_to_wait, :delay => 0, :msg => error_msg) do
    @screen.hide_cursor
33
34
    @screen.click("GnomeNotificationApplet.png")
    @screen.wait("GnomeNotificationAppletOpened.png", 10)
35
36
37
38
39
40
  end

  error_msg = "Didn't not see notification '#{notification_image}'"
  time_to_wait -= (Time.now - wait_start).ceil
  try_for(time_to_wait, :delay => 0, :msg => error_msg) do
    found = false
41
    entries = @screen.findAll("GnomeNotificationEntry.png")
42
43
44
45
    while(entries.hasNext) do
      entry = entries.next
      @screen.hide_cursor
      @screen.click(entry)
46
      close_entry = @screen.wait("GnomeNotificationEntryClose.png", 10)
47
48
49
50
51
52
53
      if @screen.exists(notification_image)
        found = true
        @screen.click(close_entry)
        break
      else
        @screen.click(entry)
      end
54
    end
55
    found
56
  end
57

58
  # Close the notification applet
intrigeri's avatar
intrigeri committed
59
  @screen.type(Sikuli::Key.ESC)
60
  @screen.waitVanish('GnomeNotificationAppletOpened.png', 10)
61
62
end

63
def post_snapshot_restore_hook
64
  $vm.wait_until_remote_shell_is_up
65
  post_vm_start_hook
66

67
68
69
  # 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.
70
  if $vm.has_network?
71
72
    if $vm.execute("systemctl --quiet is-active tor@default.service").success?
      $vm.execute("systemctl stop tor@default.service")
73
      $vm.execute("rm -f /var/log/tor/log")
74
      $vm.execute("systemctl --no-block restart tails-tor-has-bootstrapped.target")
75
      $vm.host_to_guest_time_sync
76
      $vm.spawn("restart-tor")
77
      wait_until_tor_is_working
78
      if $vm.file_content('/proc/cmdline').include?(' i2p')
79
        $vm.execute_successfully('/usr/local/sbin/tails-i2p stop')
kytv's avatar
kytv committed
80
81
82
        # we "killall tails-i2p" to prevent multiple
        # copies of the script from running
        $vm.execute_successfully('killall tails-i2p')
83
84
        $vm.spawn('/usr/local/sbin/tails-i2p start')
      end
85
    end
86
  else
87
    $vm.host_to_guest_time_sync
88
89
90
  end
end

91
Given /^a computer$/ do
92
93
  $vm.destroy_and_undefine if $vm
  $vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
94
95
end

96
Given /^the computer has (\d+) ([[:alpha:]]+) of RAM$/ do |size, unit|
97
  $vm.set_ram_size(size, unit)
98
99
end

100
Given /^the computer is set to boot from the Tails DVD$/ do
101
  $vm.set_cdrom_boot(TAILS_ISO)
102
103
end

104
Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name|
105
  $vm.set_disk_boot(name, type.downcase)
106
107
end

108
Given /^I (temporarily )?create an? (\d+) ([[:alpha:]]+) disk named "([^"]+)"$/ do |temporary, size, unit, name|
109
  $vm.storage.create_new_disk(name, {:size => size, :unit => unit,
110
                                     :type => "qcow2"})
111
  add_after_scenario_hook { $vm.storage.delete_volume(name) } if temporary
112
113
end

114
Given /^I plug (.+) drive "([^"]+)"$/ do |bus, name|
115
116
  $vm.plug_drive(name, bus.downcase)
  if $vm.is_running?
117
118
119
120
121
    step "drive \"#{name}\" is detected by Tails"
  end
end

Then /^drive "([^"]+)" is detected by Tails$/ do |name|
122
  raise "Tails is not running" unless $vm.is_running?
123
  try_for(10, :msg => "Drive '#{name}' is not detected by Tails") do
124
    $vm.disk_detected?(name)
125
126
127
  end
end

128
Given /^the network is plugged$/ do
129
  $vm.plug_network
130
131
132
end

Given /^the network is unplugged$/ do
133
  $vm.unplug_network
134
135
end

136
137
138
139
140
Given /^the network connection is ready(?: within (\d+) seconds)?$/ do |timeout|
  timeout ||= 30
  try_for(timeout.to_i) { $vm.has_network? }
end

141
Given /^the hardware clock is set to "([^"]*)"$/ do |time|
142
  $vm.set_hardware_clock(DateTime.parse(time).to_time)
143
144
end

145
Given /^I capture all network traffic$/ do
146
  @sniffer = Sniffer.new("sniffer", $vmnet)
147
  @sniffer.capture
148
149
150
151
  add_after_scenario_hook do
    @sniffer.stop
    @sniffer.clear
  end
152
153
154
155
156
157
158
end

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

When /^I start the computer$/ do
159
  assert(!$vm.is_running?,
160
         "Trying to start a VM that is already running")
161
  $vm.start
162
  post_vm_start_hook
163
164
end

165
Given /^I start Tails( from DVD)?( with network unplugged)?( and I login)?$/ do |dvd_boot, network_unplugged, do_login|
166
167
  step "the computer is set to boot from the Tails DVD" if dvd_boot
  if network_unplugged.nil?
168
169
170
171
    step "the network is plugged"
  else
    step "the network is unplugged"
  end
172
173
  step "I start the computer"
  step "the computer boots Tails"
174
175
176
177
178
179
180
181
182
  if do_login
    step "I log in to a new session"
    if network_unplugged.nil?
      step "Tor is ready"
      step "all notifications have disappeared"
      step "available upgrades have been checked"
    else
      step "all notifications have disappeared"
    end
183
  end
184
185
end

186
Given /^I start Tails from (.+?) drive "(.+?)"(| with network unplugged)( and I login(| with(| read-only) persistence enabled))?$/ do |drive_type, drive_name, network_unplugged, do_login, persistence_on, persistence_ro|
187
  step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\""
188
189
190
191
192
  if network_unplugged.empty?
    step "the network is plugged"
  else
    step "the network is unplugged"
  end
193
194
  step "I start the computer"
  step "the computer boots Tails"
195
196
197
  if do_login
    if ! persistence_on.empty?
      if persistence_ro.empty?
198
        step "I enable persistence"
199
      else
200
        step "I enable read-only persistence"
201
202
203
204
205
206
207
      end
    end
    step "I log in to a new session"
    if network_unplugged.empty?
      step "Tor is ready"
      step "all notifications have disappeared"
      step "available upgrades have been checked"
208
    else
209
      step "all notifications have disappeared"
210
211
    end
  end
212
213
end

214
When /^I power off the computer$/ do
215
  assert($vm.is_running?,
216
         "Trying to power off an already powered off VM")
217
  $vm.power_off
218
219
220
221
222
223
224
225
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
226
  $vm.destroy_and_undefine
227
228
end

229
def boot_menu_cmdline_image
230
231
  case @os_loader
  when "UEFI"
232
    'TailsBootMenuKernelCmdlineUEFI.png'
233
  else
234
    'TailsBootMenuKernelCmdline.png'
235
236
  end
end
237

anonym's avatar
anonym committed
238
def boot_menu_tab_msg_image
239
240
  case @os_loader
  when "UEFI"
241
    'TailsBootSplashTabMsgUEFI.png'
242
  else
243
    'TailsBootSplashTabMsg.png'
244
  end
245
246
end

anonym's avatar
anonym committed
247
248
def memory_wipe_timeout
  nr_gigs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
249
  nr_gigs_of_ram*30
anonym's avatar
anonym committed
250
end
251

anonym's avatar
anonym committed
252
Given /^Tails is at the boot menu's cmdline( after rebooting)?$/ do |reboot|
anonym's avatar
anonym committed
253
  boot_timeout = 3*60
254
  # We need some extra time for memory wiping if rebooting
anonym's avatar
anonym committed
255
  boot_timeout += memory_wipe_timeout if reboot
256
257
258
259
260
261
262
263
  # 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.
264
  tab_spammer_code = <<-EOF
anonym's avatar
anonym committed
265
266
267
268
269
270
271
272
273
274
275
276
    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
277
  EOF
278
  # Our UEFI firmware (OVMF) has the interesting "feature" that pressing
279
280
281
  # 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.
282
  dealt_with_uefi_setup = false
anonym's avatar
anonym committed
283
284
  # The below code is not completely reliable, so we might have to
  # retry by rebooting.
285
286
  try_for(boot_timeout) do
    begin
anonym's avatar
anonym committed
287
      tab_spammer = IO.popen(['ruby', '-e', tab_spammer_code])
288
      if not(dealt_with_uefi_setup) && @os_loader == 'UEFI'
289
        @screen.wait('UEFIFirmwareSetup.png', 30)
290
291
292
293
294
        Process.kill("TSTP", tab_spammer.pid)
        @screen.type(Sikuli::Key.ENTER)
        Process.kill("CONT", tab_spammer.pid)
        dealt_with_uefi_setup = true
      end
295
      @screen.wait(boot_menu_cmdline_image, 15)
296
    rescue FindFailed => e
297
298
299
300
      debug_log('We missed the boot menu before we could deal with it, ' +
                'resetting...')
      dealt_with_uefi_setup = false
      $vm.reset
301
      retry
302
303
304
    ensure
      Process.kill("TERM", tab_spammer.pid)
      tab_spammer.close
305
    end
306
  end
307
end
308

309
Given /^the computer (re)?boots Tails$/ do |reboot|
anonym's avatar
anonym committed
310
  step "Tails is at the boot menu's cmdline" + (reboot ? ' after rebooting' : '')
anonym's avatar
anonym committed
311
  @screen.type(" autotest_never_use_this_option blacklist=psmouse #{@boot_options}" +
312
               Sikuli::Key.ENTER)
313
  @screen.wait('TailsGreeter.png', 5*60)
314
  $vm.wait_until_remote_shell_is_up
anonym's avatar
anonym committed
315
  step 'I configure Tails to use a simulated Tor network'
316
317
end

318
319
320
321
Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
  case lang
  when 'German'
    @language = "German"
322
323
324
    @screen.wait_and_click('TailsGreeterLanguage.png', 10)
    @screen.wait_and_click("TailsGreeterLanguage#{@language}.png", 10)
    @screen.wait_and_click("TailsGreeterLoginButton#{@language}.png", 10)
325
326
327
328
329
  when ''
    @screen.wait_and_click('TailsGreeterLoginButton.png', 10)
  else
    raise "Unsupported language: #{lang}"
  end
330
  step 'Tails Greeter has applied all settings'
331
  step 'the Tails desktop is ready'
332
333
end

334
335
Given /^I enable more Tails Greeter options$/ do
  match = @screen.find('TailsGreeterMoreOptions.png')
336
  @screen.click(match.getCenter.offset(match.w/2, match.h*2))
intrigeri's avatar
intrigeri committed
337
  @screen.wait_and_click('TailsGreeterForward.png', 20)
338
339
340
  @screen.wait('TailsGreeterLoginButton.png', 20)
end

341
342
343
344
Given /^I enable the specific Tor configuration option$/ do
  @screen.click('TailsGreeterTorConf.png')
end

345
Given /^I set an administration password$/ do
346
  @screen.wait("TailsGreeterAdminPassword.png", 20)
347
348
349
  @screen.type(@sudo_password)
  @screen.type(Sikuli::Key.TAB)
  @screen.type(@sudo_password)
350
351
end

352
353
354
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
355
  try_for(120) {
356
357
    $vm.execute_successfully("loginctl").stdout
      .match(/^\s*\S+\s+\d+\s+#{LIVE_USER}\s+seat\d+\s*$/) != nil
358
  }
359
360
end

361
362
363
364
365
366
367
def florence_keyboard_is_visible
  $vm.execute(
    "xdotool search --all --onlyvisible --maxdepth 1 --classname 'Florence'",
    :user => LIVE_USER,
  ).success?
end

368
Given /^the Tails desktop is ready$/ do
369
370
371
  desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
  # We wait for the Florence icon to be displayed to ensure reliable systray icon clicking.
  @screen.wait("GnomeSystrayFlorence.png", 180)
372
  @screen.wait(desktop_started_picture, 180)
373
374
375
376
377
378
379
  # 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
  )
380
381
382
383
384
  # We need to enable the accessibility toolkit for dogtail.
  $vm.execute_successfully(
    'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
    :user => LIVE_USER,
  )
385
386
387
388
389
390
391
  # Sometimes the Florence window is not hidden on startup (#11398).
  # Whenever that's the case, hide it ourselves and verify that it vanishes.
  # I could not find that window using Accerciser, so I'm not using dogtail;
  # and it doesn't feel worth it to add an image and use Sikuli, since we can
  # instead do this programmatically with xdotool.
  if florence_keyboard_is_visible
    @screen.click("GnomeSystrayFlorence.png")
anonym's avatar
anonym committed
392
    try_for(5, delay: 0.1) { ! florence_keyboard_is_visible }
393
  end
394
395
end

396
When /^I see the 'Tor is ready' notification$/ do
397
  robust_notification_wait('TorIsReadyNotification.png', 300)
398
end
399
400
401
402

Given /^Tor is ready$/ do
  step "Tor has built a circuit"
  step "the time has synced"
403
404
405
406
  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
407
408
409
410
411
412
413
414
end

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

Given /^the time has synced$/ do
  ["/var/run/tordate/done", "/var/run/htpdate/success"].each do |file|
415
    try_for(300) { $vm.execute("test -e #{file}").success? }
416
417
418
  end
end

419
420
Given /^available upgrades have been checked$/ do
  try_for(300) {
421
    $vm.execute("test -e '/var/run/tails-upgrader/checked_upgrades'").success?
422
423
424
  }
end

425
Given /^the Tor Browser has started$/ do
426
  tor_browser_picture = "TorBrowserWindow.png"
427
  @screen.wait(tor_browser_picture, 60)
428
429
end

430
Given /^the Tor Browser (?:has started and )?load(?:ed|s) the (startup page|Tails roadmap)$/ do |page|
431
432
  case page
  when "startup page"
433
    title = 'Tails - News'
434
  when "Tails roadmap"
anonym's avatar
anonym committed
435
    title = 'Roadmap - Tails - RiseupLabs Code Repository'
436
437
438
  else
    raise "Unsupported page: #{page}"
  end
439
  step "the Tor Browser has started"
anonym's avatar
anonym committed
440
  step "\"#{title}\" has loaded in the Tor Browser"
441
442
end

443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
Given /^the Tor Browser has started in offline mode$/ do
  @screen.wait("TorBrowserOffline.png", 60)
end

Given /^I add a bookmark to eff.org in the Tor Browser$/ do
  url = "https://www.eff.org"
  step "I open the address \"#{url}\" in the Tor Browser"
  @screen.wait("TorBrowserOffline.png", 5)
  @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)
459
460
end

461
Given /^all notifications have disappeared$/ do
462
463
464
  begin
    @screen.click("GnomeNotificationApplet.png")
  rescue FindFailed
465
    # No notifications, so we're done here.
466
    next
467
  end
468
  @screen.wait("GnomeNotificationAppletOpened.png", 10)
469
  begin
470
    entries = @screen.findAll("GnomeNotificationEntry.png")
471
472
473
474
    while(entries.hasNext) do
      entry = entries.next
      @screen.hide_cursor
      @screen.click(entry)
475
      @screen.wait_and_click("GnomeNotificationEntryClose.png", 10)
476
477
478
    end
  rescue FindFailed
    # No notifications, so we're good to go.
479
  end
480
481
  @screen.hide_cursor
  # Click anywhere to close the notification applet
482
  @screen.click("GnomeApplicationsMenu.png")
483
  @screen.hide_cursor
Tails developers's avatar
Tails developers committed
484
485
end

486
487
488
489
490
491
492
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
493
494
495
end

Then /^all Internet traffic has only flowed through Tor$/ do
496
  allowed_hosts = allowed_hosts_under_tor_enforcement
anonym's avatar
anonym committed
497
  assert_all_connections(@sniffer.pcap_file) do |c|
498
    allowed_hosts.include?({ address: c.daddr, port: c.dport })
499
  end
500
end
501

502
503
Given /^I enter the sudo password in the pkexec prompt$/ do
  step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
504
505
end

506
507
def deal_with_polkit_prompt (image, password)
  @screen.wait(image, 60)
508
  @screen.type(password)
509
  @screen.type(Sikuli::Key.ENTER)
510
511
512
513
514
  @screen.waitVanish(image, 10)
end

Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
  deal_with_polkit_prompt('PolicyKitAuthPrompt.png', password)
515
516
end

kytv's avatar
kytv committed
517
518
519
520
521
522
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
523
524
end

525
Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time|
Tails developers's avatar
Tails developers committed
526
527
  try_for(time.to_i, :msg => "Process '#{process}' is not running after " +
                             "waiting for #{time} seconds") do
528
    $vm.has_process?(process)
529
530
531
  end
end

532
533
534
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
535
    not $vm.has_process?(process)
536
537
538
  end
end

539
Given /^I kill the process "([^"]+)"$/ do |process|
540
  $vm.execute("killall #{process}")
541
  try_for(10, :msg => "Process '#{process}' could not be killed") {
542
    !$vm.has_process?(process)
543
  }
544
545
end

546
547
548
Then /^Tails eventually (shuts down|restarts)$/ do |mode|
  nr_gibs_of_ram = convert_from_bytes($vm.get_ram_size_in_bytes, 'GiB').ceil
  timeout = nr_gibs_of_ram*5*60
549
  # Work around Tails bug #11786, where something goes wrong when we
550
551
552
  # kexec to the new kernel for memory wiping and gets dropped to a
  # BusyBox shell instead.
  try_for(timeout) do
553
554
    if @screen.existsAny(['TailsBug11786a.png', 'TailsBug11786b.png'])
      puts "We were hit by bug #11786: memory wiping got stuck"
555
556
557
558
559
560
561
      if mode == 'restarts'
        $vm.reset
      else
        $vm.power_off
      end
    else
      if mode == 'restarts'
anonym's avatar
anonym committed
562
        @screen.find('TailsGreeter.png')
563
564
565
566
567
        true
      else
        ! $vm.is_running?
      end
    end
568
569
570
571
  end
end

Given /^I shutdown Tails and wait for the computer to power off$/ do
572
  $vm.spawn("poweroff")
573
574
575
576
  step 'Tails eventually shuts down'
end

When /^I request a shutdown using the emergency shutdown applet$/ do
577
  @screen.hide_cursor
578
  @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
579
580
581
582
583
  # 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'
584
  @screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10)
585
586
end

587
When /^I warm reboot the computer$/ do
588
  $vm.spawn("reboot")
589
590
end

591
592
593
When /^I request a reboot using the emergency shutdown applet$/ do
  @screen.hide_cursor
  @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
594
595
596
  # See comment on /^I request a shutdown using the emergency shutdown applet$/
  # that explains why we need to wait.
  step 'I wait 5 seconds'
597
  @screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10)
598
599
600
end

Given /^package "([^"]+)" is installed$/ do |package|
601
  assert($vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?,
602
         "Package '#{package}' is not installed")
603
end
604

605
When /^I start the Tor Browser$/ do
606
  step 'I start "Tor Browser" via the GNOME "Internet" applications menu'
607
end
608

609
610
611
612
613
614
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
kytv's avatar
kytv committed
615
  @screen.wait('GnomeQuestionDialogIcon.png', 30)
616
617
618
  step 'I type "y"'
end

619
620
When /^I start the Tor Browser in offline mode$/ do
  step "I start the Tor Browser"
621
622
  @screen.wait_and_click("TorBrowserOfflinePrompt.png", 10)
  @screen.click("TorBrowserOfflinePromptStart.png")
623
624
end

625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
Given /^I add a wired DHCP NetworkManager connection called "([^"]+)"$/ do |con_name|
  con_content = <<EOF
[802-3-ethernet]
duplex=full

[connection]
id=#{con_name}
uuid=bbc60668-1be0-11e4-a9c6-2f1ce0e75bf1
type=802-3-ethernet
timestamp=1395406011

[ipv6]
method=auto

[ipv4]
method=auto
EOF
  con_content.split("\n").each do |line|
643
    $vm.execute("echo '#{line}' >> /tmp/NM.#{con_name}")
644
  end
645
  con_file = "/etc/NetworkManager/system-connections/#{con_name}"
646
647
  $vm.execute("install -m 0600 '/tmp/NM.#{con_name}' '#{con_file}'")
  $vm.execute_successfully("nmcli connection load '#{con_file}'")
648
  try_for(10) {
649
    nm_con_list = $vm.execute("nmcli --terse --fields NAME connection show").stdout
650
651
652
653
654
    nm_con_list.split("\n").include? "#{con_name}"
  }
end

Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
655
656
657
658
  $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
659
end
660
661

When /^I start and focus GNOME Terminal$/ do
662
  step 'I start "Terminal" via the GNOME "Utilities" applications menu'
intrigeri's avatar
intrigeri committed
663
  @screen.wait('GnomeTerminalWindow.png', 40)
664
665
666
end

When /^I run "([^"]+)" in GNOME Terminal$/ do |command|
667
  if !$vm.has_process?("gnome-terminal-server")
668
669
670
671
    step "I start and focus GNOME Terminal"
  else
    @screen.wait_and_click('GnomeTerminalWindow.png', 20)
  end
672
673
  @screen.type(command + Sikuli::Key.ENTER)
end
674

675
676
677
678
679
680
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"
  ) {
681
    $vm.file_exist?(file)
682
683
684
685
  }
end

When /^the file "([^"]+)" does not exist$/ do |file|
686
  assert(! ($vm.file_exist?(file)))
687
688
689
end

When /^the directory "([^"]+)" exists$/ do |directory|
690
  assert($vm.directory_exist?(directory))
691
692
693
end

When /^the directory "([^"]+)" does not exist$/ do |directory|
694
  assert(! ($vm.directory_exist?(directory)))
695
696
697
end

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

702
703
def is_persistent?(app)
  conf = get_persistence_presets(true)["#{app}"]
704
705
  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
706
  c.success? and c.stdout.chomp != "aufs"
707
708
709
710
711
712
713
714
715
716
717
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

718
Given /^I start "([^"]+)" via the GNOME "([^"]+)" applications menu$/ do |app_name, submenu|
anonym's avatar
anonym committed
719
720
721
  app = Dogtail::Application.new('gnome-shell')
  for element in ['Applications', submenu, app_name] do
    app.child(element, roleName: 'label').click
722
  end
723
end
724
725
726
727
728
729

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

When /^I press the "([^"]+)" key$/ do |key|
730
731
732
  begin
    @screen.type(eval("Sikuli::Key.#{key}"))
  rescue RuntimeError
Tails developers's avatar
Tails developers committed
733
    raise "unsupported key #{key}"
734
735
  end
end
736
737
738
739

Then /^the (amnesiac|persistent) Tor Browser directory (exists|does not exist)$/ do |persistent_or_not, mode|
  case persistent_or_not
  when "amnesiac"
740
    dir = "/home/#{LIVE_USER}/Tor Browser"
741
  when "persistent"
742
    dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
  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

759
Then /^there is no GNOME bookmark for the persistent Tor Browser directory$/ do
760
761
762
763
764
  try_for(65) do
    @screen.wait_and_click('GnomePlaces.png', 10)
    @screen.wait("GnomePlacesWithoutTorBrowserPersistent.png", 10)
    @screen.type(Sikuli::Key.ESC)
  end
765
766
end

767
def pulseaudio_sink_inputs
768
  pa_info = $vm.execute_successfully('pacmd info', :user => LIVE_USER).stdout
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
  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

When /^I double-click on the "Tails documentation" link on the Desktop$/ do
  @screen.wait_and_double_click("DesktopTailsDocumentationIcon.png", 10)
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

795
796
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
797
  @screen.type("s", Sikuli::KeyModifier.CTRL)
798
  @screen.wait("TorBrowserSaveDialog.png", 10)
799
  if output_dir == "persistent Tor Browser"
800
    output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
801
802
803
804
805
806
    @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)
807
  elsif output_dir == "default downloads"
808
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
809
810
  else
    @screen.type(output_dir + '/')
811
812
813
814
815
  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)
816
817
  if should_work
    try_for(10, :msg => "The page was not saved to #{output_dir}/#{output_file}") {
818
      $vm.file_exist?("#{output_dir}/#{output_file}")
819
820
821
822
    }
  else
    @screen.wait("TorBrowserCannotSavePage.png", 10)
  end
823
824
825
826
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"
827
    output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
828
  else
829
    output_dir = "/home/#{LIVE_USER}/Tor Browser"
830
831
  end
  @screen.type("p", Sikuli::KeyModifier.CTRL)
832
  @screen.wait("TorBrowserPrintDialog.png", 20)
833
  @screen.wait_and_click("BrowserPrintToFile.png", 10)
834
835
836
837
838
  @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
839
  @screen.type(output_dir + '/' + output_file.sub(/[.]pdf$/, '') + Sikuli::Key.ENTER)
anonym's avatar
anonym committed
840
  try_for(30, :msg => "The page was not printed to #{output_dir}/#{output_file}") {
841
    $vm.file_exist?("#{output_dir}/#{output_file}")
842
843
  }
end
844

anonym's avatar
anonym committed
845
Given /^a web server is running on the LAN$/ do
846
847
848
  @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
849
850
851
852
853
854
855
856
857
858
859
860
861
862
  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")
863
864
  server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
                                   :Port => #{@web_server_port},
anonym's avatar
anonym committed
865
866
867
868
869
870
                                   :DocumentRoot => "/dev/null")
  server.mount_proc("/") do |req, res|
    res.body = "#{web_server_hello_msg}"
  end
  server.start
EOF
871
  add_lan_host(@web_server_ip_addr, @web_server_port)
872
  proc = IO.popen(['ruby', '-e', code])
anonym's avatar
anonym committed
873
874
875
876
  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
877
  add_after_scenario_hook { Process.kill("TERM", proc.pid) }
anonym's avatar
anonym committed
878
879
880
881
882
883
884
885
886
887

  # 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
888
    msg = $vm.execute_successfully("curl #{@web_server_url}",
anonym's avatar
anonym committed
889
                                   :user => LIVE_USER).stdout.chomp
890
    web_server_hello_msg == msg
anonym's avatar
anonym committed
891
892
  end
end
893

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

898
899
900
901
902
903
904
905
906
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
907

anonym's avatar
anonym committed
908
Given /^I (?:re)?start monitoring the AppArmor log of "([^"]+)"$/ do |profile|
909
910
  # AppArmor log entries may be dropped if printk rate limiting is
  # enabled.
911
  $vm.execute_successfully('sysctl -w kernel.printk_ratelimit=0')
anonym's avatar
anonym committed
912
913
  # We will only care about entries for this profile from this time
  # and on.
914
915
  guest_time = $vm.execute_successfully(
    'date +"%Y-%m-%d %H:%M:%S"').stdout.chomp
anonym's avatar
anonym committed
916
917
  @apparmor_profile_monitoring_start ||= Hash.new
  @apparmor_profile_monitoring_start[profile] = guest_time
918
919
end

920
When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"(?: after at most (\d+) seconds)?$/ do |anti_test, profile, file, time|
anonym's avatar
anonym committed
921
922
923
924
925
  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]
926
  block = Proc.new do
927
928
929
930
931
932
    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))
933
934
935
936
937
938
939
940
941
    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
942
    raise e, "AppArmor has #{anti_test ? "" : "not "}denied the operation"
943
944
  end
end
945

946
Then /^I force Tor to use a new circuit$/ do
anonym's avatar
anonym committed
947
  force_new_tor_circuit
948
end
949
950
951
952
953
954

When /^I eject the boot medium$/ do
  dev = boot_device
  dev_type = device_info(dev)['ID_TYPE']
  case dev_type
  when 'cd'
955
    $vm.eject_cdrom
956
957
958
959
960
961
962
  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
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978

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
979
980
981
982

def share_host_files(files)
  files = [files] if files.class == String
  assert_equal(Array, files.class)
983
  disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
984
985
  # Let's add some extra space for filesysten overhead etc.
  disk_size += [convert_to_bytes(1, 'MiB'), (disk_size * 0.10).ceil].max
986
  disk = random_alpha_string(10)
987
  step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
988
989
990
991
992
993
994
995
996
997
998
999
1000
  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 +