diff --git a/.github/workflows/kiosk-iso.yml b/.github/workflows/kiosk-iso.yml new file mode 100644 index 0000000..3171b4d --- /dev/null +++ b/.github/workflows/kiosk-iso.yml @@ -0,0 +1,43 @@ +name: Build NixOS Kiosk ISO + +on: + push: + branches: [ main, profile-install ] + workflow_dispatch: + +jobs: + flake-check: + runs-on: ubuntu-latest + container: + image: nixos/nix:2.33.0 + env: + NIX_CONFIG: extra-experimental-features = nix-command flakes + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Flake check + run: nix flake check -L + + build-iso: + runs-on: ubuntu-latest + needs: [ flake-check ] + container: + image: nixos/nix:2.33.0 + env: + NIX_CONFIG: extra-experimental-features = nix-command flakes + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build ISO + run: | + nix --version + nix build .#iso -L --system x86_64-linux + ls -la result + mkdir -p artifacts + cp -v result/iso/*.iso artifacts/ + - name: Upload ISO artifact + uses: actions/upload-artifact@v4 + with: + name: kiosk-iso + path: artifacts/*.iso + if-no-files-found: error diff --git a/Firefox.zip b/Firefox.zip new file mode 100644 index 0000000..c4020ce Binary files /dev/null and b/Firefox.zip differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..032fb8b --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b991131 --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Kiosk NixOS ISO with Firefox in kiosk mode"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: { + # Build a bootable ISO image using the built-in NixOS iso module + packages.iso = ( + (nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ./nixos/kiosk.nix + (import "${nixpkgs}/nixos/modules/installer/cd-dvd/iso-image.nix") + ]; + }).config.system.build.isoImage + ); + + # Expose the NixOS configuration for direct use if desired + nixosConfigurations.kiosk = (nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ./nixos/kiosk.nix ]; + }); + }); +} diff --git a/nixos/kiosk.nix b/nixos/kiosk.nix new file mode 100644 index 0000000..e2688f7 --- /dev/null +++ b/nixos/kiosk.nix @@ -0,0 +1,151 @@ +{ config, pkgs, lib, ... }: + +{ + ############################################ + # Base system + ############################################ + nixpkgs.hostPlatform = "x86_64-linux"; + system.stateVersion = "24.11"; + + # Simple console-based kiosk using cage (Wayland single-app compositor) + services.xserver.enable = false; # Not using an X11 display manager + + # Autologin to TTY1 as kiosk user + services.getty.autologinUser = "kiosk"; + + # Kiosk user + users.users.kiosk = { + isNormalUser = true; + description = "Kiosk User"; + home = "/home/kiosk"; + extraGroups = [ "wheel" ]; + initialPassword = "kiosk"; + }; + + # Packages required (aligning with the Debian preseed intent) + environment.systemPackages = with pkgs; [ + firefox + cage + curl + unzip + # chromium # available if you want it in addition to Firefox + ]; + + ############################################ + # Firefox policies (preconfigured profile settings) + ############################################ + programs.firefox = { + enable = true; + policies = { + DisableDeveloperTools = true; + BlockAboutAddons = true; + BlockAboutConfig = true; + BlockAboutProfiles = true; + BlockAboutSupport = true; + DisableFirefoxAccounts = true; + DisablePrivateBrowsing = true; + DisableProfileImport = true; + DisableProfileRefresh = true; + DisableSafeMode = true; + DisablePocket = true; + DisableFirefoxScreenshots = true; + DisableSetDesktopBackground = true; + + Homepage = { + URL = "https://mahn.ke"; + Locked = true; + }; + + NewTabPage = { Enabled = false; }; + + # Use a Linux path for downloads in kiosk + DownloadDirectory = { + Path = "/home/kiosk/Downloads"; + Locked = true; + }; + + PromptForDownloadLocation = false; + StartDownloadsInTempDirectory = false; + DisableAppUpdate = true; + + Permissions = { + Camera = "deny"; + Microphone = "deny"; + Location = "deny"; + Notifications = "deny"; + }; + + ShowHomeButton = false; + DisplayMenuBar = false; + DisplayBookmarksToolbar = false; + + # Extension & user messaging controls (per your Debian policy JSON) + UserMessaging = { + ExtensionRecommendations = false; + FeatureRecommendations = false; + UrlbarInterventions = false; + SkipOnboarding = false; + MoreFromMozilla = false; + FirefoxLabs = false; + Locked = false; + }; + + # Install Tampermonkey automatically (Firefox will fetch at runtime). + # Note: AMO URL may change; this is the typical latest channel. + Extensions = { + Install = [ + "https://addons.mozilla.org/firefox/downloads/latest/tampermonkey/latest.xpi" + ]; + }; + }; + + # Helpful preferences to keep Firefox minimal + preferences = { + "browser.fullscreen.autohide" = true; + "browser.shell.checkDefaultBrowser" = false; + "browser.startup.page" = 1; # Start with homepage + }; + }; + + ############################################ + # Kiosk launch behavior (replicates your bash_profile approach) + ############################################ + # Create a bash_profile for the kiosk user that launches cage + firefox + system.activationScripts.kioskBashProfile = lib.stringAfter ["users"] '' + mkdir -p /home/kiosk + chown kiosk:kiosk /home/kiosk + sudo -u kiosk mkdir -p /home/kiosk/.config + cat > /home/kiosk/.bash_profile <<'EOF' +if [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then + exec ${pkgs.cage}/bin/cage ${pkgs.firefox}/bin/firefox --kiosk https://c3nav.de +fi +EOF + chown kiosk:kiosk /home/kiosk/.bash_profile + ''; + + # Unpack preconfigured Firefox profile from the repository into kiosk's home + system.activationScripts.kioskFirefoxProfile = lib.stringAfter ["users"] '' + mkdir -p /home/kiosk/.mozilla/firefox + # Only unzip if directory is empty (first activation) + if [ -z "$(ls -A /home/kiosk/.mozilla/firefox 2>/dev/null)" ]; then + ${pkgs.unzip}/bin/unzip -o ${../Firefox.zip} -d /home/kiosk/.mozilla/firefox + chown -R kiosk:kiosk /home/kiosk/.mozilla/firefox + fi + ''; + + ############################################ + # Include your userscripts in the image for easy import + ############################################ + environment.etc."kiosk/tampermonkey".source = ./../tampermonkey; + + ############################################ + # Networking & basic services + ############################################ + networking.hostName = "kiosk"; + time.timeZone = "UTC"; + services.openssh.enable = true; # optional, mirrors preseed tasksel ssh-server + + + # Keep system simple, disable unneeded DM + services.displayManager.enable = false; +} diff --git a/post_install.sh b/post_install.sh index 8c9659e..a847cc0 100644 --- a/post_install.sh +++ b/post_install.sh @@ -59,4 +59,10 @@ tee /home/kiosk/.bash_profile > /dev/null <<'EOF' if [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then exec cage firefox --kiosk https://c3nav.de fi -EOF \ No newline at end of file +EOF + +mkdir -p /home/kiosk/.mozilla/firefox +curl -fsSL -o /tmp/Firefox.zip "https://git.hamburg.ccc.de/Firefox.zip" +unzip -o /tmp/Firefox.zip -d /home/kiosk/.mozilla/firefox +chown -R kiosk:kiosk /home/kiosk/.mozilla/firefox +rm -f /tmp/Firefox.zip \ No newline at end of file diff --git a/preseed.cfg b/preseed.cfg index b934651..527a83b 100644 --- a/preseed.cfg +++ b/preseed.cfg @@ -53,6 +53,7 @@ d-i pkgsel/include string \ sudo \ cage \ firefox-esr \ + unzip \ curl d-i pkgsel/exclude string gnome-software @@ -64,4 +65,4 @@ d-i finish-install/reboot_in_progress note d-i preseed/late_command string \ in-target curl -o /tmp/post_install.sh https://git.hamburg.ccc.de/ViMaSter/preseed/raw/branch/main/post_install.sh; \ in-target chmod +x /tmp/post_install.sh; \ - in-target /tmp/post_install.sh + in-target /tmp/post_install.sh; \ No newline at end of file diff --git a/tampermonkey/backtohome.js b/tampermonkey/backtohome.js index 5dbb51e..a7a0935 100644 --- a/tampermonkey/backtohome.js +++ b/tampermonkey/backtohome.js @@ -9,22 +9,31 @@ // @grant GM_addStyle // ==/UserScript== +const target = "https://kiosk.39c3.by.vincent.mahn.ke/"; (function() { 'use strict'; + // if the user is on target already or subdomain, do nothing + if (window.location.href === target || window.location.href.startsWith(target + '/')) { + return; + } + const btn = document.createElement('button'); btn.textContent = 'Home'; btn.style.cssText = ' all: unset; box-sizing: border-box; border: 2px solid #141414; background: #faf5f5; text-align: center; color: #141414; position: fixed; right: 10px; bottom: 10px; width: 60px; height: 60px; z-index: 2147483647; padding-top: 6px; border-radius: 60px;'; btn.innerHTML = ''; btn.addEventListener('click', () => { - window.location.href = 'http://127.0.0.1:8080'; + window.location.href = target; }); (document.body || document.documentElement).appendChild(btn); - const IDLE_LIMIT_MS = 30_000; // 30 seconds + const IDLE_LIMIT_MS = 60_000; let idleTimer = null; let promptVisible = false; + const PROMPT_LIMIT_MS = 30_000; + let promptInterval = null; + let promptTimeout = null; const modal = document.createElement('div'); modal.id = 'idle-modal'; @@ -46,6 +55,7 @@

Do you want to stay on this page or go back to the home page?

+

Auto return in 30s

`; document.body.appendChild(modal); @@ -76,21 +86,57 @@ promptVisible = true; modal.hidden = false; backdrop.hidden = false; - debugger; - // Move focus to the primary action for accessibility - document.querySelector("#idle-stay").addEventListener("click", () => { + + // Clear any previous prompt timers + if (promptInterval) { clearInterval(promptInterval); promptInterval = null; } + if (promptTimeout) { clearTimeout(promptTimeout); promptTimeout = null; } + + // Wire button actions (once to avoid duplicates) + const stayBtn = document.querySelector("#idle-stay"); + const goBtn = document.querySelector("#idle-go"); + + if (stayBtn) { + stayBtn.addEventListener("click", () => { hidePrompt(); resetIdleTimer(); - }); - document.querySelector("#idle-go").addEventListener("click", () => { - window.location.href = 'https://www.google.com'; - }); + }, { once: true }); + } + + if (goBtn) { + goBtn.addEventListener("click", () => { + window.location.href = target; + }, { once: true }); + } + + // 30s countdown visible to the user + let remaining = PROMPT_LIMIT_MS / 1000; // seconds + const countdownEl = document.querySelector("#idle-countdown"); + if (countdownEl) countdownEl.textContent = String(remaining); + + promptInterval = setInterval(() => { + remaining -= 1; + if (remaining >= 0 && countdownEl) { + countdownEl.textContent = String(remaining); + } + }, 1000); + + // Auto-go when expired + promptTimeout = setTimeout(() => { + const go = document.querySelector("#idle-go"); + if (go) { + go.click(); + } else { + window.location.href = target; + } + }, PROMPT_LIMIT_MS); } function hidePrompt() { promptVisible = false; modal.hidden = true; backdrop.hidden = true; + if (promptInterval) { clearInterval(promptInterval); promptInterval = null; } + if (promptTimeout) { clearTimeout(promptTimeout); promptTimeout = null; } } function onIdle() {