mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-04 12:21:52 -04:00
Drive a loopback daemon (secure stdio-pipe transport by default, also green
under --use-tcp) via the new write_daemon_conf helper and assert the behaviour
of the security-relevant rsyncd.conf parameters, transferring >=3-deep trees:
daemon-access path / read only / write only / list, incl. a deep sub-path
pull and that a list=no module is hidden yet usable by name.
daemon-filter daemon exclude hides matching files everywhere; incoming /
outgoing chmod rewrite modes of every transferred file.
daemon-auth auth users + secrets file accept the right password, reject a
wrong one and an unauthenticated request; strict modes rejects
a world-readable secrets file.
daemon-exec pre-/post-xfer exec run with RSYNC_MODULE_NAME /
RSYNC_EXIT_STATUS; a failing pre-xfer exec aborts the transfer
(marker files polled for, since post-xfer exec runs after the
client disconnects under TCP).
daemon-munge munge symlinks stores incoming links with the /rsyncd-munged/
prefix and strips it on the way out.
daemon-refuse refuse options: a named option, a wildcard, and the '* !a !v'
allow-list idiom.
Green on master under pipe and --use-tcp transports and under --protocol=29.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Daemon coverage: pre-xfer exec and post-xfer exec.
|
|
|
|
A module's pre-xfer/post-xfer exec hooks must run with the documented
|
|
environment (RSYNC_MODULE_NAME, RSYNC_EXIT_STATUS, ...), and a non-zero
|
|
pre-xfer exec must abort the transfer.
|
|
"""
|
|
|
|
import subprocess
|
|
import time
|
|
|
|
from rsyncfns import (
|
|
FROMDIR, SCRATCHDIR,
|
|
make_tree, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,
|
|
write_daemon_conf,
|
|
)
|
|
|
|
|
|
def wait_for(path, want, secs=5):
|
|
"""Poll for a marker file to contain `want`. post-xfer exec runs on the
|
|
daemon side after the client disconnects (a real race under --use-tcp),
|
|
so we must wait for it rather than check immediately."""
|
|
deadline = time.monotonic() + secs
|
|
while time.monotonic() < deadline:
|
|
if path.is_file() and path.read_text().strip() == want:
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
DAEMON_PORT = 12889
|
|
|
|
src = FROMDIR
|
|
rmtree(src)
|
|
make_tree(src, depth=3)
|
|
|
|
markers = SCRATCHDIR / 'markers'
|
|
rmtree(markers)
|
|
makepath(markers)
|
|
hookdir = SCRATCHDIR / 'hookdest'
|
|
faildir = SCRATCHDIR / 'faildest'
|
|
makepath(hookdir, faildir)
|
|
|
|
|
|
def script(name, body):
|
|
p = SCRATCHDIR / name
|
|
p.write_text('#!/bin/sh\n' + body)
|
|
p.chmod(0o755)
|
|
return p
|
|
|
|
|
|
pre = script('pre.sh', f'echo "$RSYNC_MODULE_NAME" > {markers}/pre.out\nexit 0\n')
|
|
post = script('post.sh', f'echo "$RSYNC_EXIT_STATUS" > {markers}/post.out\n'
|
|
'exit 0\n')
|
|
prefail = script('prefail.sh', 'exit 1\n')
|
|
|
|
conf = write_daemon_conf([
|
|
('hook', {'path': hookdir, 'read only': 'no',
|
|
'pre-xfer exec': pre, 'post-xfer exec': post}),
|
|
('failhook', {'path': faildir, 'read only': 'no',
|
|
'pre-xfer exec': prefail}),
|
|
])
|
|
url = start_test_daemon(conf, DAEMON_PORT)
|
|
|
|
# --- pre/post hooks run with the documented environment ---------------------
|
|
proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}hook/'),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
|
text=True)
|
|
if proc.returncode not in (0, 23):
|
|
test_fail(f"transfer through exec-hook module failed: {proc.stderr}")
|
|
if not wait_for(markers / 'pre.out', 'hook'):
|
|
test_fail("pre-xfer exec did not run with RSYNC_MODULE_NAME=hook")
|
|
if not wait_for(markers / 'post.out', '0'):
|
|
test_fail("post-xfer exec did not run with RSYNC_EXIT_STATUS=0")
|
|
|
|
# --- a failing pre-xfer exec aborts the transfer ----------------------------
|
|
proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}failhook/'),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
|
text=True)
|
|
if proc.returncode == 0:
|
|
test_fail("a failing pre-xfer exec did not abort the transfer")
|
|
if list(faildir.iterdir()):
|
|
test_fail("transfer wrote files despite a failing pre-xfer exec")
|
|
|
|
print("daemon-exec: pre-xfer/post-xfer exec env + abort-on-failure verified")
|