diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml new file mode 100644 index 0000000..201b35e --- /dev/null +++ b/.github/workflows/nix-build.yml @@ -0,0 +1,126 @@ +name: Nix Build and Test + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + df -h + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Build browservice + run: nix build .#browservice --print-build-logs + timeout-minutes: 45 + + - name: Test browservice starts and serves HTTP + timeout-minutes: 5 + run: | + set -e + + # Start browservice in background + # Note: --no-sandbox is required in CI because chrome-sandbox needs setuid root + echo "Starting browservice..." + ./result/bin/browservice --use-dedicated-xvfb=yes --chromium-args=--no-sandbox > /tmp/browservice.log 2>&1 & + PID=$! + + # Wait for HTTP server to start (increased timeout for CI) + echo "Waiting for HTTP server to start..." + for i in {1..60}; do + if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/ 2>/dev/null | grep -q "200"; then + echo "HTTP server is up after ${i}s!" + break + fi + if [ $i -eq 60 ]; then + echo "Timeout waiting for HTTP server after 60s" + echo "=== Browservice logs ===" + cat /tmp/browservice.log || true + echo "=== End logs ===" + kill -9 $PID 2>/dev/null || true + exit 1 + fi + sleep 1 + done + + # Test the HTTP endpoint + echo "Testing HTTP endpoint..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/) + echo "HTTP response code: $HTTP_CODE" + + if [ "$HTTP_CODE" != "200" ]; then + echo "Expected HTTP 200, got $HTTP_CODE" + echo "=== Browservice logs ===" + cat /tmp/browservice.log || true + echo "=== End logs ===" + kill -9 $PID 2>/dev/null || true + exit 1 + fi + + # Verify HTML content + echo "Verifying HTML content..." + CONTENT=$(curl -s http://127.0.0.1:8080/) + if echo "$CONTENT" | grep -q "Browservice"; then + echo "HTML content verified!" + else + echo "Expected 'Browservice' in HTML content" + echo "Got: $CONTENT" + kill -9 $PID 2>/dev/null || true + exit 1 + fi + + # Cleanup - use SIGKILL to ensure process tree dies + echo "Cleaning up..." + kill -9 $PID 2>/dev/null || true + sleep 1 + + echo "All tests passed!" + + - name: Check flake + run: nix flake check + + - name: Test NixOS module evaluation + run: | + nix eval .#nixosModules.default --apply 'm: builtins.typeOf m' + echo "NixOS module exports correctly" + + build-aarch64: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + extra-platforms = aarch64-linux + + - name: Setup QEMU for aarch64 + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Build browservice for aarch64 (evaluation only) + run: | + # Just evaluate the derivation to check it's valid + # Full build would take too long on emulation + nix eval .#packages.aarch64-linux.browservice.drvPath + echo "aarch64-linux derivation evaluates successfully" diff --git a/.gitignore b/.gitignore index de00ef6..4eddc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ debug release gen GPUCache + +# Nix +result +result-* +.direnv diff --git a/README_NIX.md b/README_NIX.md new file mode 100644 index 0000000..9b8060b --- /dev/null +++ b/README_NIX.md @@ -0,0 +1,285 @@ +# Browservice on Nix/NixOS + +This document explains how to build and run Browservice using Nix, and how to deploy it as a NixOS service. + +## Quick Start + +### Run directly (no installation required) + +```bash +nix run github:ttalvitie/browservice +``` + +Then open http://127.0.0.1:8080 in your legacy browser. + +### Build locally + +```bash +# Clone the repository +git clone https://github.com/ttalvitie/browservice.git +cd browservice + +# Build +nix build + +# Run +./result/bin/browservice +``` + +## Installation + +### Add to your flake-based NixOS configuration + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + browservice.url = "github:ttalvitie/browservice"; + }; + + outputs = { self, nixpkgs, browservice, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ./configuration.nix + browservice.nixosModules.default + ]; + }; + }; +} +``` + +### Add to Home Manager or as a package + +```nix +{ + environment.systemPackages = [ + inputs.browservice.packages.${system}.default + ]; +} +``` + +## NixOS Module Configuration + +### Minimal configuration + +```nix +{ + services.browservice = { + enable = true; + }; +} +``` + +This starts browservice listening on `127.0.0.1:8080`. + +### Full configuration example + +```nix +{ + services.browservice = { + enable = true; + + # Open port 8080 in the firewall + openFirewall = true; + + # Run as a specific user (default: browservice) + user = "browservice"; + group = "browservice"; + + # Persistent data directory (set to null for incognito mode) + dataDir = "/var/lib/browservice"; + + settings = { + # Listen on all interfaces + listenAddress = "0.0.0.0:8080"; + + # Start page for new windows + startPage = "https://example.com"; + + # Maximum concurrent browser windows + windowLimit = 16; + + # Initial zoom level + initialZoom = 1.0; + + # Font rendering mode + # Options: "no-antialiasing", "antialiasing", "antialiasing-subpixel-rgb", + # "antialiasing-subpixel-bgr", "antialiasing-subpixel-vrgb", + # "antialiasing-subpixel-vbgr", "system" + browserFontRenderMode = "antialiasing"; + + # Block file:// URL scheme (security) + blockFileScheme = true; + + # Use dedicated Xvfb server + useDedicatedXvfb = true; + + # Show control bar + showControlBar = true; + + # Navigation buttons: "yes", "no", or "auto" + showSoftNavigationButtons = "auto"; + + # Custom user agent (null for CEF default) + userAgent = null; + + # Domains to skip certificate checks (use with caution) + certificateCheckExceptions = []; + + # Extra Chromium arguments + chromiumArgs = []; + }; + + vicePlugin = { + # Plugin filename + name = "retrojsvice.so"; + + # Image quality: 10-100 or "PNG" + defaultQuality = "PNG"; + + # HTTP basic auth (null to disable, or "user:password") + httpAuth = null; + + # Maximum HTTP server threads + httpMaxThreads = 100; + + # Forward back/forward buttons + navigationForwarding = true; + + # Show quality selector widget + qualitySelector = true; + }; + + # Additional command-line arguments + extraArgs = []; + }; +} +``` + +### Configuration for public access with authentication + +```nix +{ + services.browservice = { + enable = true; + openFirewall = true; + + settings = { + listenAddress = "0.0.0.0:8080"; + startPage = "https://www.google.com"; + }; + + vicePlugin = { + # Require authentication + httpAuth = "admin:secretpassword"; + + # Use JPEG compression for better performance + defaultQuality = 70; + }; + }; +} +``` + +### Kiosk mode configuration + +```nix +{ + services.browservice = { + enable = true; + + # No persistent storage + dataDir = null; + + settings = { + listenAddress = "127.0.0.1:8080"; + startPage = "https://your-kiosk-app.example.com"; + windowLimit = 1; + showControlBar = false; + showSoftNavigationButtons = "no"; + }; + }; +} +``` + +## Available Options Reference + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enable` | bool | `false` | Enable the browservice systemd service | +| `package` | package | (from flake) | The browservice package to use | +| `openFirewall` | bool | `false` | Open the HTTP listen port in the firewall | +| `user` | string | `"browservice"` | User account to run the service | +| `group` | string | `"browservice"` | Group to run the service | +| `dataDir` | path or null | `"/var/lib/browservice"` | Data directory for cookies/cache (null = incognito) | +| `settings.listenAddress` | string | `"127.0.0.1:8080"` | HTTP server listen address | +| `settings.startPage` | string | `"about:blank"` | Initial page URL for new windows | +| `settings.windowLimit` | int | `32` | Maximum concurrent browser windows | +| `settings.initialZoom` | float | `1.0` | Default zoom factor | +| `settings.blockFileScheme` | bool | `true` | Block file:// URLs | +| `settings.useDedicatedXvfb` | bool | `true` | Use dedicated Xvfb X server | +| `settings.showControlBar` | bool | `true` | Show the control bar | +| `settings.showSoftNavigationButtons` | enum | `"auto"` | Show navigation buttons | +| `settings.browserFontRenderMode` | enum | `"antialiasing"` | Font rendering mode | +| `settings.userAgent` | string or null | `null` | Custom User-Agent header | +| `settings.certificateCheckExceptions` | list of strings | `[]` | Domains to skip cert checks | +| `settings.chromiumArgs` | list of strings | `[]` | Extra Chromium arguments | +| `vicePlugin.name` | string | `"retrojsvice.so"` | Vice plugin filename | +| `vicePlugin.defaultQuality` | int or "PNG" | `"PNG"` | Image quality (10-100 or PNG) | +| `vicePlugin.httpAuth` | string or null | `null` | HTTP basic auth credentials | +| `vicePlugin.httpMaxThreads` | int | `100` | Max HTTP server threads | +| `vicePlugin.navigationForwarding` | bool | `true` | Forward back/forward buttons | +| `vicePlugin.qualitySelector` | bool | `true` | Show quality selector widget | +| `extraArgs` | list of strings | `[]` | Additional CLI arguments | + +## Development + +Enter a development shell with all dependencies: + +```bash +nix develop +``` + +Build and test: + +```bash +nix build +./result/bin/browservice --help +``` + +Run the test suite: + +```bash +nix flake check +``` + +## Supported Platforms + +- `x86_64-linux` +- `aarch64-linux` + +## Notes + +### Chrome Sandbox + +The Chromium sandbox requires the `chrome-sandbox` binary to have setuid root permissions. When running as a NixOS service, this is handled automatically. When running manually, you may need to either: + +1. Set the permissions manually: + ```bash + sudo chown root:root ./result/lib/chrome-sandbox + sudo chmod 4755 ./result/lib/chrome-sandbox + ``` + +2. Or disable the sandbox (less secure): + ```bash + ./result/bin/browservice --chromium-args=--no-sandbox + ``` + +### CEF (Chromium Embedded Framework) + +This Nix package uses prebuilt patched CEF binaries from the official browservice releases. The patches enable: + +- Custom in-memory clipboard (isolated from system clipboard) +- Configurable font rendering parameters + +Building CEF from source is not practical in Nix due to the enormous build requirements (200GB+ disk space, 16GB+ RAM). diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7b5fb12 --- /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": 1767767207, + "narHash": "sha256-Mj3d3PfwltLmukFal5i3fFt27L6NiKXdBezC1EBuZs4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5912c1772a44e31bf1c63c0390b90501e5026886", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "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..4176e12 --- /dev/null +++ b/flake.nix @@ -0,0 +1,260 @@ +{ + description = "Browser service for legacy web browsers via server-side rendering"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + let + # NixOS module (system-independent) + nixosModule = { config, lib, pkgs, ... }: { + imports = [ ./nixos-module.nix ]; + services.browservice.package = lib.mkDefault self.packages.${pkgs.system}.browservice; + }; + in + flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: + let + pkgs = import nixpkgs { inherit system; }; + + version = "0.9.12.1"; + + cefArchMap = { + "x86_64-linux" = "x86_64"; + "aarch64-linux" = "aarch64"; + }; + + cefHashes = { + "x86_64" = "1qdlpq98jmi3r4rg9azkrprgxs5mvmg8svgfmkyg1ld1v3api80f"; + "aarch64" = "1dxjv65rjkbnyc051hf505hd1ahw752lh59pimr50nkgkq07wqy8"; + }; + + cefArch = cefArchMap.${system}; + + patchedCef = pkgs.fetchurl { + url = "https://github.com/ttalvitie/browservice/releases/download/v${version}/patched_cef_${cefArch}.tar.bz2"; + sha256 = cefHashes.${cefArch}; + }; + + runtimeLibs = with pkgs; [ + # X11 and graphics + xorg.libX11 + xorg.libXcomposite + xorg.libXcursor + xorg.libXdamage + xorg.libXext + xorg.libXfixes + xorg.libXi + xorg.libXrandr + xorg.libXrender + xorg.libXScrnSaver + xorg.libXtst + xorg.libxcb + xorg.libxshmfence + libxkbcommon + + # GTK and related + gtk3 + glib + pango + cairo + gdk-pixbuf + atk + at-spi2-atk + at-spi2-core + dbus + + # System libs + alsa-lib + cups + libdrm + mesa + libGL + libGLU + expat + nspr + nss + udev + libgbm + + # Fonts + fontconfig + freetype + + # Other + zlib + bzip2 + ]; + + cefDllWrapper = pkgs.stdenv.mkDerivation { + pname = "cef-dll-wrapper"; + inherit version; + + src = patchedCef; + + nativeBuildInputs = with pkgs; [ + cmake + autoPatchelfHook + ]; + + buildInputs = runtimeLibs; + + dontConfigure = true; + + unpackPhase = '' + mkdir -p cef + tar xf $src -C cef --strip-components 1 + ''; + + buildPhase = '' + mkdir -p cef/releasebuild + cd cef/releasebuild + cmake -DCMAKE_BUILD_TYPE=Release .. + make -j$NIX_BUILD_CORES libcef_dll_wrapper + ''; + + installPhase = '' + cd $NIX_BUILD_TOP + mkdir -p $out/{lib,include,Resources,Release} + + # Install wrapper library + cp cef/releasebuild/libcef_dll_wrapper/libcef_dll_wrapper.a $out/lib/ + + # Install CEF shared libraries + cp -r cef/Release/* $out/Release/ + cp -r cef/Resources/* $out/Resources/ + + # Install headers + cp -r cef/include $out/ + ''; + }; + + browservice = pkgs.stdenv.mkDerivation { + pname = "browservice"; + inherit version; + + src = ./.; + + # Workaround: newer GCC/libstdc++ no longer transitively includes + NIX_CFLAGS_COMPILE = "-include cstdint"; + + nativeBuildInputs = with pkgs; [ + pkg-config + autoPatchelfHook + python3 + ]; + + buildInputs = with pkgs; [ + pango + xorg.libX11 + xorg.libxcb + poco + libjpeg + zlib + openssl + ] ++ runtimeLibs; + + preBuild = '' + # Setup CEF from pre-built wrapper + mkdir -p cef/{Release,Resources,include,releasebuild/libcef_dll_wrapper} + cp -r ${cefDllWrapper}/Release/* cef/Release/ + cp -r ${cefDllWrapper}/Resources/* cef/Resources/ + cp -r ${cefDllWrapper}/include/* cef/include/ + cp ${cefDllWrapper}/lib/libcef_dll_wrapper.a cef/releasebuild/libcef_dll_wrapper/ + + # Generate retrojsvice HTML + pushd viceplugins/retrojsvice + mkdir -p gen + python3 gen_html_cpp.py > gen/html.cpp + popd + ''; + + buildPhase = '' + runHook preBuild + make -j$NIX_BUILD_CORES release + runHook postBuild + ''; + + installPhase = '' + mkdir -p $out/bin $out/lib $out/share/browservice + + # Install main binary + cp release/bin/browservice $out/bin/browservice-unwrapped + + # Install CEF resources (Release dir, then Resources without duplicating locales) + cp -rn cef/Release/* $out/lib/ || true + # Copy Resources, handling locales directory specially + for item in cef/Resources/*; do + if [ -d "$item" ]; then + cp -rn "$item" $out/lib/ || true + else + cp -n "$item" $out/lib/ || true + fi + done + + # Install retrojsvice plugin + cp release/bin/retrojsvice.so $out/lib/ + + # Create wrapper script + cat > $out/bin/browservice << 'WRAPPER' + #!/usr/bin/env bash + SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + LIB_DIR="$(dirname "$SCRIPT_DIR")/lib" + export LD_LIBRARY_PATH="$LIB_DIR''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + exec "$SCRIPT_DIR/browservice-unwrapped" "$@" + WRAPPER + chmod +x $out/bin/browservice + ''; + + # Handle the RPATH for the binary + postFixup = '' + patchelf --set-rpath "$out/lib:${pkgs.lib.makeLibraryPath runtimeLibs}" $out/bin/browservice-unwrapped + + # The chrome-sandbox binary needs special handling (setuid) + if [ -f $out/lib/chrome-sandbox ]; then + chmod 755 $out/lib/chrome-sandbox + fi + ''; + + passthru = { + inherit cefDllWrapper; + }; + + meta = with pkgs.lib; { + description = "Browser as a service for legacy web browsers via server-side rendering"; + homepage = "https://github.com/ttalvitie/browservice"; + license = licenses.mit; + platforms = [ "x86_64-linux" "aarch64-linux" ]; + mainProgram = "browservice"; + }; + }; + + in { + packages = { + default = browservice; + inherit browservice cefDllWrapper; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = [ browservice ]; + + packages = with pkgs; [ + xvfb-run + xorg.xauth + gdb + ]; + + shellHook = '' + echo "Browservice development shell" + echo "Run 'nix build' to build the package" + ''; + }; + }) // { + # System-independent outputs + nixosModules = { + default = nixosModule; + browservice = nixosModule; + }; + }; +} diff --git a/nixos-module.nix b/nixos-module.nix new file mode 100644 index 0000000..0c1c03f --- /dev/null +++ b/nixos-module.nix @@ -0,0 +1,283 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.browservice; + inherit (lib) mkEnableOption mkOption mkIf types optionalString concatStringsSep; + + boolToYesNo = b: if b then "yes" else "no"; + + fontRenderModeType = types.enum [ + "no-antialiasing" + "antialiasing" + "antialiasing-subpixel-rgb" + "antialiasing-subpixel-bgr" + "antialiasing-subpixel-vrgb" + "antialiasing-subpixel-vbgr" + "system" + ]; + + navigationButtonsType = types.enum [ "yes" "no" "auto" ]; + +in { + options.services.browservice = { + enable = mkEnableOption "Browservice - browser service for legacy web browsers"; + + package = mkOption { + type = types.package; + description = "The browservice package to use."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the HTTP listen port in the firewall."; + }; + + user = mkOption { + type = types.str; + default = "browservice"; + description = "User account under which browservice runs."; + }; + + group = mkOption { + type = types.str; + default = "browservice"; + description = "Group under which browservice runs."; + }; + + dataDir = mkOption { + type = types.nullOr types.path; + default = "/var/lib/browservice"; + description = '' + Directory for storing persistent data (cookies, cache). + Set to null for incognito mode (no persistent storage). + ''; + }; + + settings = { + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1:8080"; + description = "IP address and port for the HTTP server to listen on."; + }; + + startPage = mkOption { + type = types.str; + default = "about:blank"; + description = "URL of the initial page for each new window."; + }; + + windowLimit = mkOption { + type = types.ints.positive; + default = 32; + description = "Maximum number of browser windows that can be open at the same time."; + }; + + initialZoom = mkOption { + type = types.float; + default = 1.0; + description = "Initial zoom factor for new browser windows."; + }; + + blockFileScheme = mkOption { + type = types.bool; + default = true; + description = '' + Block attempts to access local files through the file:// URI scheme. + WARNING: There may be ways around the block; do NOT allow untrusted users to access Browservice. + ''; + }; + + useDedicatedXvfb = mkOption { + type = types.bool; + default = true; + description = "Run the browser in its own Xvfb X server."; + }; + + showControlBar = mkOption { + type = types.bool; + default = true; + description = "Show the control bar (disable for kiosk mode)."; + }; + + showSoftNavigationButtons = mkOption { + type = navigationButtonsType; + default = "auto"; + description = '' + Show navigation buttons (Back/Forward/Refresh/Home) in the control bar. + 'auto' enables unless the vice plugin implements navigation buttons. + ''; + }; + + browserFontRenderMode = mkOption { + type = fontRenderModeType; + default = "antialiasing"; + description = "Font render mode for the browser area."; + }; + + userAgent = mkOption { + type = types.nullOr types.str; + default = null; + description = "Custom User-Agent header. Null uses CEF default."; + }; + + certificateCheckExceptions = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of domains for which SSL/TLS certificate checking is disabled. + Only exact match is considered (e.g., 'example.com' does not cover 'www.example.com'). + ''; + }; + + chromiumArgs = mkOption { + type = types.listOf types.str; + default = []; + description = "Extra arguments to forward to Chromium."; + }; + }; + + vicePlugin = { + name = mkOption { + type = types.str; + default = "retrojsvice.so"; + description = "Vice plugin to use for the user interface."; + }; + + defaultQuality = mkOption { + type = types.either types.ints.positive (types.enum [ "PNG" ]); + default = "PNG"; + description = "Initial image quality for each window (10-100 or 'PNG')."; + }; + + httpAuth = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + HTTP basic authentication credentials in 'USER:PASSWORD' format. + Use 'env' to read from HTTP_AUTH_CREDENTIALS environment variable. + Null disables authentication. + ''; + }; + + httpMaxThreads = mkOption { + type = types.ints.positive; + default = 100; + description = "Maximum number of HTTP server threads."; + }; + + navigationForwarding = mkOption { + type = types.bool; + default = true; + description = "Forward client back/forward buttons to the program."; + }; + + qualitySelector = mkOption { + type = types.bool; + default = true; + description = "Show image quality selector widget."; + }; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = []; + description = "Extra command-line arguments to pass to browservice."; + }; + }; + + config = mkIf cfg.enable { + users.users = mkIf (cfg.user == "browservice") { + browservice = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = cfg.dataDir != null; + description = "Browservice daemon user"; + }; + }; + + users.groups = mkIf (cfg.group == "browservice") { + browservice = {}; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + (lib.toInt (lib.last (lib.splitString ":" cfg.settings.listenAddress))) + ]; + }; + + systemd.services.browservice = { + description = "Browservice - Browser service for legacy web browsers"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + # Required for Xvfb and xauth when using --use-dedicated-xvfb + path = with pkgs; [ + xorg.xauth + xorg.xorgserver # Provides Xvfb + ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + ExecStart = let + args = [ + "--use-dedicated-xvfb=${boolToYesNo cfg.settings.useDedicatedXvfb}" + "--block-file-scheme=${boolToYesNo cfg.settings.blockFileScheme}" + "--show-control-bar=${boolToYesNo cfg.settings.showControlBar}" + "--show-soft-navigation-buttons=${cfg.settings.showSoftNavigationButtons}" + "--browser-font-render-mode=${cfg.settings.browserFontRenderMode}" + "--start-page=${cfg.settings.startPage}" + "--window-limit=${toString cfg.settings.windowLimit}" + "--initial-zoom=${toString cfg.settings.initialZoom}" + "--vice-plugin=${cfg.vicePlugin.name}" + "--vice-opt-http-listen-addr=${cfg.settings.listenAddress}" + "--vice-opt-default-quality=${toString cfg.vicePlugin.defaultQuality}" + "--vice-opt-http-max-threads=${toString cfg.vicePlugin.httpMaxThreads}" + "--vice-opt-navigation-forwarding=${boolToYesNo cfg.vicePlugin.navigationForwarding}" + "--vice-opt-quality-selector=${boolToYesNo cfg.vicePlugin.qualitySelector}" + ] + ++ lib.optional (cfg.dataDir != null) "--data-dir=${cfg.dataDir}" + ++ lib.optional (cfg.settings.userAgent != null) "--user-agent=${cfg.settings.userAgent}" + ++ lib.optional (cfg.settings.certificateCheckExceptions != []) + "--certificate-check-exceptions=${concatStringsSep "," cfg.settings.certificateCheckExceptions}" + ++ lib.optional (cfg.settings.chromiumArgs != []) + "--chromium-args=${concatStringsSep "," cfg.settings.chromiumArgs}" + ++ lib.optional (cfg.vicePlugin.httpAuth != null) + "--vice-opt-http-auth=${cfg.vicePlugin.httpAuth}" + ++ cfg.extraArgs; + in "${cfg.package}/bin/browservice ${concatStringsSep " " args}"; + + Restart = "on-failure"; + RestartSec = "5s"; + + # Security hardening + NoNewPrivileges = false; # Required for chrome-sandbox setuid + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = lib.optional (cfg.dataDir != null) cfg.dataDir; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictNamespaces = false; # Required for CEF sandbox + LockPersonality = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + + # Allow binding to privileged ports and disable user namespacing for CEF + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + PrivateUsers = false; + }; + }; + + # Create data directory and .browservice subdirectory + systemd.tmpfiles.rules = lib.optionals (cfg.dataDir != null) [ + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.dataDir}/.browservice 0750 ${cfg.user} ${cfg.group} -" + ]; + }; +}