1# 2# Wireshark tests 3# 4# Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl> 5# 6# SPDX-License-Identifier: GPL-2.0-or-later 7# 8'''Fixtures that are specific to Wireshark.''' 9 10from contextlib import contextmanager 11import os 12import re 13import subprocess 14import sys 15import tempfile 16import types 17 18import fixtures 19import subprocesstest 20 21 22@fixtures.fixture(scope='session') 23def capture_interface(request, cmd_dumpcap): 24 ''' 25 Name of capture interface. Tests will be skipped if dumpcap is not 26 available or no Loopback interface is available. 27 ''' 28 disabled = request.config.getoption('--disable-capture', default=False) 29 if disabled: 30 fixtures.skip('Capture tests are disabled via --disable-capture') 31 proc = subprocess.Popen((cmd_dumpcap, '-D'), stdout=subprocess.PIPE, 32 stderr=subprocess.PIPE, universal_newlines=True) 33 outs, errs = proc.communicate() 34 if proc.returncode != 0: 35 print('"dumpcap -D" exited with %d. stderr:\n%s' % 36 (proc.returncode, errs)) 37 fixtures.skip('Test requires capture privileges and an interface.') 38 # Matches: "lo (Loopback)" (Linux), "lo0 (Loopback)" (macOS) or 39 # "\Device\NPF_{...} (Npcap Loopback Adapter)" (Windows) 40 print('"dumpcap -D" output:\n%s' % (outs,)) 41 m = re.search(r'^(\d+)\. .*\(.*Loopback.*\)', outs, re.MULTILINE) 42 if not m: 43 fixtures.skip('Test requires a capture interface.') 44 iface = m.group(1) 45 # Interface found, check for capture privileges (needed for Linux). 46 try: 47 subprocess.check_output((cmd_dumpcap, '-L', '-i', iface), 48 stderr=subprocess.STDOUT, 49 universal_newlines=True) 50 return iface 51 except subprocess.CalledProcessError as e: 52 print('"dumpcap -L -i %s" exited with %d. Output:\n%s' % (iface, 53 e.returncode, 54 e.output)) 55 fixtures.skip('Test requires capture privileges.') 56 57 58@fixtures.fixture(scope='session') 59def program_path(request): 60 ''' 61 Path to the Wireshark binaries as set by the --program-path option, the 62 WS_BIN_PATH environment variable or (curdir)/run. 63 ''' 64 curdir_run = os.path.join(os.curdir, 'run') 65 if sys.platform == 'win32': 66 curdir_run_config = os.path.join(curdir_run, 'RelWithDebInfo') 67 if os.path.exists(curdir_run_config): 68 curdir_run = curdir_run_config 69 paths = ( 70 request.config.getoption('--program-path', default=None), 71 os.environ.get('WS_BIN_PATH'), 72 curdir_run, 73 ) 74 for path in paths: 75 if type(path) == str and os.path.isdir(path): 76 return path 77 raise AssertionError('Missing directory with Wireshark binaries') 78 79 80@fixtures.fixture(scope='session') 81def program(program_path, request): 82 skip_if_missing = request.config.getoption('--skip-missing-programs', 83 default='') 84 skip_if_missing = skip_if_missing.split(',') if skip_if_missing else [] 85 dotexe = '' 86 if sys.platform.startswith('win32'): 87 dotexe = '.exe' 88 89 def resolver(name): 90 path = os.path.abspath(os.path.join(program_path, name + dotexe)) 91 if not os.access(path, os.X_OK): 92 if skip_if_missing == ['all'] or name in skip_if_missing: 93 fixtures.skip('Program %s is not available' % (name,)) 94 raise AssertionError('Program %s is not available' % (name,)) 95 return path 96 return resolver 97 98 99@fixtures.fixture(scope='session') 100def cmd_capinfos(program): 101 return program('capinfos') 102 103 104@fixtures.fixture(scope='session') 105def cmd_dumpcap(program): 106 return program('dumpcap') 107 108 109@fixtures.fixture(scope='session') 110def cmd_mergecap(program): 111 return program('mergecap') 112 113 114@fixtures.fixture(scope='session') 115def cmd_rawshark(program): 116 return program('rawshark') 117 118 119@fixtures.fixture(scope='session') 120def cmd_tshark(program): 121 return program('tshark') 122 123 124@fixtures.fixture(scope='session') 125def cmd_text2pcap(program): 126 return program('text2pcap') 127 128 129@fixtures.fixture(scope='session') 130def cmd_editcap(program): 131 return program('editcap') 132 133 134@fixtures.fixture(scope='session') 135def cmd_wireshark(program): 136 return program('wireshark') 137 138 139@fixtures.fixture(scope='session') 140def wireshark_command(cmd_wireshark): 141 # Windows can always display the GUI and macOS can if we're in a login session. 142 # On Linux, headless mode is used, see QT_QPA_PLATFORM in the 'test_env' fixture. 143 if sys.platform == 'darwin' and 'SECURITYSESSIONID' not in os.environ: 144 fixtures.skip('Wireshark GUI tests require loginwindow session') 145 if sys.platform not in ('win32', 'darwin', 'linux'): 146 if 'DISPLAY' not in os.environ: 147 fixtures.skip('Wireshark GUI tests require DISPLAY') 148 return (cmd_wireshark, '-ogui.update.enabled:FALSE') 149 150 151@fixtures.fixture(scope='session') 152def cmd_extcap(program): 153 def extcap_name(name): 154 if sys.platform == 'darwin': 155 return program(os.path.join('Wireshark.app/Contents/MacOS/extcap', name)) 156 else: 157 return program(os.path.join('extcap', name)) 158 return extcap_name 159 160 161@fixtures.fixture(scope='session') 162def features(cmd_tshark, make_env): 163 '''Returns an object describing available features in tshark.''' 164 try: 165 tshark_v = subprocess.check_output( 166 (cmd_tshark, '--version'), 167 stderr=subprocess.PIPE, 168 universal_newlines=True, 169 env=make_env() 170 ) 171 tshark_v = re.sub(r'\s+', ' ', tshark_v) 172 except subprocess.CalledProcessError as ex: 173 print('Failed to detect tshark features: %s' % (ex,)) 174 tshark_v = '' 175 gcry_m = re.search(r'with +Gcrypt +([0-9]+\.[0-9]+)', tshark_v) 176 return types.SimpleNamespace( 177 have_x64='Compiled (64-bit)' in tshark_v, 178 have_lua='with Lua' in tshark_v, 179 have_nghttp2='with nghttp2' in tshark_v, 180 have_kerberos='with MIT Kerberos' in tshark_v or 'with Heimdal Kerberos' in tshark_v, 181 have_libgcrypt16=gcry_m and float(gcry_m.group(1)) >= 1.6, 182 have_libgcrypt17=gcry_m and float(gcry_m.group(1)) >= 1.7, 183 have_libgcrypt18=gcry_m and float(gcry_m.group(1)) >= 1.8, 184 have_gnutls='with GnuTLS' in tshark_v, 185 have_pkcs11='and PKCS #11 support' in tshark_v, 186 have_brotli='with brotli' in tshark_v, 187 have_plugins='binary plugins supported' in tshark_v, 188 ) 189 190 191@fixtures.fixture(scope='session') 192def dirs(): 193 '''Returns fixed directories containing test input.''' 194 this_dir = os.path.dirname(__file__) 195 return types.SimpleNamespace( 196 baseline_dir=os.path.join(this_dir, 'baseline'), 197 capture_dir=os.path.join(this_dir, 'captures'), 198 config_dir=os.path.join(this_dir, 'config'), 199 key_dir=os.path.join(this_dir, 'keys'), 200 lua_dir=os.path.join(this_dir, 'lua'), 201 protobuf_lang_files_dir=os.path.join(this_dir, 'protobuf_lang_files'), 202 tools_dir=os.path.join(this_dir, '..', 'tools'), 203 ) 204 205 206@fixtures.fixture(scope='session') 207def capture_file(dirs): 208 '''Returns the path to a capture file.''' 209 def resolver(filename): 210 return os.path.join(dirs.capture_dir, filename) 211 return resolver 212 213 214@fixtures.fixture 215def home_path(): 216 '''Per-test home directory, removed when finished.''' 217 with tempfile.TemporaryDirectory(prefix='wireshark-tests-home-') as dirname: 218 yield dirname 219 220 221@fixtures.fixture 222def conf_path(home_path): 223 '''Path to the Wireshark configuration directory.''' 224 if sys.platform.startswith('win32'): 225 conf_path = os.path.join(home_path, 'Wireshark') 226 else: 227 conf_path = os.path.join(home_path, '.config', 'wireshark') 228 os.makedirs(conf_path) 229 return conf_path 230 231 232@fixtures.fixture(scope='session') 233def make_env(): 234 """A factory for a modified environment to ensure reproducible tests.""" 235 def make_env_real(home=None): 236 env = os.environ.copy() 237 env['TZ'] = 'UTC' 238 home_env = 'APPDATA' if sys.platform.startswith('win32') else 'HOME' 239 if home: 240 env[home_env] = home 241 else: 242 # This directory is supposed not to be written and is used by 243 # "readonly" tests that do not read any other preferences. 244 env[home_env] = "/wireshark-tests-unused" 245 # XDG_CONFIG_HOME takes precedence over HOME, which we don't want. 246 try: 247 del env['XDG_CONFIG_HOME'] 248 except KeyError: 249 pass 250 return env 251 return make_env_real 252 253 254@fixtures.fixture 255def base_env(home_path, make_env, request): 256 """A modified environment to ensure reproducible tests. Tests can modify 257 this environment as they see fit.""" 258 env = make_env(home=home_path) 259 260 # Remove this if test instances no longer inherit from SubprocessTestCase? 261 if isinstance(request.instance, subprocesstest.SubprocessTestCase): 262 # Inject the test environment as default if it was not overridden. 263 request.instance.injected_test_env = env 264 return env 265 266 267@fixtures.fixture 268def test_env(base_env, conf_path, request, dirs): 269 '''A process environment with a populated configuration directory.''' 270 # Populate our UAT files 271 uat_files = [ 272 '80211_keys', 273 'dtlsdecrypttablefile', 274 'esp_sa', 275 'ssl_keys', 276 'c1222_decryption_table', 277 'ikev1_decryption_table', 278 'ikev2_decryption_table', 279 ] 280 # uat.c replaces backslashes... 281 key_dir_path = os.path.join(dirs.key_dir, '').replace('\\', '\\x5c') 282 for uat in uat_files: 283 template_file = os.path.join(dirs.config_dir, uat + '.tmpl') 284 out_file = os.path.join(conf_path, uat) 285 with open(template_file, 'r') as f: 286 template_contents = f.read() 287 cf_contents = template_contents.replace('TEST_KEYS_DIR', key_dir_path) 288 with open(out_file, 'w') as f: 289 f.write(cf_contents) 290 291 env = base_env 292 env['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1' 293 env['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1' 294 295 # Allow GUI tests to be run without opening windows nor requiring a Xserver. 296 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file 297 # in the current directory, output0000.png, output0001.png, etc. Note that 298 # this will overwrite existing files. 299 if sys.platform == 'linux': 300 # This option was verified working on Arch Linux with Qt 5.12.0-2 and 301 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and 302 # Windows it unfortunately crashes (Qt 5.12.0). 303 env['QT_QPA_PLATFORM'] = 'minimal' 304 305 # Remove this if test instances no longer inherit from SubprocessTestCase? 306 if isinstance(request.instance, subprocesstest.SubprocessTestCase): 307 # Inject the test environment as default if it was not overridden. 308 request.instance.injected_test_env = env 309 return env 310 311 312@fixtures.fixture 313def test_env_80211_user_tk(base_env, conf_path, request, dirs): 314 '''A process environment with a populated configuration directory.''' 315 # Populate our UAT files 316 uat_files = [ 317 '80211_keys', 318 ] 319 # uat.c replaces backslashes... 320 key_dir_path = os.path.join(dirs.key_dir, '').replace('\\', '\\x5c') 321 for uat in uat_files: 322 template_file = os.path.join(dirs.config_dir, uat + '.user_tk_tmpl') 323 out_file = os.path.join(conf_path, uat) 324 with open(template_file, 'r') as f: 325 template_contents = f.read() 326 cf_contents = template_contents.replace('TEST_KEYS_DIR', key_dir_path) 327 with open(out_file, 'w') as f: 328 f.write(cf_contents) 329 330 env = base_env 331 env['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1' 332 env['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1' 333 334 # Allow GUI tests to be run without opening windows nor requiring a Xserver. 335 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file 336 # in the current directory, output0000.png, output0001.png, etc. Note that 337 # this will overwrite existing files. 338 if sys.platform == 'linux': 339 # This option was verified working on Arch Linux with Qt 5.12.0-2 and 340 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and 341 # Windows it unfortunately crashes (Qt 5.12.0). 342 env['QT_QPA_PLATFORM'] = 'minimal' 343 344 # Remove this if test instances no longer inherit from SubprocessTestCase? 345 if isinstance(request.instance, subprocesstest.SubprocessTestCase): 346 # Inject the test environment as default if it was not overridden. 347 request.instance.injected_test_env = env 348 return env 349 350@fixtures.fixture 351def unicode_env(home_path, make_env): 352 '''A Wireshark configuration directory with Unicode in its path.''' 353 home_env = 'APPDATA' if sys.platform.startswith('win32') else 'HOME' 354 uni_home = os.path.join(home_path, 'unicode-Ф-€-中-testcases') 355 env = make_env(home=uni_home) 356 if sys.platform == 'win32': 357 pluginsdir = os.path.join(uni_home, 'Wireshark', 'plugins') 358 else: 359 pluginsdir = os.path.join(uni_home, '.local/lib/wireshark/plugins') 360 os.makedirs(pluginsdir) 361 return types.SimpleNamespace( 362 path=lambda *args: os.path.join(uni_home, *args), 363 env=env, 364 pluginsdir=pluginsdir 365 ) 366 367 368@fixtures.fixture(scope='session') 369def make_screenshot(): 370 '''Creates a screenshot and save it to a file. Intended for CI purposes.''' 371 def make_screenshot_real(filename): 372 try: 373 if sys.platform == 'darwin': 374 subprocess.check_call(['screencapture', filename]) 375 else: 376 print("Creating a screenshot on this platform is not supported") 377 return 378 size = os.path.getsize(filename) 379 print("Created screenshot %s (%d bytes)" % (filename, size)) 380 except (subprocess.CalledProcessError, OSError) as e: 381 print("Failed to take screenshot:", e) 382 return make_screenshot_real 383 384 385@fixtures.fixture 386def make_screenshot_on_error(request, make_screenshot): 387 '''Writes a screenshot when a process times out.''' 388 @contextmanager 389 def make_screenshot_on_error_real(): 390 try: 391 yield 392 except subprocess.TimeoutExpired: 393 filename = request.instance.filename_from_id('screenshot.png') 394 make_screenshot(filename) 395 raise 396 return make_screenshot_on_error_real 397