Files
holesail-browser/native-host/holesail-manager/state.js
T
Raven Scott 6fcd9fcf2b
CI / Build & Test (push) Successful in 3m19s
eat: implement Holesail-Browser enhancement plan
- Add unit tests (hostname-validator, TLDs, payload-schemas) and integration tests for message handler registry
- Refactor native host message router into handler registry (handlers/state, tunnels, ssh, rdp, backup, ca, connections)
- Add ESLint config and npm test + lint steps in CI
- Dashboard: visibility-based refresh pause, configurable refresh interval (2s/5s/10s/paused)
- Accessibility: ARIA on nav and modals, focus trap and restore, prefers-reduced-motion
- Empty states: primary action buttons for virtual hosts, servers, service tunnels
- Native host rate limiting for backup and CA operations; update SECURITY.md
- CONTRIBUTING: "Adding a new dashboard page", dev workflow; add npm run dev script
2026-03-15 00:24:31 -04:00

153 lines
5.8 KiB
JavaScript

/**
* State persistence for the native host.
* Loads and saves all persistent data (tunnels, settings, connections) to state.json.
* Handles migration from the legacy holesail-persist.json format.
*/
const path = require('bare-path');
const fs = require('bare-fs');
const STATE_FILENAME = 'state.json';
const LEGACY_PERSIST_FILENAME = 'holesail-persist.json';
const SETTINGS_DEFAULTS = {
proxyPort: 8443,
connectProxyPort: 8442,
readyTimeoutMs: 30000,
notifyOnDisconnect: true,
notifyOnTunnelError: true,
debug: false,
disableOnFileUrls: false,
backupRetention: 5,
backupIntervalHours: 0,
tunnelAutoReconnect: true,
latencyPingEnabled: false,
latencyPingIntervalMs: 5000,
dashboardRefreshIntervalMs: 2000
};
const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true';
function debugLog(...args) {
if (!DEBUG) return;
const msg = '[holesail-manager:debug] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
if (process.stderr) process.stderr.write(msg + '\n');
}
let stateFilePath = null;
function setStoragePath(baseDir) {
if (baseDir && typeof baseDir === 'string') {
stateFilePath = path.join(baseDir, STATE_FILENAME);
}
}
function getStatePath() {
if (stateFilePath) return stateFilePath;
const base = process.env.HOLESAIL_BROWSER_STORAGE || path.join(__dirname, '..', 'holesail-browser-storage');
stateFilePath = path.join(base, STATE_FILENAME);
return stateFilePath;
}
/** Create the storage directory if it does not exist (e.g. fresh install on Windows). */
function ensureStorageDir() {
const dir = path.dirname(getStatePath());
try {
fs.mkdirSync(dir, { recursive: true });
} catch (e) {
if (process.stderr) process.stderr.write('[holesail-manager] could not create storage dir: ' + e.message + '\n');
}
}
function getLegacyPersistPath() {
const dir = path.dirname(getStatePath());
return path.join(dir, LEGACY_PERSIST_FILENAME);
}
function buildDefaultState() {
return {
settings: { ...SETTINGS_DEFAULTS },
servers: [],
virtualHosts: [],
serviceTunnels: [],
sshConnections: [],
rdpConnections: [],
nextServerId: 0,
nextServiceTunnelId: 0
};
}
function saveStateSync(data) {
const file = getStatePath();
const json = JSON.stringify(data, null, 2);
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, json, 'utf8');
debugLog('state saved path=', file);
} catch (e) {
if (process.stderr) process.stderr.write('[holesail-manager] state save failed: ' + e.message + ' (path: ' + file + ')\n');
}
}
function loadState() {
const file = getStatePath();
try {
const raw = fs.readFileSync(file, 'utf8');
const trimmed = raw && typeof raw === 'string' ? raw.trim() : '';
if (!trimmed) return buildDefaultState();
const data = JSON.parse(raw);
if (!data || typeof data !== 'object') return buildDefaultState();
const out = {
settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [],
serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [],
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0
};
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
process.stderr.write('[holesail-manager] state loaded from ' + file + ' (servers=' + out.servers.length + ' vhosts=' + out.virtualHosts.length + ')\n');
}
return out;
} catch (e) {
if (e && e.code !== 'ENOENT') {
if (process.stderr) process.stderr.write('[holesail-manager] state load failed: ' + e.message + '\n');
return buildDefaultState();
}
}
// state.json not found — try migrating from legacy holesail-persist.json
const legacyFile = getLegacyPersistPath();
try {
const raw = fs.readFileSync(legacyFile, 'utf8');
const trimmed = raw && typeof raw === 'string' ? raw.trim() : '';
if (!trimmed) return buildDefaultState();
const data = JSON.parse(raw);
if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n');
const out = {
settings: { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [],
serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [],
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0
};
try { fs.unlinkSync(legacyFile); } catch (_) {}
if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n');
return out;
} catch (e) {
if (e && e.code !== 'ENOENT') {
if (process.stderr) process.stderr.write('[holesail-manager] legacy persist load failed: ' + e.message + '\n');
}
}
return buildDefaultState();
}
module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState, ensureStorageDir };