mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
515 lines
18 KiB
JavaScript
515 lines
18 KiB
JavaScript
const { ipcRenderer, contextBridge } = require("electron");
|
|
|
|
const dataListeners = new Map();
|
|
const exitListeners = new Map();
|
|
const transferProgressListeners = new Map();
|
|
const transferCompleteListeners = new Map();
|
|
const transferErrorListeners = new Map();
|
|
const chainProgressListeners = new Map();
|
|
const authFailedListeners = new Map();
|
|
const languageChangeListeners = new Set();
|
|
const fullscreenChangeListeners = new Set();
|
|
const updateStatusListeners = new Set();
|
|
|
|
ipcRenderer.on("netcatty:data", (_event, payload) => {
|
|
const set = dataListeners.get(payload.sessionId);
|
|
if (!set) return;
|
|
set.forEach((cb) => {
|
|
try {
|
|
cb(payload.data);
|
|
} catch (err) {
|
|
console.error("Data callback failed", err);
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:exit", (_event, payload) => {
|
|
const set = exitListeners.get(payload.sessionId);
|
|
if (set) {
|
|
set.forEach((cb) => {
|
|
try {
|
|
cb(payload);
|
|
} catch (err) {
|
|
console.error("Exit callback failed", err);
|
|
}
|
|
});
|
|
}
|
|
dataListeners.delete(payload.sessionId);
|
|
exitListeners.delete(payload.sessionId);
|
|
});
|
|
|
|
// Chain progress events (for jump host connections)
|
|
ipcRenderer.on("netcatty:chain:progress", (_event, payload) => {
|
|
const { hop, total, label, status } = payload;
|
|
// Notify all registered chain progress listeners
|
|
chainProgressListeners.forEach((cb) => {
|
|
try {
|
|
cb(hop, total, label, status);
|
|
} catch (err) {
|
|
console.error("Chain progress callback failed", err);
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:languageChanged", (_event, language) => {
|
|
languageChangeListeners.forEach((cb) => {
|
|
try {
|
|
cb(language);
|
|
} catch (err) {
|
|
console.error("Language changed callback failed", err);
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:window:fullscreen-changed", (_event, isFullscreen) => {
|
|
fullscreenChangeListeners.forEach((cb) => {
|
|
try {
|
|
cb(isFullscreen);
|
|
} catch (err) {
|
|
console.error("Fullscreen changed callback failed", err);
|
|
}
|
|
});
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:update:status", (_event, payload) => {
|
|
updateStatusListeners.forEach((cb) => {
|
|
try {
|
|
cb(payload);
|
|
} catch (err) {
|
|
console.error("Update status callback failed", err);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Authentication failed events
|
|
ipcRenderer.on("netcatty:auth:failed", (_event, payload) => {
|
|
const set = authFailedListeners.get(payload.sessionId);
|
|
if (set) {
|
|
set.forEach((cb) => {
|
|
try {
|
|
cb(payload);
|
|
} catch (err) {
|
|
console.error("Auth failed callback failed", err);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Transfer progress events
|
|
ipcRenderer.on("netcatty:transfer:progress", (_event, payload) => {
|
|
const cb = transferProgressListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb(payload.transferred, payload.totalBytes, payload.speed);
|
|
} catch (err) {
|
|
console.error("Transfer progress callback failed", err);
|
|
}
|
|
}
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:transfer:complete", (_event, payload) => {
|
|
const cb = transferCompleteListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb();
|
|
} catch (err) {
|
|
console.error("Transfer complete callback failed", err);
|
|
}
|
|
}
|
|
// Cleanup listeners
|
|
transferProgressListeners.delete(payload.transferId);
|
|
transferCompleteListeners.delete(payload.transferId);
|
|
transferErrorListeners.delete(payload.transferId);
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:transfer:error", (_event, payload) => {
|
|
const cb = transferErrorListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb(payload.error);
|
|
} catch (err) {
|
|
console.error("Transfer error callback failed", err);
|
|
}
|
|
}
|
|
// Cleanup listeners
|
|
transferProgressListeners.delete(payload.transferId);
|
|
transferCompleteListeners.delete(payload.transferId);
|
|
transferErrorListeners.delete(payload.transferId);
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:transfer:cancelled", (_event, payload) => {
|
|
// Just cleanup listeners, the UI already knows it's cancelled
|
|
transferProgressListeners.delete(payload.transferId);
|
|
transferCompleteListeners.delete(payload.transferId);
|
|
transferErrorListeners.delete(payload.transferId);
|
|
});
|
|
|
|
// Upload with progress listeners
|
|
const uploadProgressListeners = new Map();
|
|
const uploadCompleteListeners = new Map();
|
|
const uploadErrorListeners = new Map();
|
|
|
|
ipcRenderer.on("netcatty:upload:progress", (_event, payload) => {
|
|
const cb = uploadProgressListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb(payload.transferred, payload.totalBytes, payload.speed);
|
|
} catch (err) {
|
|
console.error("Upload progress callback failed", err);
|
|
}
|
|
}
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:upload:complete", (_event, payload) => {
|
|
const cb = uploadCompleteListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb();
|
|
} catch (err) {
|
|
console.error("Upload complete callback failed", err);
|
|
}
|
|
}
|
|
// Cleanup listeners
|
|
uploadProgressListeners.delete(payload.transferId);
|
|
uploadCompleteListeners.delete(payload.transferId);
|
|
uploadErrorListeners.delete(payload.transferId);
|
|
});
|
|
|
|
ipcRenderer.on("netcatty:upload:error", (_event, payload) => {
|
|
const cb = uploadErrorListeners.get(payload.transferId);
|
|
if (cb) {
|
|
try {
|
|
cb(payload.error);
|
|
} catch (err) {
|
|
console.error("Upload error callback failed", err);
|
|
}
|
|
}
|
|
// Cleanup listeners
|
|
uploadProgressListeners.delete(payload.transferId);
|
|
uploadCompleteListeners.delete(payload.transferId);
|
|
uploadErrorListeners.delete(payload.transferId);
|
|
});
|
|
|
|
// Port forwarding status listeners
|
|
const portForwardStatusListeners = new Map();
|
|
|
|
ipcRenderer.on("netcatty:portforward:status", (_event, payload) => {
|
|
const { tunnelId, status, error } = payload;
|
|
const callbacks = portForwardStatusListeners.get(tunnelId);
|
|
if (callbacks) {
|
|
callbacks.forEach((cb) => {
|
|
try {
|
|
cb(status, error);
|
|
} catch (err) {
|
|
console.error("Port forward status callback failed", err);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const api = {
|
|
startSSHSession: async (options) => {
|
|
const result = await ipcRenderer.invoke("netcatty:start", options);
|
|
return result.sessionId;
|
|
},
|
|
startTelnetSession: async (options) => {
|
|
const result = await ipcRenderer.invoke("netcatty:telnet:start", options);
|
|
return result.sessionId;
|
|
},
|
|
startMoshSession: async (options) => {
|
|
const result = await ipcRenderer.invoke("netcatty:mosh:start", options);
|
|
return result.sessionId;
|
|
},
|
|
startLocalSession: async (options) => {
|
|
const result = await ipcRenderer.invoke("netcatty:local:start", options || {});
|
|
return result.sessionId;
|
|
},
|
|
writeToSession: (sessionId, data) => {
|
|
ipcRenderer.send("netcatty:write", { sessionId, data });
|
|
},
|
|
execCommand: async (options) => {
|
|
return ipcRenderer.invoke("netcatty:ssh:exec", options);
|
|
},
|
|
generateKeyPair: async (options) => {
|
|
return ipcRenderer.invoke("netcatty:key:generate", options);
|
|
},
|
|
resizeSession: (sessionId, cols, rows) => {
|
|
ipcRenderer.send("netcatty:resize", { sessionId, cols, rows });
|
|
},
|
|
closeSession: (sessionId) => {
|
|
ipcRenderer.send("netcatty:close", { sessionId });
|
|
},
|
|
onSessionData: (sessionId, cb) => {
|
|
if (!dataListeners.has(sessionId)) dataListeners.set(sessionId, new Set());
|
|
dataListeners.get(sessionId).add(cb);
|
|
return () => dataListeners.get(sessionId)?.delete(cb);
|
|
},
|
|
onSessionExit: (sessionId, cb) => {
|
|
if (!exitListeners.has(sessionId)) exitListeners.set(sessionId, new Set());
|
|
exitListeners.get(sessionId).add(cb);
|
|
return () => exitListeners.get(sessionId)?.delete(cb);
|
|
},
|
|
onAuthFailed: (sessionId, cb) => {
|
|
if (!authFailedListeners.has(sessionId)) authFailedListeners.set(sessionId, new Set());
|
|
authFailedListeners.get(sessionId).add(cb);
|
|
return () => authFailedListeners.get(sessionId)?.delete(cb);
|
|
},
|
|
openSftp: async (options) => {
|
|
const result = await ipcRenderer.invoke("netcatty:sftp:open", options);
|
|
return result.sftpId;
|
|
},
|
|
listSftp: async (sftpId, path) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:list", { sftpId, path });
|
|
},
|
|
readSftp: async (sftpId, path) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:read", { sftpId, path });
|
|
},
|
|
writeSftp: async (sftpId, path, content) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:write", { sftpId, path, content });
|
|
},
|
|
closeSftp: async (sftpId) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:close", { sftpId });
|
|
},
|
|
mkdirSftp: async (sftpId, path) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:mkdir", { sftpId, path });
|
|
},
|
|
deleteSftp: async (sftpId, path) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:delete", { sftpId, path });
|
|
},
|
|
renameSftp: async (sftpId, oldPath, newPath) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:rename", { sftpId, oldPath, newPath });
|
|
},
|
|
statSftp: async (sftpId, path) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:stat", { sftpId, path });
|
|
},
|
|
chmodSftp: async (sftpId, path, mode) => {
|
|
return ipcRenderer.invoke("netcatty:sftp:chmod", { sftpId, path, mode });
|
|
},
|
|
// Write binary with real-time progress callback
|
|
writeSftpBinaryWithProgress: async (sftpId, path, content, transferId, onProgress, onComplete, onError) => {
|
|
// Register callbacks
|
|
if (onProgress) uploadProgressListeners.set(transferId, onProgress);
|
|
if (onComplete) uploadCompleteListeners.set(transferId, onComplete);
|
|
if (onError) uploadErrorListeners.set(transferId, onError);
|
|
|
|
return ipcRenderer.invoke("netcatty:sftp:writeBinaryWithProgress", {
|
|
sftpId,
|
|
path,
|
|
content,
|
|
transferId
|
|
});
|
|
},
|
|
// Local filesystem operations
|
|
listLocalDir: async (path) => {
|
|
return ipcRenderer.invoke("netcatty:local:list", { path });
|
|
},
|
|
readLocalFile: async (path) => {
|
|
return ipcRenderer.invoke("netcatty:local:read", { path });
|
|
},
|
|
writeLocalFile: async (path, content) => {
|
|
return ipcRenderer.invoke("netcatty:local:write", { path, content });
|
|
},
|
|
deleteLocalFile: async (path) => {
|
|
return ipcRenderer.invoke("netcatty:local:delete", { path });
|
|
},
|
|
renameLocalFile: async (oldPath, newPath) => {
|
|
return ipcRenderer.invoke("netcatty:local:rename", { oldPath, newPath });
|
|
},
|
|
mkdirLocal: async (path) => {
|
|
return ipcRenderer.invoke("netcatty:local:mkdir", { path });
|
|
},
|
|
statLocal: async (path) => {
|
|
return ipcRenderer.invoke("netcatty:local:stat", { path });
|
|
},
|
|
getHomeDir: async () => {
|
|
return ipcRenderer.invoke("netcatty:local:homedir");
|
|
},
|
|
getSystemInfo: async () => {
|
|
return ipcRenderer.invoke("netcatty:system:info");
|
|
},
|
|
// Read system known_hosts file
|
|
readKnownHosts: async () => {
|
|
return ipcRenderer.invoke("netcatty:known-hosts:read");
|
|
},
|
|
setTheme: async (theme) => {
|
|
return ipcRenderer.invoke("netcatty:setTheme", theme);
|
|
},
|
|
setBackgroundColor: async (color) => {
|
|
return ipcRenderer.invoke("netcatty:setBackgroundColor", color);
|
|
},
|
|
setLanguage: async (language) => {
|
|
return ipcRenderer.invoke("netcatty:setLanguage", language);
|
|
},
|
|
onLanguageChanged: (cb) => {
|
|
languageChangeListeners.add(cb);
|
|
return () => languageChangeListeners.delete(cb);
|
|
},
|
|
// Streaming transfer with real progress
|
|
startStreamTransfer: async (options, onProgress, onComplete, onError) => {
|
|
const { transferId } = options;
|
|
// Register callbacks
|
|
if (onProgress) transferProgressListeners.set(transferId, onProgress);
|
|
if (onComplete) transferCompleteListeners.set(transferId, onComplete);
|
|
if (onError) transferErrorListeners.set(transferId, onError);
|
|
|
|
return ipcRenderer.invoke("netcatty:transfer:start", options);
|
|
},
|
|
cancelTransfer: async (transferId) => {
|
|
// Cleanup listeners
|
|
transferProgressListeners.delete(transferId);
|
|
transferCompleteListeners.delete(transferId);
|
|
transferErrorListeners.delete(transferId);
|
|
return ipcRenderer.invoke("netcatty:transfer:cancel", { transferId });
|
|
},
|
|
// Window controls for custom title bar
|
|
windowMinimize: () => ipcRenderer.invoke("netcatty:window:minimize"),
|
|
windowMaximize: () => ipcRenderer.invoke("netcatty:window:maximize"),
|
|
windowClose: () => ipcRenderer.invoke("netcatty:window:close"),
|
|
windowIsMaximized: () => ipcRenderer.invoke("netcatty:window:isMaximized"),
|
|
windowIsFullscreen: () => ipcRenderer.invoke("netcatty:window:isFullscreen"),
|
|
onWindowFullScreenChanged: (cb) => {
|
|
fullscreenChangeListeners.add(cb);
|
|
return () => fullscreenChangeListeners.delete(cb);
|
|
},
|
|
onUpdateStatus: (cb) => {
|
|
updateStatusListeners.add(cb);
|
|
return () => updateStatusListeners.delete(cb);
|
|
},
|
|
|
|
// Settings window
|
|
openSettingsWindow: () => ipcRenderer.invoke("netcatty:settings:open"),
|
|
closeSettingsWindow: () => ipcRenderer.invoke("netcatty:settings:close"),
|
|
|
|
// Cross-window settings sync
|
|
notifySettingsChanged: (payload) => ipcRenderer.send("netcatty:settings:changed", payload),
|
|
onSettingsChanged: (callback) => {
|
|
const handler = (_event, payload) => callback(payload);
|
|
ipcRenderer.on("netcatty:settings:changed", handler);
|
|
return () => ipcRenderer.removeListener("netcatty:settings:changed", handler);
|
|
},
|
|
|
|
// Cloud sync session (in-memory only, shared across windows)
|
|
cloudSyncSetSessionPassword: (password) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:session:setPassword", password),
|
|
cloudSyncGetSessionPassword: () =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:session:getPassword"),
|
|
cloudSyncClearSessionPassword: () =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:session:clearPassword"),
|
|
|
|
// Cloud sync network operations (proxied via main process)
|
|
cloudSyncWebdavInitialize: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:webdav:initialize", { config }),
|
|
cloudSyncWebdavUpload: (config, syncedFile) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:webdav:upload", { config, syncedFile }),
|
|
cloudSyncWebdavDownload: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:webdav:download", { config }),
|
|
cloudSyncWebdavDelete: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:webdav:delete", { config }),
|
|
|
|
cloudSyncS3Initialize: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:s3:initialize", { config }),
|
|
cloudSyncS3Upload: (config, syncedFile) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:s3:upload", { config, syncedFile }),
|
|
cloudSyncS3Download: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:s3:download", { config }),
|
|
cloudSyncS3Delete: (config) =>
|
|
ipcRenderer.invoke("netcatty:cloudSync:s3:delete", { config }),
|
|
|
|
// Open URL in default browser
|
|
openExternal: (url) => ipcRenderer.invoke("netcatty:openExternal", url),
|
|
|
|
// App info
|
|
getAppInfo: () => ipcRenderer.invoke("netcatty:app:getInfo"),
|
|
getUpdateStatus: () => ipcRenderer.invoke("netcatty:update:getStatus"),
|
|
updateCheck: () => ipcRenderer.invoke("netcatty:update:check"),
|
|
downloadUpdate: () => ipcRenderer.invoke("netcatty:update:download"),
|
|
installUpdate: () => ipcRenderer.invoke("netcatty:update:install"),
|
|
|
|
// Tell main process the renderer has mounted/painted (used to avoid initial blank screen).
|
|
rendererReady: () => ipcRenderer.send("netcatty:renderer:ready"),
|
|
|
|
// Port Forwarding API
|
|
startPortForward: async (options) => {
|
|
return ipcRenderer.invoke("netcatty:portforward:start", options);
|
|
},
|
|
stopPortForward: async (tunnelId) => {
|
|
return ipcRenderer.invoke("netcatty:portforward:stop", { tunnelId });
|
|
},
|
|
getPortForwardStatus: async (tunnelId) => {
|
|
return ipcRenderer.invoke("netcatty:portforward:status", { tunnelId });
|
|
},
|
|
listPortForwards: async () => {
|
|
return ipcRenderer.invoke("netcatty:portforward:list");
|
|
},
|
|
onPortForwardStatus: (tunnelId, cb) => {
|
|
if (!portForwardStatusListeners.has(tunnelId)) {
|
|
portForwardStatusListeners.set(tunnelId, new Set());
|
|
}
|
|
portForwardStatusListeners.get(tunnelId).add(cb);
|
|
return () => {
|
|
portForwardStatusListeners.get(tunnelId)?.delete(cb);
|
|
if (portForwardStatusListeners.get(tunnelId)?.size === 0) {
|
|
portForwardStatusListeners.delete(tunnelId);
|
|
}
|
|
};
|
|
},
|
|
// Chain progress listener for jump host connections
|
|
onChainProgress: (cb) => {
|
|
const id = Date.now().toString() + Math.random().toString(16).slice(2);
|
|
chainProgressListeners.set(id, cb);
|
|
return () => {
|
|
chainProgressListeners.delete(id);
|
|
};
|
|
},
|
|
|
|
// OAuth callback server
|
|
startOAuthCallback: (expectedState) => ipcRenderer.invoke("oauth:startCallback", expectedState),
|
|
cancelOAuthCallback: () => ipcRenderer.invoke("oauth:cancelCallback"),
|
|
|
|
// GitHub Device Flow (proxied via main process to avoid CORS)
|
|
githubStartDeviceFlow: (options) => ipcRenderer.invoke("netcatty:github:deviceFlow:start", options),
|
|
githubPollDeviceFlowToken: (options) => ipcRenderer.invoke("netcatty:github:deviceFlow:poll", options),
|
|
|
|
// Google OAuth (proxied via main process to avoid CORS)
|
|
googleExchangeCodeForTokens: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:oauth:exchange", options),
|
|
googleRefreshAccessToken: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:oauth:refresh", options),
|
|
googleGetUserInfo: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:oauth:userinfo", options),
|
|
|
|
// Google Drive API (proxied via main process to avoid CORS/COEP issues in renderer)
|
|
googleDriveFindSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:drive:findSyncFile", options),
|
|
googleDriveCreateSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:drive:createSyncFile", options),
|
|
googleDriveUpdateSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:drive:updateSyncFile", options),
|
|
googleDriveDownloadSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:drive:downloadSyncFile", options),
|
|
googleDriveDeleteSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:google:drive:deleteSyncFile", options),
|
|
|
|
// OneDrive OAuth + Graph (proxied via main process to avoid CORS)
|
|
onedriveExchangeCodeForTokens: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:oauth:exchange", options),
|
|
onedriveRefreshAccessToken: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:oauth:refresh", options),
|
|
onedriveGetUserInfo: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:oauth:userinfo", options),
|
|
onedriveFindSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:drive:findSyncFile", options),
|
|
onedriveUploadSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:drive:uploadSyncFile", options),
|
|
onedriveDownloadSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:drive:downloadSyncFile", options),
|
|
onedriveDeleteSyncFile: (options) =>
|
|
ipcRenderer.invoke("netcatty:onedrive:drive:deleteSyncFile", options),
|
|
};
|
|
|
|
// Merge with existing netcatty (if any) to avoid stale objects on hot reload
|
|
const existing = (typeof window !== "undefined" && window.netcatty) ? window.netcatty : {};
|
|
contextBridge.exposeInMainWorld("netcatty", { ...existing, ...api });
|