Harden open wrapper for Bash 3 and IDN host parity
This commit is contained in:
parent
0046b674aa
commit
3afa345f3a
2 changed files with 176 additions and 18 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
SYSTEM_OPEN_BIN="${CMUX_OPEN_WRAPPER_SYSTEM_OPEN:-/usr/bin/open}"
|
||||
DEFAULTS_BIN="${CMUX_OPEN_WRAPPER_DEFAULTS:-/usr/bin/defaults}"
|
||||
PYTHON3_BIN="${CMUX_OPEN_WRAPPER_PYTHON3:-}"
|
||||
|
||||
if [[ ! -x "$SYSTEM_OPEN_BIN" ]]; then
|
||||
SYSTEM_OPEN_BIN="/usr/bin/open"
|
||||
|
|
@ -17,6 +18,14 @@ if [[ ! -x "$DEFAULTS_BIN" ]]; then
|
|||
DEFAULTS_BIN="/usr/bin/defaults"
|
||||
fi
|
||||
|
||||
if [[ -n "$PYTHON3_BIN" ]]; then
|
||||
if [[ ! -x "$PYTHON3_BIN" ]]; then
|
||||
PYTHON3_BIN=""
|
||||
fi
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON3_BIN="$(command -v python3)"
|
||||
fi
|
||||
|
||||
settings_domain="${CMUX_BUNDLE_ID:-}"
|
||||
whitelist_raw=""
|
||||
whitelist_patterns=()
|
||||
|
|
@ -32,10 +41,74 @@ trim() {
|
|||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
to_lower_ascii() {
|
||||
# Bash 3.2-compatible lowercase conversion.
|
||||
LC_ALL=C printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
normalize_boolean() {
|
||||
to_lower_ascii "$(trim "$1")"
|
||||
}
|
||||
|
||||
is_false_setting() {
|
||||
local normalized
|
||||
normalized="$(normalize_boolean "$1")"
|
||||
case "$normalized" in
|
||||
0|false|no|off)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
canonicalize_idn_host() {
|
||||
local value="$1"
|
||||
[[ -z "$PYTHON3_BIN" ]] && {
|
||||
printf '%s' "$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
local canonicalized
|
||||
canonicalized="$("$PYTHON3_BIN" - "$value" <<'PY' 2>/dev/null || true
|
||||
import sys
|
||||
|
||||
host = sys.argv[1].strip().rstrip(".")
|
||||
if not host:
|
||||
raise SystemExit(1)
|
||||
|
||||
labels = host.split(".")
|
||||
if any(not label for label in labels):
|
||||
raise SystemExit(1)
|
||||
|
||||
try:
|
||||
canonical = ".".join(label.encode("idna").decode("ascii") for label in labels)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
|
||||
sys.stdout.write(canonical.lower())
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$canonicalized" ]]; then
|
||||
printf '%s' "$canonicalized"
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
is_http_url() {
|
||||
local value="$1"
|
||||
case "$value" in
|
||||
[Hh][Tt][Tt][Pp]://*|[Hh][Tt][Tt][Pp][Ss]://*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
normalize_host() {
|
||||
local value
|
||||
value="$(trim "$1")"
|
||||
value="${value,,}"
|
||||
value="$(to_lower_ascii "$value")"
|
||||
[[ -z "$value" ]] && return 1
|
||||
|
||||
if [[ "$value" == *"://"* ]]; then
|
||||
|
|
@ -68,13 +141,14 @@ normalize_host() {
|
|||
done
|
||||
|
||||
[[ -z "$value" ]] && return 1
|
||||
value="$(canonicalize_idn_host "$value")"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
normalize_whitelist_pattern() {
|
||||
local value
|
||||
value="$(trim "$1")"
|
||||
value="${value,,}"
|
||||
value="$(to_lower_ascii "$value")"
|
||||
[[ -z "$value" ]] && return 1
|
||||
|
||||
if [[ "$value" == \*.* ]]; then
|
||||
|
|
@ -152,13 +226,14 @@ for arg in "$@"; do
|
|||
passthrough=true
|
||||
break
|
||||
;;
|
||||
http://*|https://*)
|
||||
urls+=("$arg")
|
||||
;;
|
||||
*)
|
||||
# Non-URL, non-flag argument (file path, etc.) → pass through all
|
||||
passthrough=true
|
||||
break
|
||||
if is_http_url "$arg"; then
|
||||
urls+=("$arg")
|
||||
else
|
||||
# Non-URL, non-flag argument (file path, etc.) → pass through all
|
||||
passthrough=true
|
||||
break
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -174,11 +249,9 @@ if [[ -n "$settings_domain" ]]; then
|
|||
# Backward compatibility for installs that predate the dedicated open-wrapper toggle.
|
||||
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserOpenTerminalLinksInCmuxBrowser 2>/dev/null || true)"
|
||||
fi
|
||||
case "${open_in_cmux,,}" in
|
||||
0|false|no)
|
||||
system_open "$@"
|
||||
;;
|
||||
esac
|
||||
if is_false_setting "$open_in_cmux"; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
whitelist_raw="$("$DEFAULTS_BIN" read "$settings_domain" browserHostWhitelist 2>/dev/null || true)"
|
||||
if [[ -n "$whitelist_raw" ]]; then
|
||||
|
|
|
|||
|
|
@ -65,21 +65,21 @@ fi
|
|||
key="${3:-}"
|
||||
case "$key" in
|
||||
browserInterceptTerminalOpenCommandInCmuxBrowser)
|
||||
if [[ -v FAKE_DEFAULTS_INTERCEPT_OPEN ]]; then
|
||||
if [[ "${FAKE_DEFAULTS_INTERCEPT_OPEN+x}" == "x" ]]; then
|
||||
printf '%s\\n' "$FAKE_DEFAULTS_INTERCEPT_OPEN"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
browserOpenTerminalLinksInCmuxBrowser)
|
||||
if [[ -v FAKE_DEFAULTS_LEGACY_OPEN ]]; then
|
||||
if [[ "${FAKE_DEFAULTS_LEGACY_OPEN+x}" == "x" ]]; then
|
||||
printf '%s\\n' "$FAKE_DEFAULTS_LEGACY_OPEN"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
browserHostWhitelist)
|
||||
if [[ -v FAKE_DEFAULTS_WHITELIST ]]; then
|
||||
if [[ "${FAKE_DEFAULTS_WHITELIST+x}" == "x" ]]; then
|
||||
printf '%s' "$FAKE_DEFAULTS_WHITELIST"
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -97,7 +97,10 @@ esac
|
|||
"""#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\\n' "$*" >> "$FAKE_CMUX_LOG"
|
||||
url="${*: -1}"
|
||||
url=""
|
||||
for arg in "$@"; do
|
||||
url="$arg"
|
||||
done
|
||||
if [[ -n "${FAKE_CMUX_FAIL_URLS:-}" ]]; then
|
||||
IFS=',' read -r -a failures <<< "$FAKE_CMUX_FAIL_URLS"
|
||||
for fail_url in "${failures[@]}"; do
|
||||
|
|
@ -139,7 +142,7 @@ exit 0
|
|||
env.pop("FAKE_CMUX_FAIL_URLS", None)
|
||||
|
||||
result = subprocess.run(
|
||||
[str(wrapper), *args],
|
||||
["/bin/bash", str(wrapper), *args],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
|
@ -166,6 +169,26 @@ def test_toggle_disabled_passthrough(failures: list[str]) -> None:
|
|||
expect(open_log == [url], f"toggle off: expected system open [{url}], got {open_log}", failures)
|
||||
|
||||
|
||||
def test_toggle_disabled_case_insensitive_passthrough(failures: list[str]) -> None:
|
||||
url = "https://example.com"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
args=[url],
|
||||
intercept_setting=" FaLsE ",
|
||||
whitelist="",
|
||||
)
|
||||
expect(code == 0, f"toggle off (case-insensitive): wrapper exited {code}: {stderr}", failures)
|
||||
expect(
|
||||
cmux_log == [],
|
||||
f"toggle off (case-insensitive): cmux should not be called, got {cmux_log}",
|
||||
failures,
|
||||
)
|
||||
expect(
|
||||
open_log == [url],
|
||||
f"toggle off (case-insensitive): expected system open [{url}], got {open_log}",
|
||||
failures,
|
||||
)
|
||||
|
||||
|
||||
def test_whitelist_miss_passthrough(failures: list[str]) -> None:
|
||||
url = "https://example.com"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
|
|
@ -226,13 +249,75 @@ def test_legacy_toggle_fallback_passthrough(failures: list[str]) -> None:
|
|||
expect(open_log == [url], f"legacy fallback: expected system open [{url}], got {open_log}", failures)
|
||||
|
||||
|
||||
def test_legacy_toggle_fallback_case_insensitive_passthrough(failures: list[str]) -> None:
|
||||
url = "https://example.com"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
args=[url],
|
||||
intercept_setting=None,
|
||||
legacy_open_setting=" Off ",
|
||||
whitelist="",
|
||||
)
|
||||
expect(code == 0, f"legacy fallback (case-insensitive): wrapper exited {code}: {stderr}", failures)
|
||||
expect(
|
||||
cmux_log == [],
|
||||
f"legacy fallback (case-insensitive): cmux should not be called, got {cmux_log}",
|
||||
failures,
|
||||
)
|
||||
expect(
|
||||
open_log == [url],
|
||||
f"legacy fallback (case-insensitive): expected system open [{url}], got {open_log}",
|
||||
failures,
|
||||
)
|
||||
|
||||
|
||||
def test_uppercase_scheme_routes_to_cmux(failures: list[str]) -> None:
|
||||
url = "HTTPS://api.example.com/path?q=1"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
args=[url],
|
||||
intercept_setting="1",
|
||||
whitelist="*.example.com",
|
||||
)
|
||||
expect(code == 0, f"uppercase scheme: wrapper exited {code}: {stderr}", failures)
|
||||
expect(open_log == [], f"uppercase scheme: system open should not be called, got {open_log}", failures)
|
||||
expect(cmux_log == [f"browser open {url}"], f"uppercase scheme: unexpected cmux log {cmux_log}", failures)
|
||||
|
||||
|
||||
def test_unicode_whitelist_matches_punycode_url(failures: list[str]) -> None:
|
||||
url = "https://xn--bcher-kva.example/path"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
args=[url],
|
||||
intercept_setting="1",
|
||||
whitelist="bücher.example",
|
||||
)
|
||||
expect(code == 0, f"unicode whitelist: wrapper exited {code}: {stderr}", failures)
|
||||
expect(open_log == [], f"unicode whitelist: system open should not be called, got {open_log}", failures)
|
||||
expect(cmux_log == [f"browser open {url}"], f"unicode whitelist: unexpected cmux log {cmux_log}", failures)
|
||||
|
||||
|
||||
def test_punycode_whitelist_matches_unicode_url(failures: list[str]) -> None:
|
||||
url = "https://bücher.example/path"
|
||||
open_log, cmux_log, code, stderr = run_wrapper(
|
||||
args=[url],
|
||||
intercept_setting="1",
|
||||
whitelist="xn--bcher-kva.example",
|
||||
)
|
||||
expect(code == 0, f"punycode whitelist: wrapper exited {code}: {stderr}", failures)
|
||||
expect(open_log == [], f"punycode whitelist: system open should not be called, got {open_log}", failures)
|
||||
expect(cmux_log == [f"browser open {url}"], f"punycode whitelist: unexpected cmux log {cmux_log}", failures)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures: list[str] = []
|
||||
test_toggle_disabled_passthrough(failures)
|
||||
test_toggle_disabled_case_insensitive_passthrough(failures)
|
||||
test_whitelist_miss_passthrough(failures)
|
||||
test_whitelist_match_routes_to_cmux(failures)
|
||||
test_partial_failures_only_fallback_failed_urls(failures)
|
||||
test_legacy_toggle_fallback_passthrough(failures)
|
||||
test_legacy_toggle_fallback_case_insensitive_passthrough(failures)
|
||||
test_uppercase_scheme_routes_to_cmux(failures)
|
||||
test_unicode_whitelist_matches_punycode_url(failures)
|
||||
test_punycode_whitelist_matches_unicode_url(failures)
|
||||
|
||||
if failures:
|
||||
print("open wrapper regression tests failed:")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue