diff --git a/rsync-ssl b/rsync-ssl index 8aa2ae642..a829387df 100755 --- a/rsync-ssl +++ b/rsync-ssl @@ -226,9 +226,20 @@ if [[ "$1" == --HELPER ]]; then rsync_ssl_helper "${@}" fi -if [[ "$1" == --type=* ]]; then - export RSYNC_SSL_TYPE="${1/--type=/}" - shift -fi +args=() +dash_dash_seen=false +for arg in "$@"; do + if [[ $dash_dash_seen == true ]]; then + args+=("$arg") + elif [[ "$arg" == "--" ]]; then + args+=("$arg") + dash_dash_seen=true + elif [[ "$arg" == --type=* ]]; then + export RSYNC_SSL_TYPE="${arg#--type=}" + else + args+=("$arg") + fi +done +set -- "${args[@]}" rsync_ssl_run "${@}" diff --git a/rsync-ssl.1.md b/rsync-ssl.1.md index 6165f349f..030e56f02 100644 --- a/rsync-ssl.1.md +++ b/rsync-ssl.1.md @@ -25,12 +25,15 @@ rsync version to be at least 3.2.0. ## OPTIONS -If the **first** arg is a `--type=SSL_TYPE` option, the script will only use +If an arg is a `--type=SSL_TYPE` option, the script will only use that particular program to open an ssl connection instead of trying to find an openssl or stunnel executable via a simple heuristic (assuming that the `RSYNC_SSL_TYPE` environment variable is not set as well -- see below). This option must specify one of `openssl` or `stunnel`. The equal sign is -required for this particular option. +required for this particular option. The wrapper's option scan stops at a +`--` argument: the `--` and everything after it are passed through to rsync +unchanged, so a `--type=...` token after a `--` is not consumed by the +wrapper. All the other options are passed through to the rsync command, so consult the **rsync**(1) manpage for more information on how it works. diff --git a/testsuite/rsync-ssl-type-option_test.py b/testsuite/rsync-ssl-type-option_test.py new file mode 100644 index 000000000..ae4a7e7b1 --- /dev/null +++ b/testsuite/rsync-ssl-type-option_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# rsync-ssl only recognized --type=SSL_TYPE as the FIRST argument, so +# "rsync-ssl --dry-run --type=stunnel host::mod" passed the option through to +# the underlying rsync, which rejected it with "--type=stunnel: unknown option". +# Fix: scan the whole argument list for --type=..., export RSYNC_SSL_TYPE, and +# drop the option before handing the remaining args to rsync. +# A `--` arg stops the wrapper-option scan: `--` and everything after it are +# passed through to rsync verbatim, so an operand such as `--type=stunnel` +# that was protected from option parsing is not consumed by the wrapper. +# +# A fake rsync in PATH records the args it receives and the RSYNC_SSL_TYPE it +# observes; rsync-ssl is run in its normal (non-HELPER) mode with --type= in +# various positions. The recorded args must contain every other option but +# never a --type= token, RSYNC_SSL_TYPE must match what the wrapper consumed, +# and rsync-ssl must exit successfully. + +import os +import subprocess + +from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail + +base = SCRATCHDIR / 'rsync-ssl-type-opt' +rmtree(base) +base.mkdir(parents=True) + +args_capture = base / 'rsync_args' +type_capture = base / 'rsync_ssl_type' +fakebin = base / 'bin' +fakebin.mkdir(parents=True) +fake_rsync = fakebin / 'rsync' +fake_rsync.write_text( + f"#!/usr/bin/env bash\n" + f"printf '%s\\n' \"$@\" > {args_capture}\n" + f"printf '%s\\n' \"${{RSYNC_SSL_TYPE-UNSET}}\" > {type_capture}\n" + f"exit 0\n") +fake_rsync.chmod(0o755) + +env = {**os.environ, 'PATH': str(fakebin) + os.pathsep + os.environ.get('PATH', '')} +for v in ('RSYNC_SSL_TYPE', 'RSYNC_SSL_OPENSSL', 'RSYNC_SSL_STUNNEL'): + env.pop(v, None) + + +def run(args, expect_type): + for capture in (args_capture, type_capture): + if capture.exists(): + capture.unlink() + proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + args, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if proc.returncode != 0: + test_fail(f"rsync-ssl exited {proc.returncode} for args {args!r}:\n{proc.stdout}") + got_args = args_capture.read_text().splitlines() if args_capture.exists() else [] + got_type = type_capture.read_text().strip() if type_capture.exists() else 'UNSET' + if got_type != expect_type: + test_fail(f"RSYNC_SSL_TYPE is {got_type!r}, expected {expect_type!r} " + f"for args {args!r}:\n{proc.stdout}") + return got_args + + +# --- The reported failure: --type= in the middle of the rsync args. +got = run(['--dry-run', '--type=stunnel', 'host::mod'], 'stunnel') +if any(a.startswith('--type=') for a in got): + test_fail(f"--type= was passed through to rsync instead of being consumed:\n{got}") +for want in ('--dry-run', 'host::mod'): + if want not in got: + test_fail(f"missing rsync arg {want!r} after --type= handling:\n{got}") +if not any(a.startswith('--rsh=') for a in got): + test_fail(f"missing the --rsh= helper option:\n{got}") + +# --- First, last, and no --type= keep working. +for pos_args, expect_type in ((['--type=stunnel', '--dry-run', 'host::mod'], 'stunnel'), + (['-av', 'host::mod', '--type=openssl'], 'openssl'), + (['-av', 'host::mod'], 'UNSET')): + got = run(pos_args, expect_type) + if any(a.startswith('--type=') for a in got): + test_fail(f"--type= was passed through for args {pos_args!r}:\n{got}") + for want in pos_args: + if want.startswith('--type='): + continue + if want not in got: + test_fail(f"missing rsync arg {want!r} for args {pos_args!r}:\n{got}") + +# --- `--` stops the wrapper-option scan: the protected operand is preserved. +rsh_arg = "--rsh='{}' --HELPER".format(SRCDIR / 'rsync-ssl') +got = run(['--', '--type=stunnel', 'host::mod'], 'UNSET') +if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']: + test_fail(f"args after -- must be preserved verbatim (no --type= consumed):\n{got}") + +# --- A wrapper option before `--` is still consumed; the protected one is not. +got = run(['--type=openssl', '--', '--type=stunnel', 'host::mod'], 'openssl') +if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']: + test_fail(f"--type= before -- is consumed, args after -- are preserved:\n{got}") + +print("rsync-ssl-type-option: --type=SSL_TYPE is consumed in any argument " + "position (until a -- stops the wrapper-option scan), exported as " + "RSYNC_SSL_TYPE, and rsync-ssl exits successfully")