#!/usr/bin/env bash set -euo pipefail require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 exit 1 fi } require_cmd docker require_cmd getent require_cmd ip require_cmd awk require_cmd sudo ufw_udp_range() { echo "${1//-/:}/udp" } if [[ "${EUID}" -ne 0 ]]; then sudo -v fi if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" ]]; then DEFAULT_HOME="$(getent passwd "$SUDO_USER" | awk -F: '{print $6}')" else DEFAULT_HOME="$HOME" fi HOME_DIR="${HOME_DIR:-$DEFAULT_HOME}" LAN_IP="${LAN_IP:-$(ip route get 1 | awk '{print $7; exit}')}" NEKO_USER_PASSWORD="${NEKO_USER_PASSWORD:-user123}" NEKO_ADMIN_PASSWORD="${NEKO_ADMIN_PASSWORD:-admin123}" PROFILE_UID="${PROFILE_UID:-1000}" PROFILE_GID="${PROFILE_GID:-1000}" CHROMIUM_DIR="${CHROMIUM_DIR:-$HOME_DIR/neko-browser}" GOOGLE_CHROME_DIR="${GOOGLE_CHROME_DIR:-$HOME_DIR/neko-google-chrome}" CHROMIUM_PROFILE_DIR="${CHROMIUM_PROFILE_DIR:-/root/neko-chromium-profile}" GOOGLE_CHROME_PROFILE_DIR="${GOOGLE_CHROME_PROFILE_DIR:-/root/neko-google-chrome-profile}" CHROMIUM_HTTP_PORT="${CHROMIUM_HTTP_PORT:-8080}" GOOGLE_CHROME_HTTP_PORT="${GOOGLE_CHROME_HTTP_PORT:-8081}" CHROMIUM_UDP_RANGE="${CHROMIUM_UDP_RANGE:-56000-56100}" GOOGLE_CHROME_UDP_RANGE="${GOOGLE_CHROME_UDP_RANGE:-56200-56300}" CHROMIUM_DISPLAY="${CHROMIUM_DISPLAY:-:99.0}" GOOGLE_CHROME_DISPLAY="${GOOGLE_CHROME_DISPLAY:-:100.0}" mkdir -p \ "$CHROMIUM_DIR/policies" \ "$GOOGLE_CHROME_DIR/policies" sudo mkdir -p \ "$CHROMIUM_PROFILE_DIR" \ "$GOOGLE_CHROME_PROFILE_DIR" sudo chown -R "${PROFILE_UID}:${PROFILE_GID}" \ "$CHROMIUM_PROFILE_DIR" \ "$GOOGLE_CHROME_PROFILE_DIR" cat >"$CHROMIUM_DIR/docker-compose.yaml" <"$CHROMIUM_DIR/chromium.conf" <<'EOF' [program:chromium] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/chromium --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/chromium --password-store=basic --no-first-run --start-maximized --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding stopsignal=INT autorestart=true priority=800 user=%(ENV_USER)s stdout_logfile=/var/log/neko/chromium.log stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true [program:openbox] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/openbox --config-file /etc/neko/openbox.xml autorestart=true priority=300 user=%(ENV_USER)s stdout_logfile=/var/log/neko/openbox.log stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true EOF cat >"$CHROMIUM_DIR/policies/policies.json" <<'EOF' { "ExtensionInstallForcelist": [ "dhdgffkkebhmkfjojejmpbldmpobfkfo;https://clients2.google.com/service/update2/crx" ], "ExtensionInstallAllowlist": [ "*" ], "ExtensionInstallBlocklist": [], "DownloadRestrictions": 0, "AllowFileSelectionDialogs": true, "URLAllowlist": ["file:///home/neko/Downloads/*"], "DefaultCookiesSetting": 1, "RestoreOnStartup": 1 } EOF cat >"$GOOGLE_CHROME_DIR/docker-compose.yaml" <"$GOOGLE_CHROME_DIR/google-chrome.conf" <<'EOF' [program:google-chrome] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/google-chrome --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/google-chrome --password-store=basic --no-first-run --start-maximized --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding stopsignal=INT autorestart=true priority=800 user=%(ENV_USER)s stdout_logfile=/var/log/neko/google-chrome.log stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true [program:openbox] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/openbox --config-file /etc/neko/openbox.xml autorestart=true priority=300 user=%(ENV_USER)s stdout_logfile=/var/log/neko/openbox.log stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true EOF cat >"$GOOGLE_CHROME_DIR/policies/policies.json" <<'EOF' { "ExtensionInstallForcelist": [ "dhdgffkkebhmkfjojejmpbldmpobfkfo;https://clients2.google.com/service/update2/crx" ], "ExtensionInstallAllowlist": [ "*" ], "ExtensionInstallBlocklist": [], "DownloadRestrictions": 0, "AllowFileSelectionDialogs": true, "URLAllowlist": ["file:///home/neko/Downloads/*"], "DefaultCookiesSetting": 1, "RestoreOnStartup": 1 } EOF sudo ufw allow "${CHROMIUM_HTTP_PORT}" >/dev/null 2>&1 || true sudo ufw allow "${GOOGLE_CHROME_HTTP_PORT}" >/dev/null 2>&1 || true sudo ufw allow "$(ufw_udp_range "${CHROMIUM_UDP_RANGE}")" >/dev/null 2>&1 || true sudo ufw allow "$(ufw_udp_range "${GOOGLE_CHROME_UDP_RANGE}")" >/dev/null 2>&1 || true recreate_compose_service() { local compose_dir="$1" local service="$2" local name_pattern="$3" local existing_id="" local pid="" existing_id="$(cd "$compose_dir" && docker compose ps -q "$service" 2>/dev/null || true)" if [[ -n "$existing_id" ]]; then docker update --restart=no "$existing_id" >/dev/null 2>&1 || true pid="$(docker inspect -f '{{.State.Pid}}' "$existing_id" 2>/dev/null || true)" if [[ -n "$pid" && "$pid" != "0" ]]; then sudo kill -TERM "$pid" >/dev/null 2>&1 || true sleep 3 sudo kill -KILL "$pid" >/dev/null 2>&1 || true sleep 2 fi docker rm -f "$existing_id" >/dev/null 2>&1 || true fi docker ps -a --format '{{.ID}} {{.Names}}' \ | awk -v pat="$name_pattern" '$2 ~ pat {print $1}' \ | xargs -r docker rm -f >/dev/null 2>&1 || true (cd "$compose_dir" && docker compose up -d) } wait_for_healthy() { local compose_dir="$1" local service="$2" local container_id="" local health_status="" local state_status="" for _ in $(seq 1 60); do container_id="$(cd "$compose_dir" && docker compose ps -q "$service" 2>/dev/null || true)" [[ -z "$container_id" ]] && sleep 2 && continue health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" 2>/dev/null || true)" state_status="$(docker inspect -f '{{.State.Status}}' "$container_id" 2>/dev/null || true)" if [[ "$state_status" == "running" && ( "$health_status" == "healthy" || "$health_status" == "none" ) ]]; then echo "$container_id" return 0 fi if [[ "$health_status" == "unhealthy" ]]; then echo "service ${service} became unhealthy" >&2 docker logs --tail 200 "$container_id" >&2 || true return 1 fi sleep 2 done echo "timed out waiting for ${service}" >&2 [[ -n "$container_id" ]] && docker logs --tail 200 "$container_id" >&2 || true return 1 } recreate_compose_service "$CHROMIUM_DIR" "neko-browser" "neko-browser" recreate_compose_service "$GOOGLE_CHROME_DIR" "neko-google-chrome" "neko-google-chrome" CHROMIUM_ID="$(wait_for_healthy "$CHROMIUM_DIR" "neko-browser")" GOOGLE_CHROME_ID="$(wait_for_healthy "$GOOGLE_CHROME_DIR" "neko-google-chrome")" echo echo "chromium:" echo " url: http://${LAN_IP}:${CHROMIUM_HTTP_PORT}/" echo " container: ${CHROMIUM_ID}" echo " compose: ${CHROMIUM_DIR}" echo echo "google-chrome:" echo " url: http://${LAN_IP}:${GOOGLE_CHROME_HTTP_PORT}/" echo " container: ${GOOGLE_CHROME_ID}" echo " compose: ${GOOGLE_CHROME_DIR}"