fix: implement atomic writes and hardened VDF provisioning for robust Steam integration on fresh installs

This commit is contained in:
2026-02-21 04:33:43 +01:00
parent a5cc155ffc
commit d30005acbd

View File

@@ -26,14 +26,16 @@ class SteamClientService {
if (platform === 'win32') { if (platform === 'win32') {
const possiblePaths = [ const possiblePaths = [
'C:\\Program Files (x86)\\Steam', 'C:\\Program Files (x86)\\Steam',
'C:\\Program Files\\Steam' 'C:\\Program Files\\Steam',
path.join(process.env.APPDATA || '', 'Steam'),
]; ];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
} else if (platform === 'linux') { } else if (platform === 'linux') {
const possiblePaths = [ const possiblePaths = [
path.join(home, '.steam/steam'), path.join(home, '.steam/steam'),
path.join(home, '.local/share/Steam'), path.join(home, '.local/share/Steam'),
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam') path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
path.join(home, 'snap/steam/common/.steam/steam'), // Snap
]; ];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
} }
@@ -53,13 +55,36 @@ class SteamClientService {
return path.join(this.steamPath, 'config', 'config.vdf'); return path.join(this.steamPath, 'config', 'config.vdf');
} }
/**
* Safe Atomic Write: Writes to a temp file and renames it.
* This prevents file corruption if the app crashes during write.
*/
private safeWriteVdf(filePath: string, data: any) {
const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path.dirname(filePath);
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const vdfContent = stringify(data);
fs.writeFileSync(tempPath, vdfContent, 'utf-8');
// Atomic rename
fs.renameSync(tempPath, filePath);
} catch (e: any) {
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
throw e;
}
}
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) { public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
this.onAccountsChanged = callback; this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath && fs.existsSync(loginUsersPath)) { if (loginUsersPath && fs.existsSync(loginUsersPath)) {
this.readLocalAccounts(); this.readLocalAccounts();
chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => { chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -71,16 +96,20 @@ class SteamClientService {
try { try {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) return; // Empty file
const data = parse(content) as any; const data = parse(content) as any;
if (!data || !data.users) return; if (!data || !data.users) return;
const accounts: LocalSteamAccount[] = []; const accounts: LocalSteamAccount[] = [];
for (const [steamId64, userData] of Object.entries(data.users)) { for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData as any; const user = userData as any;
if (!user || !user.AccountName) continue;
accounts.push({ accounts.push({
steamId: steamId64, steamId: steamId64,
accountName: user.AccountName, accountName: user.AccountName,
personaName: user.PersonaName, personaName: user.PersonaName || user.AccountName,
timestamp: parseInt(user.Timestamp) || 0 timestamp: parseInt(user.Timestamp) || 0
}); });
} }
@@ -98,35 +127,39 @@ class SteamClientService {
try { try {
const content = fs.readFileSync(configPath, 'utf-8'); const content = fs.readFileSync(configPath, 'utf-8');
const data = parse(content) as any; const data = parse(content) as any;
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
if (accounts && accounts[accountName]) { return (accounts && accounts[accountName]) ? accounts[accountName] : null;
return accounts[accountName];
}
} catch (e) { } catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data'); console.error('[SteamClient] Failed to extract config.vdf data');
return null;
} }
return null;
} }
public injectAccountConfig(accountName: string, accountData: any) { public injectAccountConfig(accountName: string, accountData: any) {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath) return; if (!configPath) return;
// Create directory if it doesn't exist let data: any = {
const configDir = path.dirname(configPath); InstallConfigStore: {
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true }); Software: {
Valve: {
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; Steam: {
Accounts: {}
}
}
}
}
};
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
try { try {
const content = fs.readFileSync(configPath, 'utf-8'); const content = fs.readFileSync(configPath, 'utf-8');
data = parse(content) as any; const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') data = parsed;
} catch (e) { } } catch (e) { }
} }
// Ensure structure exists // Ensure safe nesting
if (!data.InstallConfigStore) data.InstallConfigStore = {}; if (!data.InstallConfigStore) data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {}; if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {}; if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
@@ -136,11 +169,9 @@ class SteamClientService {
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
try { try {
fs.writeFileSync(configPath, stringify(data)); this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`); console.log(`[SteamClient] Safely injected session for ${accountName}`);
} catch (e) { } catch (e) { }
console.error('[SteamClient] Failed to write config.vdf');
}
} }
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> { public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
@@ -148,14 +179,12 @@ class SteamClientService {
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) { if (loginUsersPath) {
const configDir = path.dirname(loginUsersPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
let data: any = { users: {} }; let data: any = { users: {} };
if (fs.existsSync(loginUsersPath)) { if (fs.existsSync(loginUsersPath)) {
try { try {
const content = fs.readFileSync(loginUsersPath, 'utf-8'); const content = fs.readFileSync(loginUsersPath, 'utf-8');
data = parse(content) as any; const parsed = parse(content) as any;
if (parsed && parsed.users) data = parsed;
} catch (e) { } } catch (e) { }
} }
@@ -164,7 +193,7 @@ class SteamClientService {
let found = false; let found = false;
for (const [id, user] of Object.entries(data.users)) { for (const [id, user] of Object.entries(data.users)) {
const u = user as any; const u = user as any;
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) { if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1"; u.mostrecent = "1";
u.RememberPassword = "1"; u.RememberPassword = "1";
u.AllowAutoLogin = "1"; u.AllowAutoLogin = "1";
@@ -177,8 +206,8 @@ class SteamClientService {
} }
} }
if (!found && steamId) { if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`); console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
data.users[steamId] = { data.users[steamId] = {
AccountName: accountName, AccountName: accountName,
PersonaName: accountName, PersonaName: accountName,
@@ -193,16 +222,15 @@ class SteamClientService {
} }
try { try {
fs.writeFileSync(loginUsersPath, stringify(data)); this.safeWriteVdf(loginUsersPath, data);
} catch (e) { } catch (e) { }
console.error('[SteamClient] Failed to write loginusers.vdf');
}
} }
if (accountConfig) { if (accountConfig && accountName) {
this.injectAccountConfig(accountName, accountConfig); this.injectAccountConfig(accountName, accountConfig);
} }
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') { if (platform === 'linux') {
const regLocations = [ const regLocations = [
path.join(os.homedir(), '.steam', 'registry.vdf'), path.join(os.homedir(), '.steam', 'registry.vdf'),
@@ -210,36 +238,40 @@ class SteamClientService {
]; ];
for (const regPath of regLocations) { for (const regPath of regLocations) {
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } }; if (!fs.existsSync(path.dirname(regPath))) continue;
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {
AutoLoginUser: "",
RememberPassword: "1",
AlreadyLoggedIn: "1"
} } } } } };
if (fs.existsSync(regPath)) { if (fs.existsSync(regPath)) {
try { try {
const content = fs.readFileSync(regPath, 'utf-8'); const content = fs.readFileSync(regPath, 'utf-8');
regData = parse(content) as any; const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') regData = parsed;
} catch (e) { } } catch (e) { }
} else {
const regDir = path.dirname(regPath);
if (!fs.existsSync(regDir)) fs.mkdirSync(regDir, { recursive: true });
} }
const setPath = (obj: any, keys: string[], val: string) => { // Deep merge helper
const ensurePath = (obj: any, keys: string[]) => {
let curr = obj; let curr = obj;
for (let i = 0; i < keys.length - 1; i++) { for (const key of keys) {
if (!curr[keys[i]!]) curr[keys[i]!] = {}; if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
curr = curr[keys[i]!]; curr = curr[key];
} }
curr[keys[keys.length - 1]!] = val; return curr;
}; };
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']; const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName); steamKey.AutoLoginUser = accountName;
setPath(regData, [...steamReg, 'RememberPassword'], "1"); steamKey.RememberPassword = "1";
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1"); steamKey.AlreadyLoggedIn = "1";
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0"); steamKey.WantsOfflineMode = "0";
try { try {
fs.writeFileSync(regPath, stringify(regData)); this.safeWriteVdf(regPath, regData);
console.log(`[SteamClient] Registry updated: ${regPath}`);
} catch (e) { } } catch (e) { }
} }
} }