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/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/roles/admin/tasks/account.yml b/roles/admin/tasks/account.yml index bd0a4f0..d965aeb 100644 --- a/roles/admin/tasks/account.yml +++ b/roles/admin/tasks/account.yml @@ -11,12 +11,13 @@ 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: Create administrative SSH directory ansible.builtin.file: path: "{{ megaproxy_admin_account.home }}/.ssh" diff --git a/tests/integration/installer.exp b/tests/integration/installer.exp new file mode 100644 index 0000000..7b4603f --- /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 {cat /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..5a92255 --- /dev/null +++ b/tests/integration/installer.sh @@ -0,0 +1,53 @@ +#!/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 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}') +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 + 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" +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 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