1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
|
require 'fileutils'
require 'rb-inotify'
require 'time'
require 'tmpdir'
# Run once, before any feature
AfterConfiguration do |config|
# Reorder the execution of some features. As we progress through a
# run we accumulate more and more snapshots and hence use more and
# more disk space, but some features will leave nothing behind
# and/or possibly use large amounts of disk space temporarily for
# various reasons. By running these first we minimize the amount of
# disk space needed.
prioritized_features = [
# Features not using snapshots but using large amounts of scratch
# space for other reasons:
'features/untrusted_partitions.feature',
# Features using temporary snapshots:
'features/apt.feature',
'features/root_access_control.feature',
'features/time_syncing.feature',
'features/tor_bridges.feature',
# Features using large amounts of scratch space for other reasons:
'features/erase_memory.feature',
# This feature needs the almost biggest snapshot (USB install,
# excluding persistence) and will create yet another disk and
# install Tails on it. This should be the peak of disk usage.
'features/usb_install.feature',
# This feature needs a copy of the ISO and creates a new disk.
'features/usb_upgrade.feature',
# This feature needs a very big snapshot (USB install with persistence)
# and another, network-enabled snapshot.
'features/emergency_shutdown.feature',
]
feature_files = config.feature_files
# The &-intersection is specified to keep the element ordering of
# the *left* operand.
intersection = prioritized_features & feature_files
if not intersection.empty?
feature_files -= intersection
feature_files = intersection + feature_files
config.define_singleton_method(:feature_files) { feature_files }
end
# Used to keep track of when we start our first @product feature, when
# we'll do some special things.
$started_first_product_feature = false
if File.exist?($config["TMPDIR"])
if !File.directory?($config["TMPDIR"])
raise "Temporary directory '#{$config["TMPDIR"]}' exists but is not a " +
"directory"
end
if !File.owned?($config["TMPDIR"])
raise "Temporary directory '#{$config["TMPDIR"]}' must be owned by the " +
"current user"
end
FileUtils.chmod(0755, $config["TMPDIR"])
else
begin
FileUtils.mkdir_p($config["TMPDIR"])
rescue Errno::EACCES => e
raise "Cannot create temporary directory: #{e.to_s}"
end
end
# Start a thread that monitors a pseudo fifo file and debug_log():s
# anything written to it "immediately" (well, as fast as inotify
# detects it). We're forced to a convoluted solution like this
# because CRuby's thread support is horribly as soon as IO is mixed
# in (other threads get blocked).
FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO)
FileUtils.touch(DEBUG_LOG_PSEUDO_FIFO)
at_exit do
FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO)
end
Thread.new do
File.open(DEBUG_LOG_PSEUDO_FIFO) do |fd|
watcher = INotify::Notifier.new
watcher.watch(DEBUG_LOG_PSEUDO_FIFO, :modify) do
line = fd.read.chomp
debug_log(line) if line and line.length > 0
end
watcher.run
end
end
# Fix Sikuli's debug_log():ing.
bind_java_to_pseudo_fifo_logger
end
# Common
########
After do
if @after_scenario_hooks
@after_scenario_hooks.each { |block| block.call }
end
@after_scenario_hooks = Array.new
end
BeforeFeature('@product', '@source') do |feature|
raise "Feature #{feature.file} is tagged both @product and @source, " +
"which is an impossible combination"
end
at_exit do
$vm.destroy_and_undefine if $vm
if $virt
unless KEEP_SNAPSHOTS
VM.remove_all_snapshots
$vmstorage.clear_pool
end
#$vmnet.destroy_and_undefine
$virt.close
end
# The artifacts directory is empty (and useless) if it contains
# nothing but the mandatory . and ..
if Dir.entries(ARTIFACTS_DIR).size <= 2
FileUtils.rmdir(ARTIFACTS_DIR)
end
end
# For @product tests
####################
def add_after_scenario_hook(&block)
@after_scenario_hooks ||= Array.new
@after_scenario_hooks << block
end
def save_failure_artifact(type, path)
$failure_artifacts << [type, path]
end
# Due to Tails' Tor enforcement, we only allow contacting hosts that
# are Tor nodes or located on the LAN. However, when we try
# to verify that only such hosts are contacted we have a problem --
# we run all Tor nodes (via Chutney) *and* LAN hosts (used on some
# tests) on the same host, the one running the test suite. Hence we
# need to always explicitly track which nodes are LAN or not.
#
# Warning: when a host is added via this function, it is only added
# for the current scenario. As such, if this is done before saving a
# snapshot, it will not remain after the snapshot is loaded.
def add_lan_host(ipaddr, port)
@lan_hosts ||= []
@lan_hosts << { address: ipaddr, port: port }
end
BeforeFeature('@product') do |feature|
if TAILS_ISO.nil?
raise "No ISO image specified, and none could be found in the " +
"current directory"
end
if File.exist?(TAILS_ISO)
# Workaround: when libvirt takes ownership of the ISO image it may
# become unreadable for the live user inside the guest in the
# host-to-guest share used for some tests.
if !File.world_readable?(TAILS_ISO)
if File.owned?(TAILS_ISO)
File.chmod(0644, TAILS_ISO)
else
raise "warning: the Tails ISO image must be world readable or be " +
"owned by the current user to be available inside the guest " +
"VM via host-to-guest shares, which is required by some tests"
end
end
else
raise "The specified Tails ISO image '#{TAILS_ISO}' does not exist"
end
if !File.exist?(OLD_TAILS_ISO)
raise "The specified old Tails ISO image '#{OLD_TAILS_ISO}' does not exist"
end
if not($started_first_product_feature)
$virt = Libvirt::open("qemu:///system")
VM.remove_all_snapshots if !KEEP_SNAPSHOTS
$vmnet = VMNet.new($virt, VM_XML_PATH)
$vmstorage = VMStorage.new($virt, VM_XML_PATH)
$started_first_product_feature = true
end
ensure_chutney_is_running
end
AfterFeature('@product') do
unless KEEP_SNAPSHOTS
checkpoints.each do |name, vals|
if vals[:temporary] and VM.snapshot_exists?(name)
VM.remove_snapshot(name)
end
end
end
$vmstorage.list_volumes.each do |vol_name|
next if vol_name == '__internal'
$vmstorage.delete_volume(vol_name)
end
end
# Cucumber Before hooks are executed in the order they are listed, and
# we want this hook to always run first, so it must always be the
# *first* Before hook matching @product listed in this file.
Before('@product') do |scenario|
$failure_artifacts = Array.new
if $config["CAPTURE"]
video_name = sanitize_filename("#{scenario.name}.mpg")
@video_path = "#{ARTIFACTS_DIR}/#{video_name}"
capture = IO.popen(['avconv',
'-f', 'x11grab',
'-s', '1024x768',
'-r', '5',
'-i', "#{$config['DISPLAY']}.0",
'-an',
'-c:v', 'libx264',
'-y',
@video_path,
:err => ['/dev/null', 'w'],
])
@video_capture_pid = capture.pid
end
@screen = Sikuli::Screen.new
# English will be assumed if this is not overridden
@language = ""
@os_loader = "MBR"
@sudo_password = "asdf"
@persistence_password = "asdf"
# See comment for add_lan_host() above.
@lan_hosts ||= []
end
# Cucumber After hooks are executed in the *reverse* order they are
# listed, and we want this hook to always run second last, so it must always
# be the *second* After hook matching @product listed in this file --
# hooks added dynamically via add_after_scenario_hook() are supposed to
# truly be last.
After('@product') do |scenario|
if @video_capture_pid
# We can be incredibly fast at detecting errors sometimes, so the
# screen barely "settles" when we end up here and kill the video
# capture. Let's wait a few seconds more to make it easier to see
# what the error was.
sleep 3 if scenario.failed?
Process.kill("INT", @video_capture_pid)
save_failure_artifact("Video", @video_path)
end
if scenario.failed?
time_of_fail = Time.now - TIME_AT_START
secs = "%02d" % (time_of_fail % 60)
mins = "%02d" % ((time_of_fail / 60) % 60)
hrs = "%02d" % (time_of_fail / (60*60))
elapsed = "#{hrs}:#{mins}:#{secs}"
info_log("Scenario failed at time #{elapsed}")
screen_capture = @screen.capture
save_failure_artifact("Screenshot", screen_capture.getFilename)
if scenario.exception.kind_of?(FirewallAssertionFailedError)
Dir.glob("#{$config["TMPDIR"]}/*.pcap").each do |pcap_file|
save_failure_artifact("Network capture", pcap_file)
end
end
$failure_artifacts.sort!
$failure_artifacts.each do |type, file|
artifact_name = sanitize_filename("#{elapsed}_#{scenario.name}#{File.extname(file)}")
artifact_path = "#{ARTIFACTS_DIR}/#{artifact_name}"
assert(File.exist?(file))
FileUtils.mv(file, artifact_path)
info_log
info_log_artifact_location(type, artifact_path)
end
if $config["INTERACTIVE_DEBUGGING"]
pause(
"Scenario failed: #{scenario.name}. " +
"The error was: #{scenario.exception.class.name}: #{scenario.exception}"
)
end
else
if @video_path && File.exist?(@video_path) && not($config['CAPTURE_ALL'])
FileUtils.rm(@video_path)
end
end
end
Before('@product', '@check_tor_leaks') do |scenario|
@tor_leaks_sniffer = Sniffer.new(sanitize_filename(scenario.name), $vmnet)
@tor_leaks_sniffer.capture
add_after_scenario_hook do
@tor_leaks_sniffer.clear
end
end
After('@product', '@check_tor_leaks') do |scenario|
@tor_leaks_sniffer.stop
if scenario.passed?
allowed_nodes = @bridge_hosts ? @bridge_hosts : allowed_hosts_under_tor_enforcement
assert_all_connections(@tor_leaks_sniffer.pcap_file) do |c|
allowed_nodes.include?({ address: c.daddr, port: c.dport })
end
end
end
# For @source tests
###################
# BeforeScenario
Before('@source') do
@orig_pwd = Dir.pwd
@git_clone = Dir.mktmpdir 'tails-apt-tests'
Dir.chdir @git_clone
end
# AfterScenario
After('@source') do
Dir.chdir @orig_pwd
FileUtils.remove_entry_secure @git_clone
end
|