diff --git a/.gitignore b/.gitignore index bd422ab..1e32026 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # misc .DS_Store *.pem +*.har # debug npm-debug.log* diff --git a/README.md b/README.md index 3e8e94b..52e5c9c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Personal site for Jet Pham. The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a WebGL2 Conway's Game of Life background. +It also ships as a Nix flake with a reusable NixOS module for serving the static frontend and the Q+A API on a host. + ## Features - ASCII/ANSI-inspired visual style with the IBM VGA font @@ -53,9 +55,39 @@ npm run build ## Structure ```text +api/ Q+A backend +module.nix NixOS module src/ frontend app ``` +## NixOS module + +Import the module from the flake and point it at the host-managed secret files you want to use. + +```nix +{ + inputs.website.url = "github:jetpham/website"; + + outputs = { self, nixpkgs, website, ... }: { + nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + website.nixosModules.default + ({ config, ... }: { + services.jetpham-website = { + enable = true; + domain = "jetpham.com"; + webhookSecretFile = config.age.secrets.webhook-secret.path; + }; + }) + ]; + }; + }; +} +``` + +Optional Tor support is configured by setting `services.jetpham-website.tor.enable = true;` and providing the three onion key file paths. + ## Notes - The homepage and Q+A page are the intended public pages. diff --git a/check_cleanup.txt b/check_cleanup.txt deleted file mode 100644 index 8ee83ad..0000000 --- a/check_cleanup.txt +++ /dev/null @@ -1,2 +0,0 @@ -$ bun run lint && tsc --noEmit -$ eslint . diff --git a/flake.lock b/flake.lock index 8b83aa9..f194334 100644 --- a/flake.lock +++ b/flake.lock @@ -1,53 +1,8 @@ { "nodes": { - "agenix": { - "inputs": { - "darwin": "darwin", - "home-manager": "home-manager", - "nixpkgs": [ - "nixpkgs" - ], - "systems": "systems" - }, - "locked": { - "lastModified": 1770165109, - "narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=", - "owner": "ryantm", - "repo": "agenix", - "rev": "b027ee29d959fda4b60b57566d64c98a202e0feb", - "type": "github" - }, - "original": { - "owner": "ryantm", - "repo": "agenix", - "type": "github" - } - }, - "darwin": { - "inputs": { - "nixpkgs": [ - "agenix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1744478979, - "narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=", - "owner": "lnl7", - "repo": "nix-darwin", - "rev": "43975d782b418ebf4969e9ccba82466728c2851b", - "type": "github" - }, - "original": { - "owner": "lnl7", - "ref": "master", - "repo": "nix-darwin", - "type": "github" - } - }, "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1731533236, @@ -63,27 +18,6 @@ "type": "github" } }, - "home-manager": { - "inputs": { - "nixpkgs": [ - "agenix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1745494811, - "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "home-manager", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1774386573, @@ -100,46 +34,10 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { - "agenix": "agenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": "nixpkgs_2" - }, - "locked": { - "lastModified": 1774581174, - "narHash": "sha256-258qgkMkYPkJ9qpIg63Wk8GoIbVjszkGGPU1wbVHYTk=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "a313afc75b85fc77ac154bf0e62c36f68361fd0b", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" + "nixpkgs": "nixpkgs" } }, "systems": { @@ -156,21 +54,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "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", diff --git a/flake.nix b/flake.nix index 12dec4d..9ea1199 100644 --- a/flake.nix +++ b/flake.nix @@ -2,93 +2,38 @@ description = "Jet Pham's personal website"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; - agenix.url = "github:ryantm/agenix"; - agenix.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, - rust-overlay, flake-utils, - agenix, }: (flake-utils.lib.eachDefaultSystem ( system: let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - agenixPkg = agenix.packages.${system}.default; - rustToolchain = pkgs.rust-bin.selectLatestNightlyWith ( - toolchain: - toolchain.default.override { - extensions = [ "rust-src" ]; - targets = [ "wasm32-unknown-unknown" ]; - } - ); - rustPlatform = pkgs.makeRustPlatform { - cargo = rustToolchain; - rustc = rustToolchain; - }; - - cgol-wasm = rustPlatform.buildRustPackage { - pname = "cgol-wasm"; - version = "0.1.0"; - src = ./cgol; - cargoLock.lockFile = ./cgol/Cargo.lock; - doCheck = false; - - nativeBuildInputs = [ - pkgs.wasm-bindgen-cli - pkgs.binaryen + pkgs = import nixpkgs { inherit system; }; + lib = pkgs.lib; + websiteSrc = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./index.html + ./package-lock.json + ./package.json + ./public + ./src + ./tsconfig.json + ./vite-plugin-ansi.ts + ./vite.config.ts ]; - - buildPhase = '' - runHook preBuild - cargo build --release --frozen --target wasm32-unknown-unknown - wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/cgol.wasm - wasm-opt pkg/cgol_bg.wasm -o pkg/cgol_bg.wasm -O4 \ - --enable-bulk-memory --enable-nontrapping-float-to-int \ - --enable-sign-ext --low-memory-unused --converge - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - mkdir -p $out - cp pkg/cgol_bg.wasm $out/ - cp pkg/cgol.js $out/ - cp pkg/cgol.d.ts $out/ - cp pkg/cgol_bg.wasm.d.ts $out/ 2>/dev/null || true - cat > $out/package.json <<'EOF' - { - "name": "cgol", - "type": "module", - "version": "0.1.0", - "files": ["cgol_bg.wasm", "cgol.js", "cgol.d.ts"], - "main": "cgol.js", - "types": "cgol.d.ts", - "sideEffects": ["./snippets/*"] - } - EOF - runHook postInstall - ''; }; - # Stage 2: Build the website with npm website = pkgs.buildNpmPackage { pname = "jet-website"; version = "0.1.0"; - src = pkgs.lib.cleanSource ./.; - npmDepsHash = "sha256-O4ZUSYyVWOxP15saIadsaZuRO47Y0AvsL4pwvo5b76U="; - - # Inject the Nix-built WASM before npm install resolves file: dep - postPatch = '' - mkdir -p cgol/pkg - cp -r ${cgol-wasm}/* cgol/pkg/ - ''; + src = websiteSrc; + npmDepsHash = "sha256-UDz4tXNvEa8uiDDGg16K9JbNeQZR3BsVNKtuOgcyurQ="; installPhase = '' runHook preInstall @@ -110,7 +55,6 @@ { packages = { default = website; - cgol-wasm = cgol-wasm; inherit qa-api; }; devShells.default = pkgs.mkShell { @@ -119,12 +63,11 @@ git curl openssl - agenixPkg typescript-language-server + rust-analyzer + rustc + cargo pkg-config - wasm-pack - binaryen - rustToolchain ]; }; } diff --git a/module.nix b/module.nix index 92b9669..ef5d108 100644 --- a/module.nix +++ b/module.nix @@ -1,27 +1,118 @@ self: -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.jetpham-website; - package = self.packages.x86_64-linux.default; - qaApi = self.packages.x86_64-linux.qa-api; - webhookSecretPath = - if cfg.webhookSecretFile != null then - cfg.webhookSecretFile - else - config.age.secrets.webhook-secret.path; + package = cfg.package; + qaApi = cfg.apiPackage; + apiListen = "${cfg.apiListenAddress}:${toString cfg.apiListenPort}"; + caddyCommonConfig = '' + header Cross-Origin-Opener-Policy "same-origin" + header Cross-Origin-Embedder-Policy "require-corp" + + handle /api/* { + reverse_proxy ${apiListen} + } + + handle /qa/rss.xml { + reverse_proxy ${apiListen} + } + + handle { + root * ${package} + try_files {path} /index.html + file_server + } + + ${cfg.caddy.extraConfig} + ''; in { options.services.jetpham-website = { enable = lib.mkEnableOption "Jet Pham's personal website"; + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.default; + defaultText = lib.literalExpression "self.packages.${pkgs.system}.default"; + description = "Static site package served by Caddy."; + }; + + apiPackage = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.qa-api; + defaultText = lib.literalExpression "self.packages.${pkgs.system}.qa-api"; + description = "Q&A API package run by systemd."; + }; + domain = lib.mkOption { type = lib.types.str; default = "jetpham.com"; description = "Domain to serve the website on."; }; - tor.enable = lib.mkEnableOption "Tor hidden service for the website"; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open HTTP and HTTPS ports when Caddy is enabled."; + }; + + apiListenAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Address for the local Q&A API listener."; + }; + + apiListenPort = lib.mkOption { + type = lib.types.port; + default = 3003; + description = "Port for the local Q&A API listener."; + }; + + caddy.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Serve the static site and reverse proxy the API through Caddy."; + }; + + caddy.extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Extra Caddy directives appended inside the virtual host block."; + }; + + tor = { + enable = lib.mkEnableOption "Tor hidden service for the website"; + + port = lib.mkOption { + type = lib.types.port; + default = 8888; + description = "Local Caddy port exposed through the onion service."; + }; + + onionSecretKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service secret key file."; + }; + + onionPublicKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service public key file."; + }; + + onionHostnameFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service hostname file."; + }; + }; qaNotifyEmail = lib.mkOption { type = lib.types.str; @@ -42,36 +133,31 @@ in }; webhookSecretFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + type = lib.types.path; description = "File containing the WEBHOOK_SECRET for MTA Hook authentication."; }; }; config = lib.mkIf cfg.enable { - age.secrets.webhook-secret = lib.mkIf (cfg.webhookSecretFile == null) { - file = "${self}/secrets/webhook-secret.age"; - mode = "0400"; - }; + assertions = [ + { + assertion = + !cfg.tor.enable + || ( + cfg.tor.onionSecretKeyFile != null + && cfg.tor.onionPublicKeyFile != null + && cfg.tor.onionHostnameFile != null + ); + message = "services.jetpham-website.tor requires onionSecretKeyFile, onionPublicKeyFile, and onionHostnameFile."; + } + ]; - age.secrets.tor-onion-secret-key = lib.mkIf cfg.tor.enable { - file = "${self}/secrets/tor-onion-secret-key.age"; - owner = "tor"; - group = "tor"; - mode = "0400"; - }; - age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable { - file = "${self}/secrets/tor-onion-public-key.age"; - owner = "tor"; - group = "tor"; - mode = "0444"; - }; - age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable { - file = "${self}/secrets/tor-onion-hostname.age"; - owner = "tor"; - group = "tor"; - mode = "0444"; - }; + networking.firewall.allowedTCPPorts = lib.mkIf (cfg.caddy.enable && cfg.openFirewall) [ + 80 + 443 + ]; + + services.caddy.enable = cfg.caddy.enable; services.tor = lib.mkIf cfg.tor.enable { enable = true; @@ -81,7 +167,7 @@ in port = 80; target = { addr = "127.0.0.1"; - port = 8888; + port = cfg.tor.port; }; } ]; @@ -90,39 +176,45 @@ in systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable { description = "Copy Tor onion keys into place"; - after = [ "agenix.service" ]; before = [ "tor.service" ]; wantedBy = [ "tor.service" ]; serviceConfig.Type = "oneshot"; script = '' dir="/var/lib/tor/onion/jetpham-website" mkdir -p "$dir" - cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key" - cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key" - cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname" + cp ${cfg.tor.onionSecretKeyFile} "$dir/hs_ed25519_secret_key" + cp ${cfg.tor.onionPublicKeyFile} "$dir/hs_ed25519_public_key" + cp ${cfg.tor.onionHostnameFile} "$dir/hostname" chown -R tor:tor "$dir" chmod 700 "$dir" chmod 400 "$dir/hs_ed25519_secret_key" chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname" ''; }; - # Q&A API systemd service + systemd.services.jetpham-qa-api = { description = "Jet Pham Q&A API"; - after = [ "network.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = true; StateDirectory = "jetpham-qa"; + WorkingDirectory = "/var/lib/jetpham-qa"; Environment = [ "QA_DB_PATH=/var/lib/jetpham-qa/qa.db" "QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}" "QA_MAIL_DOMAIN=${cfg.qaMailDomain}" "QA_REPLY_DOMAIN=${cfg.qaReplyDomain}" ]; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ReadWritePaths = [ "/var/lib/jetpham-qa" ]; Restart = "on-failure"; RestartSec = 5; - LoadCredential = "webhook-secret:${webhookSecretPath}"; + LoadCredential = "webhook-secret:${cfg.webhookSecretFile}"; }; script = '' if [ ! -s "$CREDENTIALS_DIRECTORY/webhook-secret" ]; then @@ -131,51 +223,21 @@ in fi export WEBHOOK_SECRET="$(cat "$CREDENTIALS_DIRECTORY/webhook-secret")" - exec ${qaApi}/bin/jetpham-qa-api + exec ${lib.getExe qaApi} ''; }; - services.caddy.virtualHosts.${cfg.domain} = { - extraConfig = '' - header Cross-Origin-Opener-Policy "same-origin" - header Cross-Origin-Embedder-Policy "require-corp" - - handle /api/* { - reverse_proxy 127.0.0.1:3003 - } - - handle /qa/rss.xml { - reverse_proxy 127.0.0.1:3003 - } - - handle { - root * ${package} - try_files {path} /index.html - file_server - } - ''; + services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable { + extraConfig = caddyCommonConfig; }; - services.caddy.virtualHosts."http://:8888" = lib.mkIf cfg.tor.enable { - extraConfig = '' - bind 127.0.0.1 - header Cross-Origin-Opener-Policy "same-origin" - header Cross-Origin-Embedder-Policy "require-corp" - - handle /api/* { - reverse_proxy 127.0.0.1:3003 - } - - handle /qa/rss.xml { - reverse_proxy 127.0.0.1:3003 - } - - handle { - root * ${package} - try_files {path} /index.html - file_server - } - ''; - }; + services.caddy.virtualHosts."http://:${toString cfg.tor.port}" = + lib.mkIf (cfg.caddy.enable && cfg.tor.enable) + { + extraConfig = '' + bind 127.0.0.1 + ${caddyCommonConfig} + ''; + }; }; } diff --git a/tor.har b/tor.har deleted file mode 100644 index e3e3506..0000000 --- a/tor.har +++ /dev/null @@ -1,252 +0,0 @@ -{ - "log": { - "version": "1.2", - "creator": { - "name": "Firefox", - "version": "140.8.0" - }, - "browser": { - "name": "Firefox", - "version": "140.8.0" - }, - "pages": [ - { - "id": "page_1", - "pageTimings": { - "onContentLoad": 1578, - "onLoad": 1578 - }, - "startedDateTime": "2026-03-26T20:03:41.824-07:00", - "title": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/" - } - ], - "entries": [ - { - "startedDateTime": "2026-03-26T20:03:41.824-07:00", - "request": { - "bodySize": 0, - "method": "GET", - "url": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/", - "httpVersion": "HTTP/1.1", - "headers": [ - { - "name": "Host", - "value": "jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" - }, - { - "name": "Accept", - "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" - }, - { - "name": "Accept-Language", - "value": "en-US,en;q=0.5" - }, - { - "name": "Accept-Encoding", - "value": "gzip, deflate, br, zstd" - }, - { - "name": "Sec-GPC", - "value": "1" - }, - { - "name": "Connection", - "value": "keep-alive" - }, - { - "name": "Upgrade-Insecure-Requests", - "value": "1" - }, - { - "name": "Sec-Fetch-Dest", - "value": "document" - }, - { - "name": "Sec-Fetch-Mode", - "value": "navigate" - }, - { - "name": "Sec-Fetch-Site", - "value": "none" - }, - { - "name": "Priority", - "value": "u=0, i" - }, - { - "name": "Pragma", - "value": "no-cache" - }, - { - "name": "Cache-Control", - "value": "no-cache" - } - ], - "cookies": [], - "queryString": [], - "headersSize": 521 - }, - "response": { - "status": 200, - "statusText": "OK", - "httpVersion": "HTTP/1.1", - "headers": [ - { - "name": "Server", - "value": "Caddy" - }, - { - "name": "Date", - "value": "Fri, 27 Mar 2026 03:03:43 GMT" - }, - { - "name": "Content-Length", - "value": "0" - } - ], - "cookies": [], - "content": { - "mimeType": "text/plain", - "size": 0, - "text": "" - }, - "redirectURL": "", - "headersSize": 90, - "bodySize": 90 - }, - "cache": {}, - "timings": { - "blocked": 894, - "dns": 0, - "connect": 894, - "ssl": 0, - "send": 0, - "wait": 617, - "receive": 0 - }, - "time": 2405, - "_securityState": "insecure", - "serverIPAddress": "0.0.0.0", - "connection": "80", - "pageref": "page_1" - }, - { - "startedDateTime": "2026-03-26T20:03:43.440-07:00", - "request": { - "bodySize": 0, - "method": "GET", - "url": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/favicon.ico", - "httpVersion": "HTTP/1.1", - "headers": [ - { - "name": "Host", - "value": "jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" - }, - { - "name": "Accept", - "value": "image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5" - }, - { - "name": "Accept-Language", - "value": "en-US,en;q=0.5" - }, - { - "name": "Accept-Encoding", - "value": "gzip, deflate, br, zstd" - }, - { - "name": "Referer", - "value": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/" - }, - { - "name": "Sec-GPC", - "value": "1" - }, - { - "name": "Connection", - "value": "keep-alive" - }, - { - "name": "Sec-Fetch-Dest", - "value": "image" - }, - { - "name": "Sec-Fetch-Mode", - "value": "no-cors" - }, - { - "name": "Sec-Fetch-Site", - "value": "same-origin" - }, - { - "name": "Priority", - "value": "u=6" - }, - { - "name": "Pragma", - "value": "no-cache" - }, - { - "name": "Cache-Control", - "value": "no-cache" - } - ], - "cookies": [], - "queryString": [], - "headersSize": 589 - }, - "response": { - "status": 200, - "statusText": "OK", - "httpVersion": "HTTP/1.1", - "headers": [ - { - "name": "Server", - "value": "Caddy" - }, - { - "name": "Date", - "value": "Fri, 27 Mar 2026 03:03:43 GMT" - }, - { - "name": "Content-Length", - "value": "0" - } - ], - "cookies": [], - "content": { - "mimeType": "text/plain", - "size": 0, - "text": "" - }, - "redirectURL": "", - "headersSize": 90, - "bodySize": 90 - }, - "cache": {}, - "timings": { - "blocked": 0, - "dns": 0, - "connect": 0, - "ssl": 0, - "send": 0, - "wait": 604, - "receive": 0 - }, - "time": 604, - "_securityState": "insecure", - "serverIPAddress": "0.0.0.0", - "connection": "80", - "pageref": "page_1" - } - ] - } -} \ No newline at end of file