1#!/usr/bin/env python3
2"""Compare SMB enumeration with a disposable Linux VM's filesystem and interrupt pagination."""
3import argparse
4import json
5import os
6from pathlib import Path
7import socket
8import subprocess
9import sys
10import time
11import uuid
12
13sys.path.insert(0, str(Path(__file__).resolve().parent / 'w7'))
14from linux_vm import load_instance, ssh_argv
15
16
17def main():
18 parser = argparse.ArgumentParser(description=__doc__)
19 parser.add_argument('vm', help='An already running, caller-owned Linux lab VM')
20 parser.add_argument('output', type=Path)
21 args = parser.parse_args()
22 output = args.output.resolve()
23 output.mkdir(parents=True, exist_ok=False)
24 config = load_instance(args.vm)
25 root = 'directory-' + uuid.uuid4().hex[:12]
26 script = f'''from pathlib import Path
27import json
28root = Path('/srv/agent') / {root!r}
29root.mkdir()
30(root / 'empty').mkdir()
31(root / 'nested šŸ¦€').mkdir()
32(root / 'denied').mkdir()
33(root / 'file.one').write_bytes(b'fixture')
34for i in range(4000):
35 name = f'Section {{i:04d}} šŸ¦€ é ' + 'x' * 80 + '.one'
36 with (root / name).open('wb') as file: file.truncate(i * 12345)
37entries = [dict(name=p.name, directory=p.is_dir(), size=p.stat().st_size) for p in root.iterdir()]
38(root / 'denied').chmod(0)
39print(json.dumps(dict(path=root.name, entries=entries), ensure_ascii=False))
40'''
41 fixture = subprocess.run(ssh_argv(args.vm, 'python3 -'), input=script, text=True,
42 capture_output=True, check=True, timeout=60)
43 (output / 'oracle.json').write_text(fixture.stdout)
44 cases = [('complete', {})]
45 cases += [(f'{direction}-{occurrence}', dict(cut=14, direction=direction, occurrence=occurrence))
46 for direction in ('request', 'response') for occurrence in (1, 2, 10)]
47 cases.append(('close', dict(cut=6, direction='response')))
48 for name, control in cases:
49 with socket.socket() as reservation:
50 reservation.bind(('127.0.0.1', 0))
51 port = reservation.getsockname()[1]
52 control_path = output / f'{name}-control.json'
53 control_path.write_text(json.dumps(control))
54 trace = output / f'{name}-trace.jsonl'
55 with trace.open('w') as log:
56 proxy = subprocess.Popen([sys.executable, str(Path(__file__).with_name('smb-proxy.py')),
57 str(control_path), '--port', str(port), '--server', '127.0.0.1',
58 '--server-port', str(config['samba_port'])], stdout=log, stderr=log)
59 try:
60 deadline = time.monotonic() + 5
61 while True:
62 records = [json.loads(line) for line in trace.read_text().splitlines()]
63 if any('listening' in row for row in records) and any('control' in row for row in records):
64 break
65 if proxy.poll() is not None or time.monotonic() > deadline:
66 raise RuntimeError('Directory test proxy did not start')
67 time.sleep(.05)
68 env = dict(os.environ, ONESTORE_SMB_LAB=f'127.0.0.1:{port}',
69 ONESTORE_SMB_DIRECTORY=root, ONESTORE_SMB_DIRECTORY_ORACLE=str(output / 'oracle.json'))
70 test = 'tests::live_directory_interruption' if control else 'smb::tests::live_directory'
71 with (output / f'{name}.log').open('w') as result:
72 subprocess.run(['cargo', 'test', '-p', 'notebook', '--features', 'smb', test, '--', '--ignored', '--exact'],
73 env=env, stdout=result, stderr=result, check=True, timeout=120)
74 finally:
75 proxy.terminate()
76 proxy.wait(timeout=5)
77 records = [json.loads(line) for line in trace.read_text().splitlines()]
78 if control:
79 assert sum('cut' in row for row in records) == 1
80 else:
81 assert sum(row.get('command') == 14 and row.get('status') == '0x0' for row in records) > 10
82 assert any(row.get('command') == 14 and row.get('status') == '0x80000006' for row in records)
83 print(f'{name}: passed', flush=True)
84 env = dict(os.environ, ONESTORE_SMB_LAB=f'127.0.0.1:{config["samba_port"]}',
85 ONESTORE_SMB_DIRECTORY_ORACLE=str(output / 'oracle.json'))
86 with (output / 'reconnected.log').open('w') as result:
87 subprocess.run(['cargo', 'test', '-p', 'notebook', '--features', 'smb', 'smb::tests::live_directory', '--', '--ignored', '--exact'],
88 env=env, stdout=result, stderr=result, check=True, timeout=120)
89 print('reconnected: passed', flush=True)
90
91
92if __name__ == '__main__':
93 main()