From b2a962aac26fb794b2de74cfac6f61ebeed2ac88 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:26:35 +0300 Subject: [PATCH 1/8] Test interactive bash installation on clean LXD hosts --- .github/workflows/integration.yml | 22 +++++++++++++++ tests/integration/installer.exp | 39 ++++++++++++++++++++++++++ tests/integration/installer.sh | 46 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 tests/integration/installer.exp create mode 100755 tests/integration/installer.sh diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index bca25fe..747309b 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -132,3 +132,25 @@ jobs: - name: Provision and verify two hosts and Jump run: tests/integration/lxd.sh images:debian/12/cloud 2 + + installer: + name: Bash installer on ${{ matrix.image }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + image: ['ubuntu:22.04', 'ubuntu:24.04', 'images:debian/12/cloud'] + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Prepare LXD and terminal driver + run: | + sudo apt-get update + sudo apt-get install -y expect + sudo snap install lxd --channel=5.21/stable || sudo snap refresh lxd --channel=5.21/stable + sudo lxd waitready + sudo lxd init --auto + - name: Install from scratch through bash and verify proxy + run: tests/integration/installer.sh '${{ matrix.image }}' diff --git a/tests/integration/installer.exp b/tests/integration/installer.exp new file mode 100644 index 0000000..2c983b7 --- /dev/null +++ b/tests/integration/installer.exp @@ -0,0 +1,39 @@ +#!/usr/bin/expect -f +set timeout 1200 +set guest [lindex $argv 0] +set address [lindex $argv 1] +set key [lindex $argv 2] +proc answer {prompt value} { + expect -ex $prompt + after 150 + send -- $value +} +spawn sudo lxc exec $guest --mode interactive -- env TERM=xterm-256color MEGAPROXY_REF=installer-ci GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=url.file:///tmp/source.git.insteadOf GIT_CONFIG_VALUE_0=https://github.com/andre487/MegaProxyServer.git bash -c {curl -fsSL file:///tmp/install.sh | bash} +answer {Continue?} "y\r" +answer {Inventory host name} "installer\r" +answer {Public IP address or DNS name} "$address\r" +answer {Permanent administrative user} "\025megaproxy-admin\r" +answer {Administrative SSH port} "\r" +answer {Administrative SSH key} "\r" +answer {Services on this host} " \r" +answer {HTTPS endpoint} "\r" +answer {Certificate type} "\033\[B\r" +answer {Enable active-probe resistance?} "n" +answer {Enable HTTP/3} "n" +answer {HTTPS user login} "installer-user\r" +answer {Generate a strong password?} "n" +answer {Password} "installer-test-password-only\r" +answer {Add another HTTPS user?} "n" +answer {HTTPS profile title} "\r" +answer {Ansible Vault password:} "installer-vault-test-only\r" +expect -ex {Key saved outside this server} +exec sudo lxc file pull $guest/opt/megaproxy-server/.secrets/keys/installer/admin-megaproxy-admin $key +send -- "y" +expect { + -ex {Setup verified. Client configurations} {} + eof { puts stderr "Installer exited before verification"; exit 1 } + timeout { puts stderr "Installer timed out"; exit 1 } +} +expect eof +set result [wait] +exit [lindex $result 3] diff --git a/tests/integration/installer.sh b/tests/integration/installer.sh new file mode 100755 index 0000000..8c46e42 --- /dev/null +++ b/tests/integration/installer.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail +image=${1:-ubuntu:24.04} +guest=megaproxy-ci-installer +work_dir=$(mktemp -d) +cleanup() { + sudo lxc delete --force "$guest" >/dev/null 2>&1 || true + rm -rf "$work_dir" +} +trap cleanup EXIT +sudo iptables -P FORWARD ACCEPT +sudo ip6tables -P FORWARD ACCEPT +sudo lxc launch "$image" "$guest" -c security.nesting=true -c security.privileged=true +for _ in $(seq 1 60); do + state=$(sudo lxc exec "$guest" -- systemctl is-system-running || true) + [[ "$state" == running || "$state" == degraded ]] && break + sleep 2 +done +sudo lxc exec "$guest" -- sh -c "sed -i 's|http://|https://|g' /etc/apt/sources.list 2>/dev/null || true; sed -i 's|http://|https://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true" +# Clone the exact tested revision, including PR merge commits not available as a remote branch. +git clone --bare . "$work_dir/source.git" +git --git-dir="$work_dir/source.git" update-ref refs/heads/installer-ci HEAD +sudo lxc file push -r "$work_dir/source.git" "$guest/tmp/" +sudo lxc file push install.sh "$guest/tmp/install.sh" +address=$(sudo lxc list "$guest" -c 4 --format csv | awk '{print $1}') +expect tests/integration/installer.exp "$guest" "$address" "$work_dir/admin-key" +sudo chown "$(id -u):$(id -g)" "$work_dir/admin-key" +chmod 600 "$work_dir/admin-key" +sudo lxc exec "$guest" -- cat /etc/ssh/ssh_host_ed25519_key.pub | awk -v host="$address" '{print host " " $0}' > "$work_dir/known_hosts" +ssh -i "$work_dir/admin-key" -o BatchMode=yes -o IdentitiesOnly=yes -o UserKnownHostsFile="$work_dir/known_hosts" "megaproxy-admin@$address" 'sudo -n true; sudo sshd -T | grep -x "permitrootlogin no"' +sudo lxc exec "$guest" -- test -s /opt/megaproxy-server/.generated/configs/MegaProxy.json +sudo lxc exec "$guest" -- systemctl is-active gost-hashed-auth megaproxy-gost haproxy +sudo lxc exec "$guest" -- sh -c 'mkdir -p /tmp/installer-target; echo installer-ok > /tmp/installer-target/index.html' +sudo lxc exec "$guest" -- systemd-run --unit=installer-target python3 -m http.server 18082 --bind 127.0.0.1 --directory /tmp/installer-target +for _ in $(seq 1 20); do + if response=$(curl --silent --show-error --fail --max-time 10 --noproxy '' --proxy-insecure --proxy "https://$address:443" --proxy-user installer-user:installer-test-password-only http://127.0.0.1:18082/); then + [[ "$response" == installer-ok ]] + break + fi + sleep 1 +done +[[ "${response:-}" == installer-ok ]] +if curl --silent --fail --max-time 10 --noproxy '' --proxy-insecure --proxy "https://$address:443" --proxy-user installer-user:wrong-password http://127.0.0.1:18082/; then + echo 'Incorrect proxy password accepted' >&2 + exit 1 +fi From 4f4e4f36a7c2ba3f699c6bd3f3191ef1ead2ea20 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:29:45 +0300 Subject: [PATCH 2/8] Feed installer without requiring curl in minimal images --- tests/integration/installer.exp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/installer.exp b/tests/integration/installer.exp index 2c983b7..7b4603f 100644 --- a/tests/integration/installer.exp +++ b/tests/integration/installer.exp @@ -8,7 +8,7 @@ proc answer {prompt value} { after 150 send -- $value } -spawn sudo lxc exec $guest --mode interactive -- env TERM=xterm-256color MEGAPROXY_REF=installer-ci GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=url.file:///tmp/source.git.insteadOf GIT_CONFIG_VALUE_0=https://github.com/andre487/MegaProxyServer.git bash -c {curl -fsSL file:///tmp/install.sh | bash} +spawn sudo lxc exec $guest --mode interactive -- env TERM=xterm-256color MEGAPROXY_REF=installer-ci GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=url.file:///tmp/source.git.insteadOf GIT_CONFIG_VALUE_0=https://github.com/andre487/MegaProxyServer.git bash -c {cat /tmp/install.sh | bash} answer {Continue?} "y\r" answer {Inventory host name} "installer\r" answer {Public IP address or DNS name} "$address\r" From 41de53e73bead9b696af4fe54c8ad95bf67e8fda Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:34:27 +0300 Subject: [PATCH 3/8] Fix ownership of installer test Git source --- tests/integration/installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/installer.sh b/tests/integration/installer.sh index 8c46e42..2dd59a3 100755 --- a/tests/integration/installer.sh +++ b/tests/integration/installer.sh @@ -21,6 +21,7 @@ sudo lxc exec "$guest" -- sh -c "sed -i 's|http://|https://|g' /etc/apt/sources. git clone --bare . "$work_dir/source.git" git --git-dir="$work_dir/source.git" update-ref refs/heads/installer-ci HEAD sudo lxc file push -r "$work_dir/source.git" "$guest/tmp/" +sudo lxc exec "$guest" -- chown -R root:root /tmp/source.git sudo lxc file push install.sh "$guest/tmp/install.sh" address=$(sudo lxc list "$guest" -c 4 --format csv | awk '{print $1}') expect tests/integration/installer.exp "$guest" "$address" "$work_dir/admin-key" From 388b7d0e481bf002779b780af70bfbd3b282b7fa Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:38:36 +0300 Subject: [PATCH 4/8] Capture SSH diagnostics on clean installer failure --- tests/integration/installer.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integration/installer.sh b/tests/integration/installer.sh index 2dd59a3..ff774b6 100755 --- a/tests/integration/installer.sh +++ b/tests/integration/installer.sh @@ -24,7 +24,12 @@ sudo lxc file push -r "$work_dir/source.git" "$guest/tmp/" sudo lxc exec "$guest" -- chown -R root:root /tmp/source.git sudo lxc file push install.sh "$guest/tmp/install.sh" address=$(sudo lxc list "$guest" -c 4 --format csv | awk '{print $1}') -expect tests/integration/installer.exp "$guest" "$address" "$work_dir/admin-key" +if ! expect tests/integration/installer.exp "$guest" "$address" "$work_dir/admin-key"; then + sudo lxc exec "$guest" -- journalctl -u ssh -b --no-pager -n 40 || true + sudo lxc exec "$guest" -- namei -l /home/megaproxy-admin/.ssh/authorized_keys || true + sudo lxc exec "$guest" -- passwd -S megaproxy-admin || true + exit 1 +fi sudo chown "$(id -u):$(id -g)" "$work_dir/admin-key" chmod 600 "$work_dir/admin-key" sudo lxc exec "$guest" -- cat /etc/ssh/ssh_host_ed25519_key.pub | awk -v host="$address" '{print host " " $0}' > "$work_dir/known_hosts" From a4501d0f8d6b5ff540c674dfe1d7aa51abc3ab4c Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:43:58 +0300 Subject: [PATCH 5/8] Trace administrative home during clean local bootstrap --- roles/admin/tasks/account.yml | 4 ++++ tests/integration/installer.sh | 1 + 2 files changed, 5 insertions(+) diff --git a/roles/admin/tasks/account.yml b/roles/admin/tasks/account.yml index bd0a4f0..6d7b0ed 100644 --- a/roles/admin/tasks/account.yml +++ b/roles/admin/tasks/account.yml @@ -17,6 +17,10 @@ append: true register: megaproxy_admin_account +- name: Inspect administrative home returned by user creation + ansible.builtin.debug: + msg: "{{ megaproxy_admin_account.home }}" + - name: Create administrative SSH directory ansible.builtin.file: path: "{{ megaproxy_admin_account.home }}/.ssh" diff --git a/tests/integration/installer.sh b/tests/integration/installer.sh index ff774b6..5a92255 100755 --- a/tests/integration/installer.sh +++ b/tests/integration/installer.sh @@ -28,6 +28,7 @@ if ! expect tests/integration/installer.exp "$guest" "$address" "$work_dir/admin sudo lxc exec "$guest" -- journalctl -u ssh -b --no-pager -n 40 || true sudo lxc exec "$guest" -- namei -l /home/megaproxy-admin/.ssh/authorized_keys || true sudo lxc exec "$guest" -- passwd -S megaproxy-admin || true + sudo lxc exec "$guest" -- find /root /home -name authorized_keys -printf "%p %u %m\n" || true exit 1 fi sudo chown "$(id -u):$(id -g)" "$work_dir/admin-key" From 148fa90824c256a7eaf6570c2867bc89a25d6209 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:50:01 +0300 Subject: [PATCH 6/8] Avoid corrupting admin home through Ansible password redaction --- roles/admin/tasks/account.yml | 5 +---- tests/test_admin_role.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 tests/test_admin_role.py diff --git a/roles/admin/tasks/account.yml b/roles/admin/tasks/account.yml index 6d7b0ed..d965aeb 100644 --- a/roles/admin/tasks/account.yml +++ b/roles/admin/tasks/account.yml @@ -11,15 +11,12 @@ name: "{{ megaproxy_admin.user }}" state: present create_home: true - password: x + password: "{{ lookup('ansible.builtin.password', '/dev/null', length=64) | password_hash('sha512') }}" update_password: on_create groups: sudo append: true register: megaproxy_admin_account -- name: Inspect administrative home returned by user creation - ansible.builtin.debug: - msg: "{{ megaproxy_admin_account.home }}" - name: Create administrative SSH directory ansible.builtin.file: diff --git a/tests/test_admin_role.py b/tests/test_admin_role.py new file mode 100644 index 0000000..0a1c051 --- /dev/null +++ b/tests/test_admin_role.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from ruamel.yaml import YAML + + +def test_admin_password_does_not_redact_home_path(): + tasks = YAML(typ='safe').load(Path('roles/admin/tasks/account.yml')) + user = next(task['ansible.builtin.user'] for task in tasks if 'ansible.builtin.user' in task) + assert "password_hash('sha512')" in user['password'] + assert "lookup('ansible.builtin.password', '/dev/null', length=64)" in user['password'] + assert user['update_password'] == 'on_create' From 8074fe818b7cdaf5ad11cbe75bd3952d72db14a5 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 14:50:19 +0300 Subject: [PATCH 7/8] Use real installer SSH login as admin home regression coverage --- tests/test_admin_role.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 tests/test_admin_role.py diff --git a/tests/test_admin_role.py b/tests/test_admin_role.py deleted file mode 100644 index 0a1c051..0000000 --- a/tests/test_admin_role.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -from ruamel.yaml import YAML - - -def test_admin_password_does_not_redact_home_path(): - tasks = YAML(typ='safe').load(Path('roles/admin/tasks/account.yml')) - user = next(task['ansible.builtin.user'] for task in tasks if 'ansible.builtin.user' in task) - assert "password_hash('sha512')" in user['password'] - assert "lookup('ansible.builtin.password', '/dev/null', length=64)" in user['password'] - assert user['update_password'] == 'on_create' From 63a2cc9fe0900b87aab7e7271bdf67ea339f04ee Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Mon, 2 Nov 2026 15:02:49 +0300 Subject: [PATCH 8/8] Restore piped stdin when interactive installer returns --- install.sh | 2 +- tests/test_local_setup.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 42b5f12..f923873 100755 --- a/install.sh +++ b/install.sh @@ -54,4 +54,4 @@ main() { ./mega-proxy setup-local } -main "$@" +main "$@" 3<&0 <&3 diff --git a/tests/test_local_setup.py b/tests/test_local_setup.py index 62e8591..8b70180 100644 --- a/tests/test_local_setup.py +++ b/tests/test_local_setup.py @@ -137,3 +137,11 @@ def test_installer_can_be_piped_and_is_valid_bash(): result = sp.run(['bash', '-s', '--', '--help'], input=script, text=True, capture_output=True) assert result.returncode == 0 assert 'Interactive Debian/Ubuntu' in result.stdout + + +def test_installer_restores_piped_stdin_after_terminal_prompts(): + invocation = Path('install.sh').read_text().splitlines()[-1] + script = 'main() { exec