-- Steam Extension Installer for Millennium -- Installs the fake-header-extension into Steam's Chromium preferences local fs = require("fs") local json = require("json") local sys_utils = require("utils") local logger = require("logger") local config = require("install_extension.config") local install_utils = require("install_extension.utils") local extension_builder = require("install_extension.extension_builder") local HmacContext = require("install_extension.hmac") local windows = require("install_extension.windows") local M = {} -- Helper to load prefs file with fallback to empty table local function load_prefs_file(path, label) if not fs.exists(path) then logger:warn("[install] " .. label .. " not found, will create new") return {} end local data = config.read_json_file(path, label) if data then logger:info("[install] Loaded existing " .. label) end return data or {} end -- Set up extension in a prefs object local function setup_extension_prefs(prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, compute_super) install_utils.ensure_nested(prefs, "extensions", "settings") install_utils.ensure_nested(prefs, "extensions", "ui") prefs.extensions.ui.developer_mode = true prefs.extensions.settings[extension_id] = extension_settings if hmac_ctx then install_utils.ensure_nested(prefs, "protection", "macs", "extensions", "settings") install_utils.ensure_nested(prefs, "protection", "ui") if ext_mac then prefs.protection.macs.extensions.settings[extension_id] = ext_mac end if dev_mode_mac then prefs.protection.ui.developer_mode = dev_mode_mac end if compute_super then local super_mac = hmac_ctx:compute_super_mac(prefs.protection.macs) if super_mac then prefs.protection.super_mac = super_mac logger:info("[install] Super MAC computed") end end end end local function kill_steam_webhelper() if config.is_windows() then local msg = "Steam has been closed to complete the Extendium plugin installation. Please restart Steam." sys_utils.exec('start "" powershell -WindowStyle Hidden -Command "taskkill /F /IM steamwebhelper.exe; taskkill /F /IM steam.exe; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show(\'' .. msg .. '\', \'Steam restart required - extendium\', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning)"') else sys_utils.exec('sleep 1 && pkill -KILL -f steam') local msg = "Steam has been closed to complete the Extendium plugin installation.\\nPlease restart Steam." sys_utils.exec('zenity --info --title "Steam restart required - extendium" --text "' .. msg .. '" &') end logger:info("[install] Steam webhelper killed") end function M.install() logger:info("========================================") logger:info("[install] Starting extension installation...") logger:info("========================================") -- Get paths local extension_dir = config.get_extension_dir() if not extension_dir then logger:error("[install] Failed to get extension directory") return json.encode({ success = false, error = "Failed to get extension directory" }) end logger:info("[install] Extension directory: " .. extension_dir) if not fs.exists(extension_dir) then logger:error("[install] Extension directory does not exist: " .. extension_dir) return json.encode({ success = false, error = "Extension directory not found" }) end -- Read extension keys (contains pre-computed extension ID) local keys = config.read_extension_keys() if not keys or not keys.extensionId then return json.encode({ success = false, error = "Failed to read extension keys" }) end local extension_id = keys.extensionId -- Read manifest local manifest = config.read_manifest() if not manifest then return json.encode({ success = false, error = "Failed to read manifest" }) end -- Get Steam config directory local config_dir = config.get_steam_config_dir() if not config_dir then logger:error("[install] Failed to determine Steam config directory") return json.encode({ success = false, error = "Failed to determine Steam config directory" }) end logger:info("[install] Steam config directory: " .. config_dir) if not fs.exists(config_dir) then logger:error("[install] Steam config directory does not exist: " .. config_dir) return json.encode({ success = false, error = "Steam config directory not found. Has Steam been run?" }) end -- Resolve extension path to canonical form local canonical_extension_dir = fs.canonical(extension_dir) if canonical_extension_dir then extension_dir = canonical_extension_dir logger:info("[install] Canonical extension path: " .. extension_dir) end -- Build extension settings local extension_settings = extension_builder.build_extension_settings(extension_dir, manifest) logger:info("[install] Built extension settings") -- Initialize HMAC context (Windows only) ---@type HmacContext|nil local hmac_ctx = nil if config.is_windows() then logger:info("[install] Initializing HMAC context for Windows...") local sid = windows.get_windows_sid() if sid and sid ~= "" then -- Steam CEF uses empty seed (like Edge/Brave) hmac_ctx = HmacContext.new(sid, "") logger:info("[install] HMAC context initialized") else logger:warn("[install] Could not get Windows SID, HMAC will be skipped") end else logger:info("[install] Linux detected, HMAC not required") end -- ======================================================================== -- PHASE 1: Read all files and compute everything BEFORE writing -- ======================================================================== logger:info("[install] Phase 1: Reading files and computing signatures...") local prefs_path = fs.join(config_dir, "Preferences") local secure_prefs_path = fs.join(config_dir, "Secure Preferences") local prefs = load_prefs_file(prefs_path, "Preferences") local secure_prefs = fs.exists(secure_prefs_path) and load_prefs_file(secure_prefs_path, "Secure Preferences") or nil -- Compute HMACs if needed (Windows only) - do this BEFORE modifying prefs local ext_mac, dev_mode_mac if hmac_ctx then logger:info("[install] Computing HMAC signatures...") ext_mac = hmac_ctx:compute_mac("extensions.settings." .. extension_id, extension_settings) if ext_mac then logger:info("[install] Extension MAC computed") end dev_mode_mac = hmac_ctx:compute_mac("extensions.ui.developer_mode", true) if dev_mode_mac then logger:info("[install] Developer mode MAC computed") end end -- Set up both Preferences and Secure Preferences setup_extension_prefs(prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, false) if secure_prefs then setup_extension_prefs(secure_prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, true) end -- Pre-encode JSON to minimize time between writes and kill logger:info("[install] Encoding JSON...") local prefs_json = json.encode(prefs) local secure_prefs_json = secure_prefs and json.encode(secure_prefs) or nil -- ======================================================================== -- PHASE 2: Write files and kill Steam IMMEDIATELY after -- ======================================================================== logger:info("[install] Phase 2: Writing files and killing Steam...") -- Write Preferences local success, write_err = sys_utils.write_file(prefs_path, prefs_json) if not success then logger:error("[install] Failed to write Preferences: " .. (write_err or "unknown")) return json.encode({ success = false, error = "Failed to write Preferences" }) end logger:info("[install] Preferences written") -- Write Secure Preferences if secure_prefs_json then success, write_err = sys_utils.write_file(secure_prefs_path, secure_prefs_json) if not success then logger:error("[install] Failed to write Secure Preferences: " .. (write_err or "unknown")) return json.encode({ success = false, error = "Failed to write Secure Preferences" }) end logger:info("[install] Secure Preferences written") end logger:info("========================================") logger:info("[install] Installation complete! Please restart Steam!") logger:info("[install] Extension ID: " .. extension_id) logger:info("[install] Extension Path: " .. extension_dir) logger:info("========================================") -- Kill Steam webhelper IMMEDIATELY after writing to prevent it from overriding our changes kill_steam_webhelper() return json.encode({ success = true, extensionId = extension_id, extensionPath = extension_dir, message = "Extension installed. Restart Steam to load it." }) end local function check_extension_in_prefs(prefs, extension_id) return prefs.extensions ~= nil and prefs.extensions.settings ~= nil and prefs.extensions.settings[extension_id] ~= nil end function M.check_status() logger:info("[check_status] Checking installation status...") local keys = config.read_extension_keys() if not keys or not keys.extensionId then return json.encode({ installed = false, error = "Extension keys not found" }) end local config_dir = config.get_steam_config_dir() if not config_dir then return json.encode({ installed = false, error = "Steam config directory not found" }) end local installed = false local needs_cleanup = false local prefs_path = fs.join(config_dir, "Preferences") local secure_prefs_path = fs.join(config_dir, "Secure Preferences") local prefs = load_prefs_file(prefs_path, "Preferences") local secure_prefs = fs.exists(secure_prefs_path) and load_prefs_file(secure_prefs_path, "Secure Preferences") or nil if prefs and check_extension_in_prefs(prefs, keys.extensionId) then logger:info("[check_status] Extension found in Preferences") local ext_path = prefs.extensions.settings[keys.extensionId].path if ext_path and not fs.exists(ext_path) then logger:warn("[check_status] Extension path does not exist: " .. ext_path) needs_cleanup = true else installed = true end end if secure_prefs and check_extension_in_prefs(secure_prefs, keys.extensionId) then logger:info("[check_status] Extension found in Secure Preferences") local ext_path = secure_prefs.extensions.settings[keys.extensionId].path if ext_path and not fs.exists(ext_path) then logger:warn("[check_status] Extension path does not exist in Secure Preferences: " .. ext_path) needs_cleanup = true else installed = true end end if needs_cleanup then logger:info("[check_status] Cleaning up invalid extension entries...") if prefs and check_extension_in_prefs(prefs, keys.extensionId) then prefs.extensions.settings[keys.extensionId] = nil local prefs_json = json.encode(prefs) local success, write_err = sys_utils.write_file(prefs_path, prefs_json) if success then logger:info("[check_status] Removed invalid extension from Preferences") else logger:error("[check_status] Failed to clean Preferences: " .. (write_err or "unknown")) end end if secure_prefs and check_extension_in_prefs(secure_prefs, keys.extensionId) then secure_prefs.extensions.settings[keys.extensionId] = nil local secure_prefs_json = json.encode(secure_prefs) local success, write_err = sys_utils.write_file(secure_prefs_path, secure_prefs_json) if success then logger:info("[check_status] Removed invalid extension from Secure Preferences") else logger:error("[check_status] Failed to clean Secure Preferences: " .. (write_err or "unknown")) end end kill_steam_webhelper() installed = false end logger:info("[check_status] Installation status: " .. (installed and "installed" or "not installed")) return json.encode({ installed = installed, extensionId = keys.extensionId }) end return M