From e50ec3abb0c355a0ac7c41471479476303419a73 Mon Sep 17 00:00:00 2001 From: Mammad <108564287+Mammad3861@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:58:39 +0330 Subject: [PATCH 1/4] Fix Chromium startup compatibility --- .gitattributes | 1 + README.md | 42 +++++++---- browser.sh | 143 +++++++++++++++++++++++++----------- tests/browser_test.sh | 165 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 55 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/browser_test.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/README.md b/README.md index 0885edf..e2412de 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,35 @@ It keeps an interactive menu for installing, uninstalling, checking status, and - Persistent config in `/opt/instant-linux-browser//config`. - Username/password prompts, with `ILB_USERNAME` and `ILB_PASSWORD` overrides. - Supports amd64/x86_64 and arm64/aarch64 Linux servers when the upstream image supports them. -- Works when downloaded first or when run through `curl | sudo bash`. +- Keeps the one-command interactive installer and direct actions for automation. ## Install Use only `raw.githubusercontent.com` URLs for curl commands. Do not use normal GitHub page URLs such as `github.com/.../blob/...`, because those return an HTML page, not the raw Bash script. -### Interactive +### One-command interactive installer + +After this branch is merged into `main`: ```bash -curl -fsSLO https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/main/browser.sh -sudo bash browser.sh +curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/main/browser.sh | sudo bash ``` -### One-line interactive +The script reads menu choices and credentials from your controlling terminal, so this remains interactive even though the script is streamed through standard input. + +### Branch validation + +Before merge, after `fix/chromium-runtime-debug` is pushed, use this branch URL on a VPS: ```bash -curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/main/browser.sh | sudo bash +curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/fix/chromium-runtime-debug/browser.sh | sudo bash +``` + +### Download first + +```bash +curl -fsSLO https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/main/browser.sh +sudo bash browser.sh ``` ### Non-interactive Chromium @@ -61,14 +73,14 @@ sudo bash browser.sh install-chromium sudo bash browser.sh install-firefox sudo bash browser.sh uninstall-chromium sudo bash browser.sh uninstall-firefox -sudo bash browser.sh status +sudo bash browser.sh diagnostics ``` Environment action form: ```bash sudo ILB_ACTION=install-chromium bash browser.sh -sudo ILB_ACTION=status bash browser.sh +sudo ILB_ACTION=diagnostics bash browser.sh ``` ## Menu Options @@ -77,18 +89,20 @@ sudo ILB_ACTION=status bash browser.sh - `2` - Uninstall Chromium - `3` - Install Firefox - `4` - Uninstall Firefox -- `5` - Show status/diagnostics +- `5` - Diagnostics - `6` - Exit -## Chromium Startup Flags +## Chromium Compatibility Chromium is launched with these flags by default: ```text ---no-sandbox --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage --disable-setuid-sandbox +--no-sandbox --disable-gpu --disable-dev-shm-usage --disable-setuid-sandbox ``` -The script passes them to the linuxserver/chromium container through both `CHROME_CLI` and `CHROME_FLAGS`. +The script passes these flags through `CHROME_CLI` and retains `CHROME_FLAGS` for older setups. It also sets `PIXELFLUX_WAYLAND=false` to use the compatible X11 path on headless VPS servers. + +Chromium startup waits briefly for both the container and Chromium process. If either fails, the installer prints recent logs and focused Docker commands for diagnosis. To override them: @@ -107,7 +121,9 @@ Before starting a browser, the script: - pulls the selected Docker image; - creates the persistent config directory; - starts the container; -- verifies that the container is still running. +- verifies that the container is running and, for Chromium, that its process exists. + +`diagnostics` is read-only: it reports Docker state when available and never installs Docker or starts its daemon. If startup fails, check logs: diff --git a/browser.sh b/browser.sh index 95a4a25..a44716d 100644 --- a/browser.sh +++ b/browser.sh @@ -3,7 +3,7 @@ # ========================================================== # Project: Instant Linux Browser (Docker-based) # Author: Mammad3861 -# Version: 1.2.1 - Stable menu/curl dispatch + Docker reliability fixes +# Version: 1.2.2 - Chromium compatibility and startup checks # Description: Deploy web-accessible Chromium and Firefox containers. # ========================================================== @@ -17,17 +17,16 @@ YELLOW='\033[1;33m' NC='\033[0m' CONFIG_BASE="${CONFIG_BASE:-/opt/instant-linux-browser}" -CHROMIUM_FLAGS="${CHROMIUM_FLAGS:---no-sandbox --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage --disable-setuid-sandbox}" +CHROMIUM_FLAGS="${CHROMIUM_FLAGS:---no-sandbox --disable-gpu --disable-dev-shm-usage --disable-setuid-sandbox}" info() { echo -e "${CYAN}$*${NC}"; } success() { echo -e "${GREEN}$*${NC}"; } warn() { echo -e "${YELLOW}$*${NC}"; } die() { echo -e "${RED}Error: $*${NC}" >&2; exit 1; } -# Root check -if [[ ${EUID:-$(id -u)} -ne 0 ]]; then - die "This script must be run with sudo or as root. Example: curl -fsSL URL | sudo bash" -fi +require_root() { + [[ ${EUID:-$(id -u)} -eq 0 ]] || die "This action must be run with sudo or as root." +} # Detect Timezone SERVER_TZ=$(cat /etc/timezone 2>/dev/null || echo "Etc/UTC") @@ -35,7 +34,7 @@ SERVER_TZ=$(cat /etc/timezone 2>/dev/null || echo "Etc/UTC") # Use the real terminal for prompts. This keeps the menu working even when # the script is executed with: curl -fsSL .../browser.sh | sudo bash TTY_INPUT="" -if [[ -r /dev/tty ]]; then +if [[ -r /dev/tty && -w /dev/tty ]]; then TTY_INPUT="/dev/tty" fi @@ -45,9 +44,10 @@ prompt_text() { local value="" if [[ -n "$TTY_INPUT" ]]; then - read -r -p "$prompt" value < "$TTY_INPUT" + printf "%s" "$prompt" > "$TTY_INPUT" + read -r value < "$TTY_INPUT" else - read -r -p "$prompt" value || true + read -r value || true fi echo "${value:-$default_value}" @@ -58,11 +58,12 @@ prompt_secret() { local value="" if [[ -n "$TTY_INPUT" ]]; then - read -r -s -p "$prompt" value < "$TTY_INPUT" - echo > /dev/tty + printf "%s" "$prompt" > "$TTY_INPUT" + read -r -s value < "$TTY_INPUT" + printf "\n" > "$TTY_INPUT" else - read -r -s -p "$prompt" value || true - echo + read -r -s value || true + printf "\n" fi echo "$value" @@ -113,7 +114,7 @@ ensure_docker_ready() { docker info >/dev/null 2>&1 || die "Docker is installed but not running/reachable. Start Docker and run this script again." } -# Resolve PUID/PGID (prefer the sudo user; fallback safely) +# Resolve PUID/PGID (prefer the sudo user; fallback to the conventional host user) resolve_puid_pgid() { local puid pgid if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then @@ -121,15 +122,8 @@ resolve_puid_pgid() { pgid=$(id -g "$SUDO_USER" 2>/dev/null) fi - if [[ -z "${puid:-}" || -z "${pgid:-}" ]]; then - if command -v getent >/dev/null 2>&1 && getent passwd 1000 >/dev/null 2>&1; then - puid=1000 - pgid=$(getent passwd 1000 | cut -d: -f4) - else - puid=0 - pgid=0 - fi - fi + puid="${puid:-1000}" + pgid="${pgid:-1000}" echo "$puid:$pgid" } @@ -153,6 +147,57 @@ container_running() { [[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || echo false)" == "true" ]] } +chromium_process_running() { + docker exec "$1" sh -c 'ps -eo comm=,args= 2>/dev/null | grep -Eq "[c]hromium|[c]hrome"' >/dev/null 2>&1 +} + +print_recent_logs() { + local container="$1" + local secret="${2:-}" + local line + + if [[ -z "$secret" ]]; then + docker logs --tail 80 "$container" 2>&1 || true + return + fi + + while IFS= read -r line; do + printf '%s\n' "${line//"$secret"/[REDACTED]}" + done < <(docker logs --tail 80 "$container" 2>&1 || true) +} + +print_chromium_failure() { + local reason="$1" + local secret="${2:-}" + echo -e "${RED}Chromium startup check failed: $reason${NC}" >&2 + echo "Recent container logs:" >&2 + print_recent_logs chromium "$secret" + echo "Debug: sudo docker ps -a --filter name=chromium" >&2 + echo "Debug: sudo docker exec chromium ps -eo pid,comm,args" >&2 +} + +verify_chromium_startup() { + local secret="${1:-}" + local attempt + + for attempt in 1 2 3 4 5; do + if ! container_running chromium; then + print_chromium_failure "the Docker container exited" "$secret" + return 1 + fi + + if chromium_process_running chromium; then + info "Chromium process detected." + return 0 + fi + + sleep 1 + done + + print_chromium_failure "the container is running but no Chromium process was found" "$secret" + return 1 +} + check_port_available() { local port="$1" if command -v ss >/dev/null 2>&1 && ss -ltnH "sport = :$port" 2>/dev/null | grep -q .; then @@ -209,6 +254,7 @@ install_browser() { local port="$3" local ssl_port=$((port + 1)) + require_root ensure_docker_ready show_arch_info @@ -252,23 +298,16 @@ install_browser() { mkdir -p "$config_dir" chown -R "${puid}:${pgid}" "${CONFIG_BASE}/${browser}" 2>/dev/null || true - local extra_caps=() - local extra_security=() local browser_env=() if [[ "$browser" == "chromium" ]]; then - # These flags help Chromium start on restricted VPS/Docker environments. - extra_caps+=(--cap-add=SYS_ADMIN) - extra_security+=(--security-opt seccomp=unconfined) - browser_env+=(-e "CHROME_CLI=$CHROMIUM_FLAGS" -e "CHROME_FLAGS=$CHROMIUM_FLAGS") + browser_env+=(-e "PIXELFLUX_WAYLAND=false" -e "CHROME_CLI=$CHROMIUM_FLAGS" -e "CHROME_FLAGS=$CHROMIUM_FLAGS") info "Applying Chromium flags through CHROME_CLI and CHROME_FLAGS: $CHROMIUM_FLAGS" fi info "Deploying $browser... Please wait." if ! docker run -d \ --name="$browser" \ - "${extra_caps[@]}" \ - "${extra_security[@]}" \ -e "PUID=$puid" \ -e "PGID=$pgid" \ -e "TZ=$SERVER_TZ" \ @@ -284,19 +323,22 @@ install_browser() { echo "Docker failed to start $browser." if container_exists "$browser"; then echo "Recent logs:" - docker logs --tail 80 "$browser" 2>/dev/null || true + print_recent_logs "$browser" "$password" fi die "Check ports, image availability, Docker permissions, and the logs above if present." fi - sleep 4 if ! container_running "$browser"; then echo -e "${RED}$browser did not start successfully.${NC}" echo "Recent logs:" - docker logs --tail 80 "$browser" 2>/dev/null || true + print_recent_logs "$browser" "$password" die "Container exited. Check the logs above." fi + if [[ "$browser" == "chromium" ]]; then + verify_chromium_startup "$password" || die "Chromium is not ready. Check the logs and debug commands above." + fi + local ip ip=$(detect_ip) @@ -304,7 +346,7 @@ install_browser() { success "Deployment Successful!" echo -e "Access URL (HTTP) : ${CYAN}http://${ip}:${port}${NC}" echo -e "Access URL (HTTPS): ${CYAN}https://${ip}:${ssl_port}${NC}" - echo -e "Credentials : ${YELLOW}$username / $password${NC}" + echo -e "Credentials : ${YELLOW}configured for $username${NC}" echo -e "${YELLOW}Note: Accept the SSL warning in your browser.${NC}" success "================================================" } @@ -312,16 +354,26 @@ install_browser() { uninstall_browser() { local browser="$1" info "Removing $browser..." + require_root ensure_docker_ready docker stop "$browser" >/dev/null 2>&1 || true docker rm "$browser" >/dev/null 2>&1 || true success "Cleanup complete. Persistent config was kept at ${CONFIG_BASE}/${browser}/config." } -show_status() { - ensure_docker_ready +show_diagnostics() { show_arch_info echo -e "${CYAN}Config base:${NC} ${CONFIG_BASE}" + if ! command -v docker >/dev/null 2>&1; then + echo "Docker: not installed" + return 0 + fi + + if ! docker info >/dev/null 2>&1; then + echo "Docker: installed but not reachable" + return 0 + fi + echo -e "${CYAN}--- Containers ---${NC}" docker ps -a --filter "name=chromium" --filter "name=firefox" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' } @@ -334,7 +386,7 @@ show_menu() { echo -e "2) Uninstall Chromium" echo -e "3) Install Firefox (HTTP 4000 / HTTPS 4001)" echo -e "4) Uninstall Firefox" - echo -e "5) Show Status" + echo -e "5) Diagnostics" echo -e "6) Exit" echo -e "${CYAN}==========================================${NC}" } @@ -342,9 +394,10 @@ show_menu() { read_menu_choice() { local choice="" if [[ -n "$TTY_INPUT" ]]; then - read -r -p "Select an option [1-6]: " choice < "$TTY_INPUT" + printf "Select an option [1-6]: " > "$TTY_INPUT" + read -r choice < "$TTY_INPUT" else - die "No interactive terminal was found. Download the script first and run 'sudo bash browser.sh', or run with ILB_ACTION=install-chromium." + die "No action and no controlling terminal. Use ILB_ACTION=install-chromium or download the script first." fi echo "$choice" } @@ -365,7 +418,7 @@ run_action() { uninstall_browser "firefox" ;; 5|status|diagnostics|diag) - show_status + show_diagnostics ;; 6|exit|quit) exit 0 @@ -384,9 +437,15 @@ main() { return fi + if [[ -z "$TTY_INPUT" ]]; then + die "No action and no controlling terminal. Use ILB_ACTION=install-chromium or download the script first." + fi + show_menu action=$(read_menu_choice) run_action "$action" } -main "$@" +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/tests/browser_test.sh b/tests/browser_test.sh new file mode 100644 index 0000000..c0c9b83 --- /dev/null +++ b/tests/browser_test.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$TEST_DIR/.." && pwd)" +SCRIPT="$PROJECT_DIR/browser.sh" +TEMP_DIR="$(mktemp -d)" +MOCK_BIN="$TEMP_DIR/bin" +MOCK_LOG="$TEMP_DIR/docker.log" +export MOCK_LOG + +cleanup() { + rm -rf "$TEMP_DIR" +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_contains() { + local expected="$1" + local file="$2" + grep -Fq -- "$expected" "$file" || fail "Expected '$expected' in $file" +} + +assert_not_contains() { + local unexpected="$1" + local file="$2" + if grep -Fq -- "$unexpected" "$file"; then + fail "Did not expect '$unexpected' in $file" + fi +} + +argument_index() { + local argument="$1" + awk -F '\t' -v argument="$argument" '{ for (field_index = 1; field_index <= NF; field_index++) if ($field_index == argument) { print field_index; exit } }' "$RUN_RECORD" +} + +assert_environment_before_image() { + local argument="$1" + local argument_index_value + local environment_flag_index + + argument_index_value="$(argument_index "$argument")" + environment_flag_index=$((argument_index_value - 1)) + + [[ -n "$argument_index_value" && "$argument_index_value" -lt "$IMAGE_INDEX" ]] || fail "$argument must occur before the image" + [[ "$(awk -F '\t' -v field="$environment_flag_index" '{ print $field }' "$RUN_RECORD")" == "-e" ]] || fail "$argument must be preceded by -e" +} + +mkdir -p "$MOCK_BIN" +cat > "$MOCK_BIN/docker" <<'MOCK_DOCKER' +#!/usr/bin/env bash +set -eu + +{ + printf 'docker' + for argument in "$@"; do + printf '\t%s' "$argument" + done + printf '\n' +} >> "$MOCK_LOG" + +case "${1:-}" in + info|pull|run|stop|rm|ps) + exit 0 + ;; + inspect) + printf 'true\n' + exit 0 + ;; + exec) + [[ "${MOCK_CHROMIUM_PROCESS:-1}" == "1" ]] + ;; + logs) + printf '%s\n' "${MOCK_LOG_LINE:-mock container log}" + ;; + *) + exit 0 + ;; +esac +MOCK_DOCKER +chmod +x "$MOCK_BIN/docker" + +bash -n "$SCRIPT" + +NO_ACTION_OUTPUT="$TEMP_DIR/no-action.out" +if ( + source "$SCRIPT" + TTY_INPUT="" + main +) > "$NO_ACTION_OUTPUT" 2>&1; then + fail "A non-interactive run without an action succeeded" +fi +assert_contains "No action and no controlling terminal" "$NO_ACTION_OUTPUT" + +INVALID_OUTPUT="$TEMP_DIR/invalid.out" +if PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" invalid-action > "$INVALID_OUTPUT" 2>&1; then + fail "An invalid action succeeded" +fi +assert_contains "Invalid option: invalid-action" "$INVALID_OUTPUT" + +DIAGNOSTICS_OUTPUT="$TEMP_DIR/diagnostics.out" +PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" diagnostics > "$DIAGNOSTICS_OUTPUT" 2>&1 +PATH="$MOCK_BIN:$PATH" ILB_ACTION=diagnostics bash "$SCRIPT" > /dev/null 2>&1 +assert_contains "--- Containers ---" "$DIAGNOSTICS_OUTPUT" +assert_not_contains "apt-get" "$MOCK_LOG" +assert_not_contains "systemctl" "$MOCK_LOG" + +if command -v script >/dev/null 2>&1; then + TTY_OUTPUT="$TEMP_DIR/tty.out" + printf '6\n' | script -qec "cat '$SCRIPT' | bash" /dev/null > "$TTY_OUTPUT" 2>&1 || fail "Streamed interactive menu failed" + assert_contains "Instant Linux Browser Installer" "$TTY_OUTPUT" +else + echo "SKIP: streamed TTY test requires the util-linux script command" +fi + +export PATH="$MOCK_BIN:$PATH" +export MOCK_LOG +export CONFIG_BASE="$TEMP_DIR/config" +source "$SCRIPT" + +if declare -f show_diagnostics | grep -Eq 'check_docker|ensure_docker_ready|apt-get|systemctl'; then + fail "Diagnostics must not install or start Docker" +fi + +require_root() { :; } +detect_arch() { echo x86_64; } +sleep() { :; } + +RESOLVED_IDS="$(SUDO_USER=root resolve_puid_pgid)" +[[ "$RESOLVED_IDS" == "1000:1000" ]] || fail "Expected safe UID/GID fallback, got $RESOLVED_IDS" + +: > "$MOCK_LOG" +INSTALL_OUTPUT="$TEMP_DIR/install.out" +ILB_USERNAME=admin ILB_PASSWORD=do-not-print-this install_browser chromium lscr.io/linuxserver/chromium:latest 3000 > "$INSTALL_OUTPUT" 2>&1 +assert_not_contains "do-not-print-this" "$INSTALL_OUTPUT" + +RUN_RECORD="$TEMP_DIR/docker-run.log" +awk -F '\t' '$2 == "run" { print; found = 1; exit } END { exit !found }' "$MOCK_LOG" > "$RUN_RECORD" || fail "Docker run invocation was not recorded" +IMAGE_INDEX="$(argument_index "lscr.io/linuxserver/chromium:latest")" +[[ -n "$IMAGE_INDEX" ]] || fail "Chromium image was not passed to docker run" +assert_environment_before_image "PIXELFLUX_WAYLAND=false" +assert_environment_before_image "CHROME_CLI=--no-sandbox --disable-gpu --disable-dev-shm-usage --disable-setuid-sandbox" +assert_not_contains "--disable-software-rasterizer" "$RUN_RECORD" + +LOG_OUTPUT="$TEMP_DIR/log.out" +export MOCK_LOG_LINE='do-not-print-this' +print_recent_logs chromium do-not-print-this > "$LOG_OUTPUT" +unset MOCK_LOG_LINE +assert_not_contains "do-not-print-this" "$LOG_OUTPUT" +assert_contains "[REDACTED]" "$LOG_OUTPUT" + +PROCESS_OUTPUT="$TEMP_DIR/process.out" +export MOCK_CHROMIUM_PROCESS=0 +if verify_chromium_startup > "$PROCESS_OUTPUT" 2>&1; then + fail "Missing Chromium process was reported as ready" +fi +unset MOCK_CHROMIUM_PROCESS +assert_contains "container is running but no Chromium process was found" "$PROCESS_OUTPUT" + +echo "PASS: browser.sh checks" From 852c9d2e603de3c9bdf0017a10713d4d8c2c7c70 Mon Sep 17 00:00:00 2001 From: Mammad <108564287+Mammad3861@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:09:28 +0330 Subject: [PATCH 2/4] Fix streamed installer execution --- README.md | 10 ---------- browser.sh | 35 +++++++++++++++++------------------ tests/browser_test.sh | 9 ++++++++- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e2412de..807d527 100644 --- a/README.md +++ b/README.md @@ -25,22 +25,12 @@ Use only `raw.githubusercontent.com` URLs for curl commands. Do not use normal G ### One-command interactive installer -After this branch is merged into `main`: - ```bash curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/main/browser.sh | sudo bash ``` The script reads menu choices and credentials from your controlling terminal, so this remains interactive even though the script is streamed through standard input. -### Branch validation - -Before merge, after `fix/chromium-runtime-debug` is pushed, use this branch URL on a VPS: - -```bash -curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/fix/chromium-runtime-debug/browser.sh | sudo bash -``` - ### Download first ```bash diff --git a/browser.sh b/browser.sh index a44716d..911d541 100644 --- a/browser.sh +++ b/browser.sh @@ -31,11 +31,10 @@ require_root() { # Detect Timezone SERVER_TZ=$(cat /etc/timezone 2>/dev/null || echo "Etc/UTC") -# Use the real terminal for prompts. This keeps the menu working even when -# the script is executed with: curl -fsSL .../browser.sh | sudo bash -TTY_INPUT="" -if [[ -r /dev/tty && -w /dev/tty ]]; then - TTY_INPUT="/dev/tty" +# Open the controlling terminal once so streamed scripts can still prompt. +TTY_FD="" +if { exec {TTY_FD}<>/dev/tty; } 2>/dev/null; then + : fi prompt_text() { @@ -43,9 +42,9 @@ prompt_text() { local default_value="${2:-}" local value="" - if [[ -n "$TTY_INPUT" ]]; then - printf "%s" "$prompt" > "$TTY_INPUT" - read -r value < "$TTY_INPUT" + if [[ -n "$TTY_FD" ]]; then + printf "%s" "$prompt" >&"$TTY_FD" + read -r value <&"$TTY_FD" else read -r value || true fi @@ -57,10 +56,10 @@ prompt_secret() { local prompt="$1" local value="" - if [[ -n "$TTY_INPUT" ]]; then - printf "%s" "$prompt" > "$TTY_INPUT" - read -r -s value < "$TTY_INPUT" - printf "\n" > "$TTY_INPUT" + if [[ -n "$TTY_FD" ]]; then + printf "%s" "$prompt" >&"$TTY_FD" + read -r -s value <&"$TTY_FD" + printf "\n" >&"$TTY_FD" else read -r -s value || true printf "\n" @@ -180,7 +179,7 @@ verify_chromium_startup() { local secret="${1:-}" local attempt - for attempt in 1 2 3 4 5; do + for ((attempt = 1; attempt <= 30; attempt++)); do if ! container_running chromium; then print_chromium_failure "the Docker container exited" "$secret" return 1 @@ -393,9 +392,9 @@ show_menu() { read_menu_choice() { local choice="" - if [[ -n "$TTY_INPUT" ]]; then - printf "Select an option [1-6]: " > "$TTY_INPUT" - read -r choice < "$TTY_INPUT" + if [[ -n "$TTY_FD" ]]; then + printf "Select an option [1-6]: " >&"$TTY_FD" + read -r choice <&"$TTY_FD" else die "No action and no controlling terminal. Use ILB_ACTION=install-chromium or download the script first." fi @@ -437,7 +436,7 @@ main() { return fi - if [[ -z "$TTY_INPUT" ]]; then + if [[ -z "$TTY_FD" ]]; then die "No action and no controlling terminal. Use ILB_ACTION=install-chromium or download the script first." fi @@ -446,6 +445,6 @@ main() { run_action "$action" } -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then +if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi diff --git a/tests/browser_test.sh b/tests/browser_test.sh index c0c9b83..ea53048 100644 --- a/tests/browser_test.sh +++ b/tests/browser_test.sh @@ -90,7 +90,10 @@ bash -n "$SCRIPT" NO_ACTION_OUTPUT="$TEMP_DIR/no-action.out" if ( source "$SCRIPT" - TTY_INPUT="" + if [[ -n "$TTY_FD" ]]; then + exec {TTY_FD}>&- + fi + TTY_FD="" main ) > "$NO_ACTION_OUTPUT" 2>&1; then fail "A non-interactive run without an action succeeded" @@ -107,6 +110,10 @@ DIAGNOSTICS_OUTPUT="$TEMP_DIR/diagnostics.out" PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" diagnostics > "$DIAGNOSTICS_OUTPUT" 2>&1 PATH="$MOCK_BIN:$PATH" ILB_ACTION=diagnostics bash "$SCRIPT" > /dev/null 2>&1 assert_contains "--- Containers ---" "$DIAGNOSTICS_OUTPUT" + +PIPED_DIAGNOSTICS_OUTPUT="$TEMP_DIR/piped-diagnostics.out" +cat "$SCRIPT" | PATH="$MOCK_BIN:$PATH" ILB_ACTION=diagnostics bash > "$PIPED_DIAGNOSTICS_OUTPUT" 2>&1 +assert_contains "--- Containers ---" "$PIPED_DIAGNOSTICS_OUTPUT" assert_not_contains "apt-get" "$MOCK_LOG" assert_not_contains "systemctl" "$MOCK_LOG" From 05eaffa504d06d801b32894e79040d51b2446d88 Mon Sep 17 00:00:00 2001 From: Mammad <108564287+Mammad3861@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:46:09 +0330 Subject: [PATCH 3/4] Add Linux shell test workflow --- .gitattributes | 3 +++ .github/workflows/shell-tests.yml | 18 ++++++++++++++++++ README.md | 7 ++++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/shell-tests.yml diff --git a/.gitattributes b/.gitattributes index dfdb8b7..7515fad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ *.sh text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.github/workflows/shell-tests.yml b/.github/workflows/shell-tests.yml new file mode 100644 index 0000000..1bb25ac --- /dev/null +++ b/.github/workflows/shell-tests.yml @@ -0,0 +1,18 @@ +name: Shell tests + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + shell-tests: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + - run: | + bash -n browser.sh tests/browser_test.sh + bash tests/browser_test.sh diff --git a/README.md b/README.md index 807d527..6766ac5 100644 --- a/README.md +++ b/README.md @@ -138,9 +138,10 @@ Delete those directories manually only if you no longer need the browser profile If exposing the browser to the public internet: - use a strong password; -- restrict access with a firewall, VPN, or IP allow-list; -- consider putting it behind Nginx, Traefik, or another HTTPS reverse proxy; -- avoid exposing ports `3000`, `3001`, `4000`, or `4001` publicly unless needed. +- do not expose HTTP port `3000` directly; use it behind a reverse proxy; +- treat `CUSTOM_USER` and `PASSWORD` as basic protection for a trusted local network, not sufficient public Internet protection; +- use HTTPS and a reverse proxy with robust authentication for public exposure; +- restrict access with a firewall, VPN, or IP allow-list. Empty passwords are allowed, but the script warns when one is selected. From c85d163dbef56f48053ae475e5a51a9bd6bfc64a Mon Sep 17 00:00:00 2001 From: Mammad <108564287+Mammad3861@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:01:26 +0330 Subject: [PATCH 4/4] Harden diagnostics and prefer HTTPS --- README.md | 6 ++++-- browser.sh | 43 ++++++++++++++++++++++++++++++++++++------- tests/browser_test.sh | 24 +++++++++++++++++++++++- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6766ac5..6a18813 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ It keeps an interactive menu for installing, uninstalling, checking status, and - Interactive Bash menu for install, uninstall, status, and exit. - Chromium and Firefox containers from linuxserver.io. -- Chromium HTTP on `3000` and HTTPS on `3001`. -- Firefox HTTP on `4000` and HTTPS on `4001`. +- Chromium HTTPS on `3001`; HTTP on `3000` is for a reverse proxy only. +- Firefox HTTPS on `4001`; HTTP on `4000` is for a reverse proxy only. - Persistent config in `/opt/instant-linux-browser//config`. - Username/password prompts, with `ILB_USERNAME` and `ILB_PASSWORD` overrides. - Supports amd64/x86_64 and arm64/aarch64 Linux servers when the upstream image supports them. @@ -31,6 +31,8 @@ curl -fsSL https://raw.githubusercontent.com/Mammad3861/Instant-Linux-Browser/ma The script reads menu choices and credentials from your controlling terminal, so this remains interactive even though the script is streamed through standard input. +Use the HTTPS browser URL shown after installation. The HTTP URL is for a reverse proxy only. + ### Download first ```bash diff --git a/browser.sh b/browser.sh index 911d541..36cbdc9 100644 --- a/browser.sh +++ b/browser.sh @@ -146,6 +146,10 @@ container_running() { [[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || echo false)" == "true" ]] } +run_docker_diagnostics() { + timeout 5 docker "$@" +} + chromium_process_running() { docker exec "$1" sh -c 'ps -eo comm=,args= 2>/dev/null | grep -Eq "[c]hromium|[c]hrome"' >/dev/null 2>&1 } @@ -343,8 +347,8 @@ install_browser() { success "================================================" success "Deployment Successful!" - echo -e "Access URL (HTTP) : ${CYAN}http://${ip}:${port}${NC}" - echo -e "Access URL (HTTPS): ${CYAN}https://${ip}:${ssl_port}${NC}" + echo -e "Browser URL (HTTPS): ${CYAN}https://${ip}:${ssl_port}${NC}" + echo -e "HTTP (proxy-only) : ${CYAN}http://${ip}:${port}${NC}" echo -e "Credentials : ${YELLOW}configured for $username${NC}" echo -e "${YELLOW}Note: Accept the SSL warning in your browser.${NC}" success "================================================" @@ -361,6 +365,8 @@ uninstall_browser() { } show_diagnostics() { + local status + show_arch_info echo -e "${CYAN}Config base:${NC} ${CONFIG_BASE}" if ! command -v docker >/dev/null 2>&1; then @@ -368,22 +374,45 @@ show_diagnostics() { return 0 fi - if ! docker info >/dev/null 2>&1; then - echo "Docker: installed but not reachable" + if ! command -v timeout >/dev/null 2>&1; then + echo "Docker: diagnostics timeout command is unavailable" + return 0 + fi + + echo "Checking Docker daemon (up to 5 seconds)..." + if run_docker_diagnostics info >/dev/null 2>&1; then + : + else + status=$? + if [[ "$status" -eq 124 ]]; then + echo "Docker: daemon check timed out after 5 seconds" + else + echo "Docker: installed but not reachable" + fi return 0 fi + echo "Docker: reachable" echo -e "${CYAN}--- Containers ---${NC}" - docker ps -a --filter "name=chromium" --filter "name=firefox" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + if run_docker_diagnostics ps -a --filter "name=chromium" --filter "name=firefox" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'; then + : + else + status=$? + if [[ "$status" -eq 124 ]]; then + echo "Docker: container query timed out after 5 seconds" + else + echo "Docker: container query failed" + fi + fi } show_menu() { echo -e "${CYAN}==========================================${NC}" echo -e "${GREEN} Instant Linux Browser Installer${NC}" echo -e "${CYAN}==========================================${NC}" - echo -e "1) Install Chromium (HTTP 3000 / HTTPS 3001)" + echo -e "1) Install Chromium (HTTPS 3001; HTTP 3000 proxy-only)" echo -e "2) Uninstall Chromium" - echo -e "3) Install Firefox (HTTP 4000 / HTTPS 4001)" + echo -e "3) Install Firefox (HTTPS 4001; HTTP 4000 proxy-only)" echo -e "4) Uninstall Firefox" echo -e "5) Diagnostics" echo -e "6) Exit" diff --git a/tests/browser_test.sh b/tests/browser_test.sh index ea53048..680c394 100644 --- a/tests/browser_test.sh +++ b/tests/browser_test.sh @@ -85,6 +85,21 @@ esac MOCK_DOCKER chmod +x "$MOCK_BIN/docker" +cat > "$MOCK_BIN/timeout" <<'MOCK_TIMEOUT' +#!/usr/bin/env bash +set -eu + +seconds="$1" +shift + +if [[ "${MOCK_DOCKER_INFO_TIMEOUT:-0}" == "1" && "${1:-}" == "docker" && "${2:-}" == "info" ]]; then + exit 124 +fi + +exec "$@" +MOCK_TIMEOUT +chmod +x "$MOCK_BIN/timeout" + bash -n "$SCRIPT" NO_ACTION_OUTPUT="$TEMP_DIR/no-action.out" @@ -109,6 +124,7 @@ assert_contains "Invalid option: invalid-action" "$INVALID_OUTPUT" DIAGNOSTICS_OUTPUT="$TEMP_DIR/diagnostics.out" PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" diagnostics > "$DIAGNOSTICS_OUTPUT" 2>&1 PATH="$MOCK_BIN:$PATH" ILB_ACTION=diagnostics bash "$SCRIPT" > /dev/null 2>&1 +assert_contains "Checking Docker daemon (up to 5 seconds)..." "$DIAGNOSTICS_OUTPUT" assert_contains "--- Containers ---" "$DIAGNOSTICS_OUTPUT" PIPED_DIAGNOSTICS_OUTPUT="$TEMP_DIR/piped-diagnostics.out" @@ -119,8 +135,9 @@ assert_not_contains "systemctl" "$MOCK_LOG" if command -v script >/dev/null 2>&1; then TTY_OUTPUT="$TEMP_DIR/tty.out" - printf '6\n' | script -qec "cat '$SCRIPT' | bash" /dev/null > "$TTY_OUTPUT" 2>&1 || fail "Streamed interactive menu failed" + printf '5\n' | PATH="$MOCK_BIN:$PATH" script -qec "cat '$SCRIPT' | bash" /dev/null > "$TTY_OUTPUT" 2>&1 || fail "Streamed interactive diagnostics failed" assert_contains "Instant Linux Browser Installer" "$TTY_OUTPUT" + assert_contains "--- Containers ---" "$TTY_OUTPUT" else echo "SKIP: streamed TTY test requires the util-linux script command" fi @@ -138,6 +155,11 @@ require_root() { :; } detect_arch() { echo x86_64; } sleep() { :; } +DIAGNOSTICS_TIMEOUT_OUTPUT="$TEMP_DIR/diagnostics-timeout.out" +MOCK_DOCKER_INFO_TIMEOUT=1 show_diagnostics > "$DIAGNOSTICS_TIMEOUT_OUTPUT" 2>&1 +assert_contains "Checking Docker daemon (up to 5 seconds)..." "$DIAGNOSTICS_TIMEOUT_OUTPUT" +assert_contains "Docker: daemon check timed out after 5 seconds" "$DIAGNOSTICS_TIMEOUT_OUTPUT" + RESOLVED_IDS="$(SUDO_USER=root resolve_puid_pgid)" [[ "$RESOLVED_IDS" == "1000:1000" ]] || fail "Expected safe UID/GID fallback, got $RESOLVED_IDS"